feat: 添加购物车上下文和结账页面
引入购物车上下文(provideCart)以管理购物车状态,并添加结账页面路由。同时,优化AppLayout组件以支持隐藏底部导航栏,并在课程详情页中实现购买功能。这些改动为结账流程提供了基础支持。
Showing
9 changed files
with
63 additions
and
124 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
This diff is collapsed. Click to expand it.
| 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