feat: 添加购物车上下文和结账页面
引入购物车上下文(provideCart)以管理购物车状态,并添加结账页面路由。同时,优化AppLayout组件以支持隐藏底部导航栏,并在课程详情页中实现购买功能。这些改动为结账流程提供了基础支持。
Showing
9 changed files
with
63 additions
and
471 deletions
| ... | @@ -8,9 +8,11 @@ | ... | @@ -8,9 +8,11 @@ |
| 8 | <script setup> | 8 | <script setup> |
| 9 | import { RouterView } from "vue-router"; | 9 | import { RouterView } from "vue-router"; |
| 10 | import { provideAuth } from '@/contexts/auth'; | 10 | import { provideAuth } from '@/contexts/auth'; |
| 11 | +import { provideCart } from '@/contexts/cart'; | ||
| 11 | 12 | ||
| 12 | -// 提供认证上下文 | 13 | +// 提供认证和购物车上下文 |
| 13 | provideAuth(); | 14 | provideAuth(); |
| 15 | +provideCart(); | ||
| 14 | </script> | 16 | </script> |
| 15 | 17 | ||
| 16 | <template> | 18 | <template> | ... | ... |
src/components/layout/AppLayout.jsx
deleted
100644 → 0
| 1 | -import React from 'react'; | ||
| 2 | -import BottomNav from './BottomNav'; | ||
| 3 | -import GradientHeader from '../ui/GradientHeader'; | ||
| 4 | - | ||
| 5 | -/** | ||
| 6 | - * AppLayout component provides consistent layout across the app | ||
| 7 | - * | ||
| 8 | - * @param {Object} props - Component props | ||
| 9 | - * @param {ReactNode} props.children - Child elements | ||
| 10 | - * @param {string} props.title - Page title | ||
| 11 | - * @param {boolean} props.showBackButton - Whether to display back button | ||
| 12 | - * @param {Function} props.onBack - Back button click handler | ||
| 13 | - * @param {ReactNode} props.rightContent - Content to display on the right side of header | ||
| 14 | - * @returns {JSX.Element} AppLayout component | ||
| 15 | - */ | ||
| 16 | -const AppLayout = ({ children, title, showBackButton, onBack, rightContent }) => { | ||
| 17 | - const handleBack = () => { | ||
| 18 | - if (onBack) { | ||
| 19 | - onBack(); | ||
| 20 | - } else { | ||
| 21 | - window.history.back(); | ||
| 22 | - } | ||
| 23 | - }; | ||
| 24 | - | ||
| 25 | - return ( | ||
| 26 | - <div className="bg-gradient-to-br from-green-50 via-teal-50 to-blue-50 min-h-screen pb-16"> | ||
| 27 | - <GradientHeader | ||
| 28 | - title={title} | ||
| 29 | - showBackButton={showBackButton} | ||
| 30 | - onBack={handleBack} | ||
| 31 | - rightContent={rightContent} | ||
| 32 | - /> | ||
| 33 | - <main className="pb-16"> | ||
| 34 | - {children} | ||
| 35 | - </main> | ||
| 36 | - <BottomNav /> | ||
| 37 | - </div> | ||
| 38 | - ); | ||
| 39 | -}; | ||
| 40 | - | ||
| 41 | -export default AppLayout; | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
| ... | @@ -9,7 +9,7 @@ | ... | @@ -9,7 +9,7 @@ |
| 9 | <main class="pb-16"> | 9 | <main class="pb-16"> |
| 10 | <slot></slot> | 10 | <slot></slot> |
| 11 | </main> | 11 | </main> |
| 12 | - <BottomNav /> | 12 | + <BottomNav v-if="!hideBottomNav" /> |
| 13 | </div> | 13 | </div> |
| 14 | </template> | 14 | </template> |
| 15 | 15 | ||
| ... | @@ -34,6 +34,10 @@ const props = defineProps({ | ... | @@ -34,6 +34,10 @@ const props = defineProps({ |
| 34 | rightContent: { | 34 | rightContent: { |
| 35 | type: Object, | 35 | type: Object, |
| 36 | default: null | 36 | default: null |
| 37 | + }, | ||
| 38 | + hideBottomNav: { | ||
| 39 | + type: Boolean, | ||
| 40 | + default: false | ||
| 37 | } | 41 | } |
| 38 | }) | 42 | }) |
| 39 | 43 | ... | ... |
src/components/layout/BottomNav.jsx
deleted
100644 → 0
| 1 | -import React from 'react'; | ||
| 2 | -import { Link, useLocation } from 'react-router-dom'; | ||
| 3 | - | ||
| 4 | -/** | ||
| 5 | - * BottomNav component for app navigation | ||
| 6 | - * | ||
| 7 | - * @returns {JSX.Element} BottomNav component | ||
| 8 | - */ | ||
| 9 | -const BottomNav = () => { | ||
| 10 | - const location = useLocation(); | ||
| 11 | - | ||
| 12 | - const navItems = [ | ||
| 13 | - { | ||
| 14 | - name: '首页', | ||
| 15 | - path: '/', | ||
| 16 | - icon: ( | ||
| 17 | - <svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 mb-1" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | ||
| 18 | - <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" /> | ||
| 19 | - </svg> | ||
| 20 | - ) | ||
| 21 | - }, | ||
| 22 | - { | ||
| 23 | - name: '课程', | ||
| 24 | - path: '/courses', | ||
| 25 | - icon: ( | ||
| 26 | - <svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 mb-1" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | ||
| 27 | - <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" /> | ||
| 28 | - </svg> | ||
| 29 | - ) | ||
| 30 | - }, | ||
| 31 | - { | ||
| 32 | - name: '空间', | ||
| 33 | - path: '/community', | ||
| 34 | - icon: ( | ||
| 35 | - <svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 mb-1" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | ||
| 36 | - <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" /> | ||
| 37 | - </svg> | ||
| 38 | - ) | ||
| 39 | - }, | ||
| 40 | - { | ||
| 41 | - name: '我的', | ||
| 42 | - path: '/profile', | ||
| 43 | - icon: ( | ||
| 44 | - <svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 mb-1" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | ||
| 45 | - <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" /> | ||
| 46 | - </svg> | ||
| 47 | - ) | ||
| 48 | - } | ||
| 49 | - ]; | ||
| 50 | - | ||
| 51 | - return ( | ||
| 52 | - <nav className="fixed bottom-0 left-0 right-0 bg-white/70 backdrop-blur-lg border-t border-gray-100 z-50"> | ||
| 53 | - <div className="flex justify-around items-center h-16"> | ||
| 54 | - {navItems.map((item) => { | ||
| 55 | - const isActive = location.pathname === item.path || | ||
| 56 | - (item.path !== '/' && location.pathname.startsWith(item.path)); | ||
| 57 | - return ( | ||
| 58 | - <Link | ||
| 59 | - key={item.name} | ||
| 60 | - to={item.path} | ||
| 61 | - className={`flex flex-col items-center justify-center w-1/4 h-full ${ | ||
| 62 | - isActive ? 'text-green-500' : 'text-gray-500' | ||
| 63 | - }`} | ||
| 64 | - > | ||
| 65 | - {React.cloneElement(item.icon, { | ||
| 66 | - className: `${item.icon.props.className} ${isActive ? 'text-green-500' : 'text-gray-500'}` | ||
| 67 | - })} | ||
| 68 | - <span className="text-xs">{item.name}</span> | ||
| 69 | - </Link> | ||
| 70 | - ); | ||
| 71 | - })} | ||
| 72 | - </div> | ||
| 73 | - </nav> | ||
| 74 | - ); | ||
| 75 | -}; | ||
| 76 | - | ||
| 77 | -export default BottomNav; | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
| ... | @@ -86,12 +86,36 @@ export function provideCart() { | ... | @@ -86,12 +86,36 @@ export function provideCart() { |
| 86 | 86 | ||
| 87 | // 处理结账流程 | 87 | // 处理结账流程 |
| 88 | function handleCheckout(userData) { | 88 | function handleCheckout(userData) { |
| 89 | - console.warn('Processing order with data:', { items: cartItems.value, userData }) | 89 | + // 构建订单数据 |
| 90 | + const orderData = { | ||
| 91 | + items: cartItems.value.map(item => ({ | ||
| 92 | + id: item.id, | ||
| 93 | + type: item.type, | ||
| 94 | + quantity: item.quantity, | ||
| 95 | + price: item.price, | ||
| 96 | + title: item.title | ||
| 97 | + })), | ||
| 98 | + totalAmount: getTotalPrice(), | ||
| 99 | + userData: userData, | ||
| 100 | + orderDate: new Date().toISOString() | ||
| 101 | + } | ||
| 90 | 102 | ||
| 103 | + // TODO: 替换为实际的API调用 | ||
| 91 | return new Promise((resolve) => { | 104 | return new Promise((resolve) => { |
| 105 | + // 模拟API调用 | ||
| 92 | setTimeout(() => { | 106 | setTimeout(() => { |
| 107 | + // 在实际应用中,这里应该调用后端API | ||
| 108 | + console.warn('提交订单数据:', orderData) | ||
| 109 | + | ||
| 110 | + // 订单提交成功后清空购物车 | ||
| 93 | clearCart() | 111 | clearCart() |
| 94 | - resolve({ success: true, orderId: 'ORD-' + Date.now() }) | 112 | + |
| 113 | + // 返回订单ID | ||
| 114 | + resolve({ | ||
| 115 | + success: true, | ||
| 116 | + orderId: 'ORD-' + Date.now(), | ||
| 117 | + orderData: orderData | ||
| 118 | + }) | ||
| 95 | }, 1500) | 119 | }, 1500) |
| 96 | }) | 120 | }) |
| 97 | } | 121 | } | ... | ... |
| ... | @@ -57,6 +57,13 @@ const routes = [ | ... | @@ -57,6 +57,13 @@ const routes = [ |
| 57 | props: true, | 57 | props: true, |
| 58 | meta: { title: 'Home' } | 58 | meta: { title: 'Home' } |
| 59 | }, | 59 | }, |
| 60 | + { | ||
| 61 | + path: '/checkout', | ||
| 62 | + name: 'CheckoutPage', | ||
| 63 | + component: () => import('../views/checkout/CheckoutPage.vue'), | ||
| 64 | + props: true, | ||
| 65 | + meta: { title: 'Home' } | ||
| 66 | + }, | ||
| 60 | ] | 67 | ] |
| 61 | 68 | ||
| 62 | const router = createRouter({ | 69 | const router = createRouter({ | ... | ... |
src/views/checkout/CheckoutPage.jsx
deleted
100644 → 0
| 1 | -import React, { useState } from 'react'; | ||
| 2 | -import { useNavigate } from 'react-router-dom'; | ||
| 3 | -import AppLayout from '../../components/layout/AppLayout'; | ||
| 4 | -import FrostedGlass from '../../components/ui/FrostedGlass'; | ||
| 5 | -import { useCart } from '../../contexts/CartContext'; | ||
| 6 | - | ||
| 7 | -/** | ||
| 8 | - * CheckoutPage component handles the order checkout process | ||
| 9 | - * with user information collection and payment method selection | ||
| 10 | - * | ||
| 11 | - * @returns {JSX.Element} CheckoutPage component | ||
| 12 | - */ | ||
| 13 | -const CheckoutPage = () => { | ||
| 14 | - const navigate = useNavigate(); | ||
| 15 | - const { cartItems, getTotalPrice, handleCheckout, clearCart } = useCart(); | ||
| 16 | - | ||
| 17 | - // Form state | ||
| 18 | - const [formData, setFormData] = useState({ | ||
| 19 | - name: '', | ||
| 20 | - phone: '', | ||
| 21 | - email: '', | ||
| 22 | - address: '', | ||
| 23 | - notes: '', | ||
| 24 | - paymentMethod: 'wechat' | ||
| 25 | - }); | ||
| 26 | - | ||
| 27 | - // Loading and success states | ||
| 28 | - const [isProcessing, setIsProcessing] = useState(false); | ||
| 29 | - const [orderComplete, setOrderComplete] = useState(false); | ||
| 30 | - const [orderId, setOrderId] = useState(''); | ||
| 31 | - | ||
| 32 | - // Format price with Chinese Yuan symbol | ||
| 33 | - const formatPrice = (price) => { | ||
| 34 | - return `¥${price.toFixed(2)}`; | ||
| 35 | - }; | ||
| 36 | - | ||
| 37 | - // Handle form input changes | ||
| 38 | - const handleInputChange = (e) => { | ||
| 39 | - const { name, value } = e.target; | ||
| 40 | - setFormData(prev => ({ | ||
| 41 | - ...prev, | ||
| 42 | - [name]: value | ||
| 43 | - })); | ||
| 44 | - }; | ||
| 45 | - | ||
| 46 | - // Handle form submission | ||
| 47 | - const handleSubmit = async (e) => { | ||
| 48 | - e.preventDefault(); | ||
| 49 | - | ||
| 50 | - if (!formData.name || !formData.phone || !formData.address) { | ||
| 51 | - alert('请填写必要信息'); | ||
| 52 | - return; | ||
| 53 | - } | ||
| 54 | - | ||
| 55 | - setIsProcessing(true); | ||
| 56 | - | ||
| 57 | - try { | ||
| 58 | - // Process checkout | ||
| 59 | - const result = await handleCheckout(formData); | ||
| 60 | - | ||
| 61 | - if (result.success) { | ||
| 62 | - setOrderId(result.orderId); | ||
| 63 | - setOrderComplete(true); | ||
| 64 | - } | ||
| 65 | - } catch (error) { | ||
| 66 | - console.error('Checkout failed:', error); | ||
| 67 | - alert('支付失败,请重试'); | ||
| 68 | - } finally { | ||
| 69 | - setIsProcessing(false); | ||
| 70 | - } | ||
| 71 | - }; | ||
| 72 | - | ||
| 73 | - // Handle navigation back to home after order completion | ||
| 74 | - const handleBackToHome = () => { | ||
| 75 | - navigate('/'); | ||
| 76 | - }; | ||
| 77 | - | ||
| 78 | - // If cart is empty, redirect to home | ||
| 79 | - if (cartItems.length === 0 && !orderComplete) { | ||
| 80 | - return ( | ||
| 81 | - <AppLayout title="结账"> | ||
| 82 | - <div className="h-screen flex flex-col items-center justify-center px-4"> | ||
| 83 | - <FrostedGlass className="p-6 rounded-xl text-center"> | ||
| 84 | - <svg xmlns="http://www.w3.org/2000/svg" className="h-16 w-16 text-gray-400 mx-auto mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | ||
| 85 | - <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z" /> | ||
| 86 | - </svg> | ||
| 87 | - <h2 className="text-xl font-bold mb-2">购物车为空</h2> | ||
| 88 | - <p className="text-gray-600 mb-6">您的购物车中没有任何商品</p> | ||
| 89 | - <button | ||
| 90 | - onClick={() => navigate('/courses')} | ||
| 91 | - className="w-full bg-gradient-to-r from-green-500 to-green-600 text-white py-3 rounded-xl font-medium" | ||
| 92 | - > | ||
| 93 | - 浏览课程 | ||
| 94 | - </button> | ||
| 95 | - </FrostedGlass> | ||
| 96 | - </div> | ||
| 97 | - </AppLayout> | ||
| 98 | - ); | ||
| 99 | - } | ||
| 100 | - | ||
| 101 | - // Show order completion screen | ||
| 102 | - if (orderComplete) { | ||
| 103 | - return ( | ||
| 104 | - <AppLayout title="支付成功"> | ||
| 105 | - <div className="h-screen flex flex-col items-center justify-center px-4"> | ||
| 106 | - <FrostedGlass className="p-6 rounded-xl text-center"> | ||
| 107 | - <div className="w-20 h-20 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4"> | ||
| 108 | - <svg xmlns="http://www.w3.org/2000/svg" className="h-10 w-10 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | ||
| 109 | - <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /> | ||
| 110 | - </svg> | ||
| 111 | - </div> | ||
| 112 | - <h2 className="text-2xl font-bold mb-2">支付成功!</h2> | ||
| 113 | - <p className="text-gray-600 mb-2">您的订单已经成功提交</p> | ||
| 114 | - <p className="text-gray-500 text-sm mb-6">订单号: {orderId}</p> | ||
| 115 | - <button | ||
| 116 | - onClick={handleBackToHome} | ||
| 117 | - className="w-full bg-gradient-to-r from-green-500 to-green-600 text-white py-3 rounded-xl font-medium" | ||
| 118 | - > | ||
| 119 | - 返回首页 | ||
| 120 | - </button> | ||
| 121 | - </FrostedGlass> | ||
| 122 | - </div> | ||
| 123 | - </AppLayout> | ||
| 124 | - ); | ||
| 125 | - } | ||
| 126 | - | ||
| 127 | - return ( | ||
| 128 | - <AppLayout title="结账" showBackButton onBackClick={() => navigate(-1)}> | ||
| 129 | - <div className="pb-20"> | ||
| 130 | - {/* Order Summary */} | ||
| 131 | - <div className="p-4 bg-gradient-to-r from-green-500/10 to-blue-500/10"> | ||
| 132 | - <FrostedGlass className="rounded-xl p-4"> | ||
| 133 | - <h3 className="font-medium mb-3">订单摘要</h3> | ||
| 134 | - <div className="space-y-3"> | ||
| 135 | - {cartItems.map((item) => ( | ||
| 136 | - <div key={`${item.type}-${item.id}`} className="flex justify-between"> | ||
| 137 | - <div className="flex flex-1"> | ||
| 138 | - <div className="w-12 h-12 rounded-lg overflow-hidden mr-3 flex-shrink-0"> | ||
| 139 | - <img | ||
| 140 | - src={item.imageUrl || "/assets/images/course-placeholder.jpg"} | ||
| 141 | - alt={item.title} | ||
| 142 | - className="w-full h-full object-cover" | ||
| 143 | - onError={(e) => { | ||
| 144 | - e.target.onerror = null; | ||
| 145 | - e.target.src = "/assets/images/course-placeholder.jpg"; | ||
| 146 | - }} | ||
| 147 | - /> | ||
| 148 | - </div> | ||
| 149 | - <div> | ||
| 150 | - <p className="font-medium text-sm line-clamp-1">{item.title}</p> | ||
| 151 | - <p className="text-xs text-gray-500"> | ||
| 152 | - {item.type === 'course' ? '课程' : '活动'} · {item.quantity} 份 | ||
| 153 | - </p> | ||
| 154 | - </div> | ||
| 155 | - </div> | ||
| 156 | - <div className="ml-2 text-right"> | ||
| 157 | - <p className="font-medium text-sm">{formatPrice(item.price * item.quantity)}</p> | ||
| 158 | - <p className="text-xs text-gray-500">{formatPrice(item.price)} / 份</p> | ||
| 159 | - </div> | ||
| 160 | - </div> | ||
| 161 | - ))} | ||
| 162 | - </div> | ||
| 163 | - | ||
| 164 | - <div className="mt-4 pt-3 border-t border-gray-200"> | ||
| 165 | - <div className="flex justify-between items-center text-sm"> | ||
| 166 | - <span className="text-gray-600">小计</span> | ||
| 167 | - <span className="font-medium">{formatPrice(getTotalPrice())}</span> | ||
| 168 | - </div> | ||
| 169 | - <div className="flex justify-between items-center text-sm mt-1"> | ||
| 170 | - <span className="text-gray-600">优惠</span> | ||
| 171 | - <span className="text-red-500">- ¥0.00</span> | ||
| 172 | - </div> | ||
| 173 | - <div className="flex justify-between items-center mt-2 font-medium"> | ||
| 174 | - <span>总计</span> | ||
| 175 | - <span className="text-lg text-green-600">{formatPrice(getTotalPrice())}</span> | ||
| 176 | - </div> | ||
| 177 | - </div> | ||
| 178 | - </FrostedGlass> | ||
| 179 | - </div> | ||
| 180 | - | ||
| 181 | - {/* Checkout Form */} | ||
| 182 | - <div className="px-4 pt-4"> | ||
| 183 | - <form onSubmit={handleSubmit}> | ||
| 184 | - <FrostedGlass className="rounded-xl p-4 mb-4"> | ||
| 185 | - <h3 className="font-medium mb-3">个人信息</h3> | ||
| 186 | - | ||
| 187 | - <div className="space-y-3"> | ||
| 188 | - <div> | ||
| 189 | - <label className="block text-sm text-gray-600 mb-1">姓名 *</label> | ||
| 190 | - <input | ||
| 191 | - type="text" | ||
| 192 | - name="name" | ||
| 193 | - value={formData.name} | ||
| 194 | - onChange={handleInputChange} | ||
| 195 | - className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm" | ||
| 196 | - placeholder="请输入您的姓名" | ||
| 197 | - required | ||
| 198 | - /> | ||
| 199 | - </div> | ||
| 200 | - | ||
| 201 | - <div> | ||
| 202 | - <label className="block text-sm text-gray-600 mb-1">手机号码 *</label> | ||
| 203 | - <input | ||
| 204 | - type="tel" | ||
| 205 | - name="phone" | ||
| 206 | - value={formData.phone} | ||
| 207 | - onChange={handleInputChange} | ||
| 208 | - className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm" | ||
| 209 | - placeholder="请输入您的手机号码" | ||
| 210 | - required | ||
| 211 | - /> | ||
| 212 | - </div> | ||
| 213 | - | ||
| 214 | - <div> | ||
| 215 | - <label className="block text-sm text-gray-600 mb-1">电子邮箱</label> | ||
| 216 | - <input | ||
| 217 | - type="email" | ||
| 218 | - name="email" | ||
| 219 | - value={formData.email} | ||
| 220 | - onChange={handleInputChange} | ||
| 221 | - className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm" | ||
| 222 | - placeholder="请输入您的邮箱(选填)" | ||
| 223 | - /> | ||
| 224 | - </div> | ||
| 225 | - | ||
| 226 | - <div> | ||
| 227 | - <label className="block text-sm text-gray-600 mb-1">联系地址 *</label> | ||
| 228 | - <input | ||
| 229 | - type="text" | ||
| 230 | - name="address" | ||
| 231 | - value={formData.address} | ||
| 232 | - onChange={handleInputChange} | ||
| 233 | - className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm" | ||
| 234 | - placeholder="请输入您的详细地址" | ||
| 235 | - required | ||
| 236 | - /> | ||
| 237 | - </div> | ||
| 238 | - | ||
| 239 | - <div> | ||
| 240 | - <label className="block text-sm text-gray-600 mb-1">备注</label> | ||
| 241 | - <textarea | ||
| 242 | - name="notes" | ||
| 243 | - value={formData.notes} | ||
| 244 | - onChange={handleInputChange} | ||
| 245 | - className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm resize-none h-20" | ||
| 246 | - placeholder="有什么需要我们注意的事项?(选填)" | ||
| 247 | - /> | ||
| 248 | - </div> | ||
| 249 | - </div> | ||
| 250 | - </FrostedGlass> | ||
| 251 | - | ||
| 252 | - <FrostedGlass className="rounded-xl p-4 mb-6"> | ||
| 253 | - <h3 className="font-medium mb-3">支付方式</h3> | ||
| 254 | - <div className="space-y-2"> | ||
| 255 | - <label className="flex items-center p-3 border border-gray-200 rounded-lg bg-white/50"> | ||
| 256 | - <input | ||
| 257 | - type="radio" | ||
| 258 | - name="paymentMethod" | ||
| 259 | - value="wechat" | ||
| 260 | - checked={formData.paymentMethod === 'wechat'} | ||
| 261 | - onChange={handleInputChange} | ||
| 262 | - className="mr-3" | ||
| 263 | - /> | ||
| 264 | - <span className="flex-1">微信支付</span> | ||
| 265 | - <svg className="h-6 w-6 text-green-500" viewBox="0 0 24 24" fill="currentColor"> | ||
| 266 | - <path d="M9.5,8.5m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" /> | ||
| 267 | - <path d="M14.5,8.5m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" /> | ||
| 268 | - <path d="M9.5,14.5m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" /> | ||
| 269 | - <path d="M14.5,14.5m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" /> | ||
| 270 | - <path d="M12,2C6.5,2 2,6.5 2,12s4.5,10 10,10s10,-4.5 10,-10S17.5,2 12,2zM19.8,11c-0.2,4.6 -4.1,7.8 -8.8,7.5c-3.8,-0.2 -7.2,-3.2 -7.5,-7.1C3.2,7 6.9,3.1 11.5,3C16.1,3 20,6.9 19.8,11z" /> | ||
| 271 | - </svg> | ||
| 272 | - </label> | ||
| 273 | - | ||
| 274 | - <label className="flex items-center p-3 border border-gray-200 rounded-lg bg-white/50"> | ||
| 275 | - <input | ||
| 276 | - type="radio" | ||
| 277 | - name="paymentMethod" | ||
| 278 | - value="alipay" | ||
| 279 | - checked={formData.paymentMethod === 'alipay'} | ||
| 280 | - onChange={handleInputChange} | ||
| 281 | - className="mr-3" | ||
| 282 | - /> | ||
| 283 | - <span className="flex-1">支付宝</span> | ||
| 284 | - <svg className="h-6 w-6 text-blue-500" viewBox="0 0 24 24" fill="currentColor"> | ||
| 285 | - <path d="M12,2C6.5,2 2,6.5 2,12s4.5,10 10,10s10,-4.5 10,-10S17.5,2 12,2zM12,20.5c-4.7,0 -8.5,-3.8 -8.5,-8.5S7.3,3.5 12,3.5s8.5,3.8 8.5,8.5S16.7,20.5 12,20.5z" /> | ||
| 286 | - <path d="M12,6c-3.3,0 -6,2.7 -6,6s2.7,6 6,6s6,-2.7 6,-6S15.3,6 12,6zM16,13h-3v3h-2v-3H8v-2h3V8h2v3h3V13z" /> | ||
| 287 | - </svg> | ||
| 288 | - </label> | ||
| 289 | - | ||
| 290 | - <label className="flex items-center p-3 border border-gray-200 rounded-lg bg-white/50"> | ||
| 291 | - <input | ||
| 292 | - type="radio" | ||
| 293 | - name="paymentMethod" | ||
| 294 | - value="bank" | ||
| 295 | - checked={formData.paymentMethod === 'bank'} | ||
| 296 | - onChange={handleInputChange} | ||
| 297 | - className="mr-3" | ||
| 298 | - /> | ||
| 299 | - <span className="flex-1">银行卡</span> | ||
| 300 | - <svg className="h-6 w-6 text-gray-500" viewBox="0 0 24 24" fill="currentColor"> | ||
| 301 | - <path d="M20,4H4C2.9,4 2,4.9 2,6v12c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2V6C22,4.9 21.1,4 20,4zM20,18H4V12h16V18zM20,8H4V6h16V8z" /> | ||
| 302 | - <path d="M5,13h2v2h-2z" /> | ||
| 303 | - <path d="M9,13h2v2h-2z" /> | ||
| 304 | - <path d="M13,13h2v2h-2z" /> | ||
| 305 | - <path d="M17,13h2v2h-2z" /> | ||
| 306 | - </svg> | ||
| 307 | - </label> | ||
| 308 | - </div> | ||
| 309 | - </FrostedGlass> | ||
| 310 | - | ||
| 311 | - <div className="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-100 shadow-md px-4 py-3"> | ||
| 312 | - <div className="flex justify-between items-center mb-3"> | ||
| 313 | - <div> | ||
| 314 | - <span className="text-sm text-gray-500">总计:</span> | ||
| 315 | - <span className="text-lg font-bold text-green-600">{formatPrice(getTotalPrice())}</span> | ||
| 316 | - </div> | ||
| 317 | - <button | ||
| 318 | - type="button" | ||
| 319 | - onClick={() => clearCart()} | ||
| 320 | - className="text-sm text-red-500" | ||
| 321 | - > | ||
| 322 | - 清空购物车 | ||
| 323 | - </button> | ||
| 324 | - </div> | ||
| 325 | - <button | ||
| 326 | - type="submit" | ||
| 327 | - disabled={isProcessing} | ||
| 328 | - className="w-full bg-gradient-to-r from-green-500 to-green-600 text-white py-3 rounded-xl font-medium disabled:opacity-70 flex items-center justify-center" | ||
| 329 | - > | ||
| 330 | - {isProcessing ? ( | ||
| 331 | - <> | ||
| 332 | - <div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin mr-2"></div> | ||
| 333 | - 处理中... | ||
| 334 | - </> | ||
| 335 | - ) : ( | ||
| 336 | - '确认支付' | ||
| 337 | - )} | ||
| 338 | - </button> | ||
| 339 | - </div> | ||
| 340 | - </form> | ||
| 341 | - </div> | ||
| 342 | - </div> | ||
| 343 | - </AppLayout> | ||
| 344 | - ); | ||
| 345 | -}; | ||
| 346 | - | ||
| 347 | -export default CheckoutPage; | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
| 1 | <template> | 1 | <template> |
| 2 | - <AppLayout :title="orderComplete ? '支付成功' : '结账'" :show-back-button="!orderComplete" @back-click="router.back()"> | 2 | + <AppLayout :title="orderComplete ? '支付成功' : '结账'" :show-back-button="!orderComplete" :hide-bottom-nav="true" @back-click="router.back()"> |
| 3 | <div v-if="cartItems.length === 0 && !orderComplete" class="h-screen flex flex-col items-center justify-center px-4"> | 3 | <div v-if="cartItems.length === 0 && !orderComplete" class="h-screen flex flex-col items-center justify-center px-4"> |
| 4 | <FrostedGlass class="p-6 rounded-xl text-center"> | 4 | <FrostedGlass class="p-6 rounded-xl text-center"> |
| 5 | <svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 text-gray-400 mx-auto mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | 5 | <svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 text-gray-400 mx-auto mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> |
| ... | @@ -294,3 +294,4 @@ const handleSubmit = async (e) => { | ... | @@ -294,3 +294,4 @@ const handleSubmit = async (e) => { |
| 294 | const handleBackToHome = () => { | 294 | const handleBackToHome = () => { |
| 295 | router.push('/') | 295 | router.push('/') |
| 296 | } | 296 | } |
| 297 | +</script> | ... | ... |
| ... | @@ -241,7 +241,10 @@ | ... | @@ -241,7 +241,10 @@ |
| 241 | ¥{{ Math.round(course?.price * 1.2) }} | 241 | ¥{{ Math.round(course?.price * 1.2) }} |
| 242 | </div> | 242 | </div> |
| 243 | </div> | 243 | </div> |
| 244 | - <button class="bg-gradient-to-r from-green-500 to-green-600 text-white px-6 py-2 rounded-full text-sm font-medium shadow-md"> | 244 | + <button |
| 245 | + @click="handlePurchase" | ||
| 246 | + class="bg-gradient-to-r from-green-500 to-green-600 text-white px-6 py-2 rounded-full text-sm font-medium shadow-md" | ||
| 247 | + > | ||
| 245 | 立即购买 | 248 | 立即购买 |
| 246 | </button> | 249 | </button> |
| 247 | </div> | 250 | </div> |
| ... | @@ -256,11 +259,13 @@ import { useRoute, useRouter } from 'vue-router' | ... | @@ -256,11 +259,13 @@ import { useRoute, useRouter } from 'vue-router' |
| 256 | import AppLayout from '@/components/layout/AppLayout.vue' | 259 | import AppLayout from '@/components/layout/AppLayout.vue' |
| 257 | import FrostedGlass from '@/components/ui/FrostedGlass.vue' | 260 | import FrostedGlass from '@/components/ui/FrostedGlass.vue' |
| 258 | import { courses } from '@/utils/mockData' | 261 | import { courses } from '@/utils/mockData' |
| 262 | +import { useCart } from '@/contexts/cart' | ||
| 259 | 263 | ||
| 260 | const route = useRoute() | 264 | const route = useRoute() |
| 261 | const router = useRouter() | 265 | const router = useRouter() |
| 262 | const course = ref(null) | 266 | const course = ref(null) |
| 263 | const activeTab = ref('课程特色') | 267 | const activeTab = ref('课程特色') |
| 268 | +const { addToCart, proceedToCheckout } = useCart() | ||
| 264 | 269 | ||
| 265 | // Curriculum items | 270 | // Curriculum items |
| 266 | const curriculumItems = [ | 271 | const curriculumItems = [ |
| ... | @@ -301,6 +306,20 @@ const RightContent = defineComponent({ | ... | @@ -301,6 +306,20 @@ const RightContent = defineComponent({ |
| 301 | 306 | ||
| 302 | const rightContent = h(RightContent) | 307 | const rightContent = h(RightContent) |
| 303 | 308 | ||
| 309 | +// Handle purchase | ||
| 310 | +const handlePurchase = () => { | ||
| 311 | + if (course.value) { | ||
| 312 | + addToCart({ | ||
| 313 | + id: course.value.id, | ||
| 314 | + type: 'course', | ||
| 315 | + title: course.value.title, | ||
| 316 | + price: course.value.price, | ||
| 317 | + imageUrl: course.value.imageUrl | ||
| 318 | + }) | ||
| 319 | + proceedToCheckout() | ||
| 320 | + } | ||
| 321 | +} | ||
| 322 | + | ||
| 304 | // Fetch course data | 323 | // Fetch course data |
| 305 | onMounted(() => { | 324 | onMounted(() => { |
| 306 | const id = route.params.id | 325 | const id = route.params.id | ... | ... |
-
Please register or login to post a comment