hookehuyr

feat: 添加购物车上下文和结账页面

引入购物车上下文(provideCart)以管理购物车状态,并添加结账页面路由。同时,优化AppLayout组件以支持隐藏底部导航栏,并在课程详情页中实现购买功能。这些改动为结账流程提供了基础支持。
...@@ -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>
......
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
......
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({
......
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
......