hookehuyr

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

引入购物车上下文(provideCart)以管理购物车状态,并添加结账页面路由。同时,优化AppLayout组件以支持隐藏底部导航栏,并在课程详情页中实现购买功能。这些改动为结账流程提供了基础支持。
......@@ -8,9 +8,11 @@
<script setup>
import { RouterView } from "vue-router";
import { provideAuth } from '@/contexts/auth';
import { provideCart } from '@/contexts/cart';
// 提供认证上下文
// 提供认证和购物车上下文
provideAuth();
provideCart();
</script>
<template>
......
import React from 'react';
import BottomNav from './BottomNav';
import GradientHeader from '../ui/GradientHeader';
/**
* AppLayout component provides consistent layout across the app
*
* @param {Object} props - Component props
* @param {ReactNode} props.children - Child elements
* @param {string} props.title - Page title
* @param {boolean} props.showBackButton - Whether to display back button
* @param {Function} props.onBack - Back button click handler
* @param {ReactNode} props.rightContent - Content to display on the right side of header
* @returns {JSX.Element} AppLayout component
*/
const AppLayout = ({ children, title, showBackButton, onBack, rightContent }) => {
const handleBack = () => {
if (onBack) {
onBack();
} else {
window.history.back();
}
};
return (
<div className="bg-gradient-to-br from-green-50 via-teal-50 to-blue-50 min-h-screen pb-16">
<GradientHeader
title={title}
showBackButton={showBackButton}
onBack={handleBack}
rightContent={rightContent}
/>
<main className="pb-16">
{children}
</main>
<BottomNav />
</div>
);
};
export default AppLayout;
\ No newline at end of file
......@@ -9,7 +9,7 @@
<main class="pb-16">
<slot></slot>
</main>
<BottomNav />
<BottomNav v-if="!hideBottomNav" />
</div>
</template>
......@@ -34,6 +34,10 @@ const props = defineProps({
rightContent: {
type: Object,
default: null
},
hideBottomNav: {
type: Boolean,
default: false
}
})
......
import React from 'react';
import { Link, useLocation } from 'react-router-dom';
/**
* BottomNav component for app navigation
*
* @returns {JSX.Element} BottomNav component
*/
const BottomNav = () => {
const location = useLocation();
const navItems = [
{
name: '首页',
path: '/',
icon: (
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 mb-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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" />
</svg>
)
},
{
name: '课程',
path: '/courses',
icon: (
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 mb-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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" />
</svg>
)
},
{
name: '空间',
path: '/community',
icon: (
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 mb-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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" />
</svg>
)
},
{
name: '我的',
path: '/profile',
icon: (
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 mb-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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" />
</svg>
)
}
];
return (
<nav className="fixed bottom-0 left-0 right-0 bg-white/70 backdrop-blur-lg border-t border-gray-100 z-50">
<div className="flex justify-around items-center h-16">
{navItems.map((item) => {
const isActive = location.pathname === item.path ||
(item.path !== '/' && location.pathname.startsWith(item.path));
return (
<Link
key={item.name}
to={item.path}
className={`flex flex-col items-center justify-center w-1/4 h-full ${
isActive ? 'text-green-500' : 'text-gray-500'
}`}
>
{React.cloneElement(item.icon, {
className: `${item.icon.props.className} ${isActive ? 'text-green-500' : 'text-gray-500'}`
})}
<span className="text-xs">{item.name}</span>
</Link>
);
})}
</div>
</nav>
);
};
export default BottomNav;
\ No newline at end of file
......@@ -86,12 +86,36 @@ export function provideCart() {
// 处理结账流程
function handleCheckout(userData) {
console.warn('Processing order with data:', { items: cartItems.value, userData })
// 构建订单数据
const orderData = {
items: cartItems.value.map(item => ({
id: item.id,
type: item.type,
quantity: item.quantity,
price: item.price,
title: item.title
})),
totalAmount: getTotalPrice(),
userData: userData,
orderDate: new Date().toISOString()
}
// TODO: 替换为实际的API调用
return new Promise((resolve) => {
// 模拟API调用
setTimeout(() => {
// 在实际应用中,这里应该调用后端API
console.warn('提交订单数据:', orderData)
// 订单提交成功后清空购物车
clearCart()
resolve({ success: true, orderId: 'ORD-' + Date.now() })
// 返回订单ID
resolve({
success: true,
orderId: 'ORD-' + Date.now(),
orderData: orderData
})
}, 1500)
})
}
......
......@@ -57,6 +57,13 @@ const routes = [
props: true,
meta: { title: 'Home' }
},
{
path: '/checkout',
name: 'CheckoutPage',
component: () => import('../views/checkout/CheckoutPage.vue'),
props: true,
meta: { title: 'Home' }
},
]
const router = createRouter({
......
This diff is collapsed. Click to expand it.
<template>
<AppLayout :title="orderComplete ? '支付成功' : '结账'" :show-back-button="!orderComplete" @back-click="router.back()">
<AppLayout :title="orderComplete ? '支付成功' : '结账'" :show-back-button="!orderComplete" :hide-bottom-nav="true" @back-click="router.back()">
<div v-if="cartItems.length === 0 && !orderComplete" class="h-screen flex flex-col items-center justify-center px-4">
<FrostedGlass class="p-6 rounded-xl text-center">
<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) => {
const handleBackToHome = () => {
router.push('/')
}
</script>
......
......@@ -241,7 +241,10 @@
¥{{ Math.round(course?.price * 1.2) }}
</div>
</div>
<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">
<button
@click="handlePurchase"
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"
>
立即购买
</button>
</div>
......@@ -256,11 +259,13 @@ import { useRoute, useRouter } from 'vue-router'
import AppLayout from '@/components/layout/AppLayout.vue'
import FrostedGlass from '@/components/ui/FrostedGlass.vue'
import { courses } from '@/utils/mockData'
import { useCart } from '@/contexts/cart'
const route = useRoute()
const router = useRouter()
const course = ref(null)
const activeTab = ref('课程特色')
const { addToCart, proceedToCheckout } = useCart()
// Curriculum items
const curriculumItems = [
......@@ -301,6 +306,20 @@ const RightContent = defineComponent({
const rightContent = h(RightContent)
// Handle purchase
const handlePurchase = () => {
if (course.value) {
addToCart({
id: course.value.id,
type: 'course',
title: course.value.title,
price: course.value.price,
imageUrl: course.value.imageUrl
})
proceedToCheckout()
}
}
// Fetch course data
onMounted(() => {
const id = route.params.id
......