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({
......
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
......