From 170db69eb436e8de1df86a782dc50c303e2361c9 Mon Sep 17 00:00:00 2001 From: superlishunqin <852326703@qq.com> Date: Mon, 14 Jul 2025 05:06:05 +0800 Subject: [PATCH] version_1 --- app/__init__.py | 4 + app/static/css/cart.css | 52 ++++- app/static/css/checkout.css | 289 ++++++++++++++++++++++++-- app/static/js/cart.js | 109 +++++++++- app/static/js/checkout.js | 260 ++++++++++++++++++++--- app/templates/admin/product_form.html | 4 +- app/templates/base.html | 4 +- app/templates/cart/index.html | 24 ++- app/templates/order/checkout.html | 74 +++++-- docker/Dockerfile | 49 +++++ docker/README.md | 72 +++++++ docker/deploy.sh | 41 ++++ docker/docker-compose.yml | 56 +++++ docker/test.sh | 21 ++ requirements.txt | 1 + update_config.py | 27 +++ 16 files changed, 1003 insertions(+), 84 deletions(-) create mode 100644 docker/README.md create mode 100755 docker/deploy.sh create mode 100755 docker/test.sh create mode 100644 update_config.py diff --git a/app/__init__.py b/app/__init__.py index cc1dc88..0c49691 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,5 +1,6 @@ from flask import Flask from config.database import init_db +from app.utils.email_service import mail from config.config import Config def create_app(config_name=None): @@ -9,6 +10,9 @@ def create_app(config_name=None): # 初始化数据库 init_db(app) + # 初始化邮件服务 + mail.init_app(app) + # 注册蓝图 from app.views.auth import auth_bp from app.views.main import main_bp diff --git a/app/static/css/cart.css b/app/static/css/cart.css index 665396c..5e290a4 100644 --- a/app/static/css/cart.css +++ b/app/static/css/cart.css @@ -7,7 +7,57 @@ } .quantity-input { - width: 60px; + width: 50px !important; + 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 { diff --git a/app/static/css/checkout.css b/app/static/css/checkout.css index aefe9d2..92e5316 100644 --- a/app/static/css/checkout.css +++ b/app/static/css/checkout.css @@ -3,22 +3,239 @@ margin-bottom: 1.5rem; } +/* 地址选择区域美化 */ +.address-container { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 1rem; + margin-top: 1rem; +} + .address-card { + position: relative; cursor: pointer; transition: all 0.3s ease; 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 { border-color: #007bff; - box-shadow: 0 2px 8px rgba(0, 123, 255, 0.1); + box-shadow: 0 4px 12px rgba(0, 123, 255, 0.15); + transform: translateY(-2px); } .address-card.selected { border-color: #007bff; - background-color: #e7f3ff; + background: linear-gradient(135deg, #e7f3ff 0%, #cce7ff 100%); + 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 { padding: 1rem 0; border-bottom: 1px solid #e9ecef; @@ -79,12 +296,13 @@ /* 响应式设计 */ @media (max-width: 768px) { - .checkout-section .col-md-4, - .checkout-section .col-md-3 { - margin-bottom: 1rem; + .address-container { + grid-template-columns: 1fr; + gap: 0.75rem; } - .address-card { + .checkout-section .col-md-4, + .checkout-section .col-md-3 { margin-bottom: 1rem; } @@ -92,6 +310,14 @@ .product-item .col-md-6 { margin-bottom: 0.5rem; } + + .address-card-body { + padding: 1rem; + } + + .receiver-name { + font-size: 1rem; + } } /* 动画效果 */ @@ -106,27 +332,39 @@ } } +@keyframes slideIn { + from { + opacity: 0; + transform: translateX(-20px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + .alert { 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 { padding: 0.75rem 1.5rem; font-size: 1.1rem; } -/* 卡片头部样式 */ -.card-header h5 { - margin-bottom: 0; - color: #495057; -} - -.card-header i { - margin-right: 0.5rem; - color: #007bff; -} - /* 表单标签样式 */ .form-check-label { cursor: pointer; @@ -162,3 +400,20 @@ .breadcrumb-item + .breadcrumb-item::before { 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; +} diff --git a/app/static/js/cart.js b/app/static/js/cart.js index a2bc0fd..79479f9 100644 --- a/app/static/js/cart.js +++ b/app/static/js/cart.js @@ -75,11 +75,13 @@ function updateTotalPrice() { // 修改数量 function changeQuantity(cartId, delta) { - const input = document.querySelector(`[data-cart-id="${cartId}"]`); + const input = document.querySelector(`[data-cart-id="${cartId}"].quantity-input`); + if (!input) return; + const currentValue = parseInt(input.value); const newValue = currentValue + delta; - - if (newValue >= 1 && newValue <= parseInt(input.max)) { + + if (newValue >= 1) { updateQuantity(cartId, newValue); } } @@ -103,25 +105,39 @@ function updateQuantity(cartId, quantity) { if (data.success) { // 更新页面显示 const cartItem = document.querySelector(`[data-cart-id="${cartId}"]`); - cartItem.querySelector('.quantity-input').value = quantity; + const quantityInput = cartItem.querySelector('.quantity-input'); + quantityInput.value = quantity; 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(); // 更新全局购物车数量 updateCartBadge(data.cart_count); + // 显示成功消息 showSuccessMessage('数量更新成功'); } else { - alert(data.message); - // 恢复原始值 + alert(data.message || '更新失败'); location.reload(); } }) .catch(error => { console.error('Error:', error); - alert('更新失败'); + alert('网络错误,请重试'); location.reload(); }); } @@ -217,3 +233,82 @@ function checkout() { 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} + + `; + + 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'; + } +} diff --git a/app/static/js/checkout.js b/app/static/js/checkout.js index da4e686..192c4e4 100644 --- a/app/static/js/checkout.js +++ b/app/static/js/checkout.js @@ -15,65 +15,145 @@ document.addEventListener('DOMContentLoaded', function() { if (subtotalElement) { 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) { selectedAddressId = addressId; - // 更新UI - document.querySelectorAll('.address-card').forEach(card => { - card.classList.remove('selected'); - }); - - const selectedCard = document.querySelector(`[data-address-id="${addressId}"]`); - if (selectedCard) { - selectedCard.classList.add('selected'); + // 添加加载状态 + const clickedCard = document.querySelector(`[data-address-id="${addressId}"]`); + if (clickedCard) { + clickedCard.classList.add('loading'); } - // 更新单选按钮 - const radioButton = document.querySelector(`input[value="${addressId}"]`); - if (radioButton) { - radioButton.checked = true; - } + // 延迟执行UI更新,给用户视觉反馈 + setTimeout(() => { + // 更新UI + document.querySelectorAll('.address-card').forEach(card => { + 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 = ` +
请稍候,不要关闭页面
+
- service@taibai-mall.com
- 400-888-8888
+ 392437483@qq.com
+ 17753825492
{{ address.receiver_phone }}
-{{ address.get_full_address() }}
-+ + {{ address.receiver_phone }} +
+ + {{ address.get_full_address() }} +
+