hookehuyr

refactor(cart): 将购物车逻辑从React迁移到Vue

迁移购物车上下文逻辑,从React的CartContext.jsx迁移到Vue的cart.js,以支持Vue项目的需求。新的实现使用Vue的ref和provide/inject API,并保留了原有的功能,如购物车管理、本地存储同步和结账流程。
1 -import React, { createContext, useContext, useState, useEffect } from 'react';
2 -import { useNavigate } from 'react-router-dom';
3 -
4 -// Create a context for the cart
5 -const CartContext = createContext();
6 -
7 -// Custom hook to use the cart context
8 -export const useCart = () => {
9 - const context = useContext(CartContext);
10 - if (!context) {
11 - throw new Error('useCart must be used within a CartProvider');
12 - }
13 - return context;
14 -};
15 -
16 -// Cart provider component
17 -export const CartProvider = ({ children }) => {
18 - const [cartItems, setCartItems] = useState([]);
19 - const navigate = useNavigate();
20 -
21 - // Load cart from localStorage on component mount
22 - useEffect(() => {
23 - const storedCart = localStorage.getItem('cart');
24 - if (storedCart) {
25 - try {
26 - setCartItems(JSON.parse(storedCart));
27 - } catch (error) {
28 - console.error('Failed to parse cart from localStorage:', error);
29 - // Reset cart if there's an error
30 - localStorage.removeItem('cart');
31 - }
32 - }
33 - }, []);
34 -
35 - // Save cart to localStorage whenever it changes
36 - useEffect(() => {
37 - localStorage.setItem('cart', JSON.stringify(cartItems));
38 - }, [cartItems]);
39 -
40 - // Add an item to the cart
41 - const addToCart = (item) => {
42 - setCartItems(prevItems => {
43 - // Check if item already exists in cart
44 - const existingItemIndex = prevItems.findIndex(i =>
45 - i.id === item.id && i.type === item.type
46 - );
47 -
48 - if (existingItemIndex >= 0) {
49 - // Item exists, update the quantity
50 - const updatedItems = [...prevItems];
51 - updatedItems[existingItemIndex] = {
52 - ...updatedItems[existingItemIndex],
53 - quantity: updatedItems[existingItemIndex].quantity + 1
54 - };
55 - return updatedItems;
56 - } else {
57 - // Item doesn't exist, add it with quantity 1
58 - return [...prevItems, { ...item, quantity: 1 }];
59 - }
60 - });
61 - };
62 -
63 - // Remove an item from the cart
64 - const removeFromCart = (itemId, itemType) => {
65 - setCartItems(prevItems =>
66 - prevItems.filter(item => !(item.id === itemId && item.type === itemType))
67 - );
68 - };
69 -
70 - // Update quantity of an item in the cart
71 - const updateQuantity = (itemId, itemType, quantity) => {
72 - if (quantity < 1) return;
73 -
74 - setCartItems(prevItems =>
75 - prevItems.map(item =>
76 - (item.id === itemId && item.type === itemType)
77 - ? { ...item, quantity }
78 - : item
79 - )
80 - );
81 - };
82 -
83 - // Clear the entire cart
84 - const clearCart = () => {
85 - setCartItems([]);
86 - };
87 -
88 - // Get the number of items in cart
89 - const getItemCount = () => {
90 - return cartItems.reduce((total, item) => total + item.quantity, 0);
91 - };
92 -
93 - // Calculate the total price of items in the cart
94 - const getTotalPrice = () => {
95 - return cartItems.reduce(
96 - (total, item) => total + (item.price * item.quantity),
97 - 0
98 - );
99 - };
100 -
101 - // Proceed to checkout
102 - const proceedToCheckout = () => {
103 - if (cartItems.length > 0) {
104 - navigate('/checkout');
105 - }
106 - };
107 -
108 - // Handle the checkout process
109 - const handleCheckout = (userData) => {
110 - // In a real application, this would send the order to a backend
111 - console.log('Processing order with data:', { items: cartItems, userData });
112 -
113 - // Simulating successful checkout
114 - return new Promise((resolve) => {
115 - setTimeout(() => {
116 - clearCart();
117 - resolve({ success: true, orderId: 'ORD-' + Date.now() });
118 - }, 1500);
119 - });
120 - };
121 -
122 - // Values to provide in the context
123 - const value = {
124 - cartItems,
125 - addToCart,
126 - removeFromCart,
127 - updateQuantity,
128 - clearCart,
129 - getItemCount,
130 - getTotalPrice,
131 - proceedToCheckout,
132 - handleCheckout
133 - };
134 -
135 - return (
136 - <CartContext.Provider value={value}>
137 - {children}
138 - </CartContext.Provider>
139 - );
140 -};
...\ No newline at end of file ...\ No newline at end of file
1 +import { ref, provide, inject, watchEffect } from 'vue'
2 +import { useRouter } from 'vue-router'
3 +
4 +const CartSymbol = Symbol()
5 +
6 +export function provideCart() {
7 + const router = useRouter()
8 + const cartItems = ref([])
9 +
10 + // 从localStorage加载购物车数据
11 + try {
12 + const storedCart = localStorage.getItem('cart')
13 + if (storedCart) {
14 + cartItems.value = JSON.parse(storedCart)
15 + }
16 + } catch (error) {
17 + console.error('Failed to parse cart from localStorage:', error)
18 + localStorage.removeItem('cart')
19 + }
20 +
21 + // 监听购物车变化并保存到localStorage
22 + watchEffect(() => {
23 + localStorage.setItem('cart', JSON.stringify(cartItems.value))
24 + })
25 +
26 + // 添加商品到购物车
27 + function addToCart(item) {
28 + const existingItemIndex = cartItems.value.findIndex(
29 + i => i.id === item.id && i.type === item.type
30 + )
31 +
32 + if (existingItemIndex >= 0) {
33 + const updatedItems = [...cartItems.value]
34 + updatedItems[existingItemIndex] = {
35 + ...updatedItems[existingItemIndex],
36 + quantity: updatedItems[existingItemIndex].quantity + 1
37 + }
38 + cartItems.value = updatedItems
39 + } else {
40 + cartItems.value = [...cartItems.value, { ...item, quantity: 1 }]
41 + }
42 + }
43 +
44 + // 从购物车移除商品
45 + function removeFromCart(itemId, itemType) {
46 + cartItems.value = cartItems.value.filter(
47 + item => !(item.id === itemId && item.type === itemType)
48 + )
49 + }
50 +
51 + // 更新商品数量
52 + function updateQuantity(itemId, itemType, quantity) {
53 + if (quantity < 1) return
54 +
55 + cartItems.value = cartItems.value.map(item =>
56 + item.id === itemId && item.type === itemType
57 + ? { ...item, quantity }
58 + : item
59 + )
60 + }
61 +
62 + // 清空购物车
63 + function clearCart() {
64 + cartItems.value = []
65 + }
66 +
67 + // 获取购物车商品总数
68 + function getItemCount() {
69 + return cartItems.value.reduce((total, item) => total + item.quantity, 0)
70 + }
71 +
72 + // 计算购物车总价
73 + function getTotalPrice() {
74 + return cartItems.value.reduce(
75 + (total, item) => total + item.price * item.quantity,
76 + 0
77 + )
78 + }
79 +
80 + // 跳转到结账页面
81 + function proceedToCheckout() {
82 + if (cartItems.value.length > 0) {
83 + router.push('/checkout')
84 + }
85 + }
86 +
87 + // 处理结账流程
88 + function handleCheckout(userData) {
89 + console.warn('Processing order with data:', { items: cartItems.value, userData })
90 +
91 + return new Promise((resolve) => {
92 + setTimeout(() => {
93 + clearCart()
94 + resolve({ success: true, orderId: 'ORD-' + Date.now() })
95 + }, 1500)
96 + })
97 + }
98 +
99 + const cart = {
100 + items: cartItems,
101 + addToCart,
102 + removeFromCart,
103 + updateQuantity,
104 + clearCart,
105 + getItemCount,
106 + getTotalPrice,
107 + proceedToCheckout,
108 + handleCheckout
109 + }
110 +
111 + provide(CartSymbol, cart)
112 +
113 + return cart
114 +}
115 +
116 +export function useCart() {
117 + const cart = inject(CartSymbol)
118 + if (!cart) {
119 + throw new Error('useCart() must be used within a component that has called provideCart()')
120 + }
121 + return cart
122 +}
1 <script setup> 1 <script setup>
2 -import { ref, onMounted, defineComponent, h } from 'vue' 2 +import { ref, onMounted, defineComponent, h } from "vue";
3 -import { useRoute, useRouter } from 'vue-router' 3 +import { useRoute, useRouter } from "vue-router";
4 -import AppLayout from '@/components/layout/AppLayout.vue' 4 +import AppLayout from "@/components/layout/AppLayout.vue";
5 -import FrostedGlass from '@/components/ui/FrostedGlass.vue' 5 +import FrostedGlass from "@/components/ui/FrostedGlass.vue";
6 -import { activities } from '@/utils/mockData' 6 +import { activities } from "@/utils/mockData";
7 7
8 -const route = useRoute() 8 +const route = useRoute();
9 -const router = useRouter() 9 +const router = useRouter();
10 -const activity = ref(null) 10 +const activity = ref(null);
11 -const activeTab = ref('活动信息') 11 +const activeTab = ref("活动信息");
12 12
13 // 获取活动数据 13 // 获取活动数据
14 onMounted(() => { 14 onMounted(() => {
15 - const id = route.params.id 15 + const id = route.params.id;
16 - const foundActivity = activities.find(a => a.id === id) 16 + const foundActivity = activities.find((a) => a.id === id);
17 if (foundActivity) { 17 if (foundActivity) {
18 - activity.value = foundActivity 18 + activity.value = foundActivity;
19 } else { 19 } else {
20 // 活动未找到,重定向到活动列表页 20 // 活动未找到,重定向到活动列表页
21 - router.push('/activities') 21 + router.push("/activities");
22 } 22 }
23 -}) 23 +});
24 24
25 // 获取状态颜色类名 25 // 获取状态颜色类名
26 const getStatusColorClass = (status) => { 26 const getStatusColorClass = (status) => {
27 switch (status) { 27 switch (status) {
28 - case '活动中': 28 + case "活动中":
29 - return 'bg-blue-100 text-blue-700' 29 + return "bg-blue-100 text-blue-700";
30 - case '进行中': 30 + case "进行中":
31 - return 'bg-green-100 text-green-700' 31 + return "bg-green-100 text-green-700";
32 - case '即将开始': 32 + case "即将开始":
33 - return 'bg-orange-100 text-orange-700' 33 + return "bg-orange-100 text-orange-700";
34 - case '已结束': 34 + case "已结束":
35 - return 'bg-gray-100 text-gray-700' 35 + return "bg-gray-100 text-gray-700";
36 default: 36 default:
37 - return 'bg-gray-100 text-gray-700' 37 + return "bg-gray-100 text-gray-700";
38 } 38 }
39 -} 39 +};
40 40
41 // 相关活动 41 // 相关活动
42 -const relatedActivities = ref([]) 42 +const relatedActivities = ref([]);
43 onMounted(() => { 43 onMounted(() => {
44 if (activity.value) { 44 if (activity.value) {
45 relatedActivities.value = activities 45 relatedActivities.value = activities
46 - .filter(a => a.id !== activity.value.id) 46 + .filter((a) => a.id !== activity.value.id)
47 - .slice(0, 3) 47 + .slice(0, 3);
48 } 48 }
49 -}) 49 +});
50 50
51 // 页面导航 51 // 页面导航
52 const navigateTo = (path) => { 52 const navigateTo = (path) => {
53 - router.push(path) 53 + router.push(path);
54 -} 54 +};
55 55
56 // 右侧内容组件 56 // 右侧内容组件
57 const RightContent = defineComponent({ 57 const RightContent = defineComponent({
58 setup() { 58 setup() {
59 - return () => h('div', { class: 'flex' }, [ 59 + return () =>
60 - h('button', { class: 'p-2' }, [ 60 + h("div", { class: "flex" }, [
61 - h('svg', { 61 + h("button", { class: "p-2" }, [
62 - xmlns: 'http://www.w3.org/2000/svg', 62 + h(
63 - class: 'h-6 w-6 text-gray-700', 63 + "svg",
64 - fill: 'none', 64 + {
65 - viewBox: '0 0 24 24', 65 + xmlns: "http://www.w3.org/2000/svg",
66 - stroke: 'currentColor' 66 + class: "h-6 w-6 text-gray-700",
67 - }, [ 67 + fill: "none",
68 - h('path', { 68 + viewBox: "0 0 24 24",
69 - 'stroke-linecap': 'round', 69 + stroke: "currentColor",
70 - 'stroke-linejoin': 'round', 70 + },
71 - 'stroke-width': '2', 71 + [
72 - d: 'M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z' 72 + h("path", {
73 - }) 73 + "stroke-linecap": "round",
74 - ]) 74 + "stroke-linejoin": "round",
75 - ]), 75 + "stroke-width": "2",
76 - h('button', { class: 'p-2' }, [ 76 + d:
77 - h('svg', { 77 + "M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z",
78 - xmlns: 'http://www.w3.org/2000/svg', 78 + }),
79 - class: 'h-6 w-6 text-gray-700', 79 + ]
80 - fill: 'none', 80 + ),
81 - viewBox: '0 0 24 24', 81 + ]),
82 - stroke: 'currentColor' 82 + h("button", { class: "p-2" }, [
83 - }, [ 83 + h(
84 - h('path', { 84 + "svg",
85 - 'stroke-linecap': 'round', 85 + {
86 - 'stroke-linejoin': 'round', 86 + xmlns: "http://www.w3.org/2000/svg",
87 - 'stroke-width': '2', 87 + class: "h-6 w-6 text-gray-700",
88 - d: 'M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z' 88 + fill: "none",
89 - }) 89 + viewBox: "0 0 24 24",
90 - ]) 90 + stroke: "currentColor",
91 - ]) 91 + },
92 - ]) 92 + [
93 - } 93 + h("path", {
94 -}) 94 + "stroke-linecap": "round",
95 + "stroke-linejoin": "round",
96 + "stroke-width": "2",
97 + d:
98 + "M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z",
99 + }),
100 + ]
101 + ),
102 + ]),
103 + ]);
104 + },
105 +});
95 </script> 106 </script>
96 107
97 <template> 108 <template>
...@@ -104,8 +115,12 @@ const RightContent = defineComponent({ ...@@ -104,8 +115,12 @@ const RightContent = defineComponent({
104 :alt="activity?.title" 115 :alt="activity?.title"
105 class="w-full h-full object-cover" 116 class="w-full h-full object-cover"
106 /> 117 />
107 - <div class="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/70 to-transparent p-4"> 118 + <div
108 - <div class="inline-block px-2 py-1 rounded bg-white/20 backdrop-blur-sm text-white text-xs mb-2"> 119 + class="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/70 to-transparent p-4"
120 + >
121 + <div
122 + class="inline-block px-2 py-1 rounded bg-white/20 backdrop-blur-sm text-white text-xs mb-2"
123 + >
109 {{ activity?.subtitle }} 124 {{ activity?.subtitle }}
110 </div> 125 </div>
111 <h1 class="text-xl text-white font-bold">{{ activity?.title }}</h1> 126 <h1 class="text-xl text-white font-bold">{{ activity?.title }}</h1>
...@@ -121,14 +136,30 @@ const RightContent = defineComponent({ ...@@ -121,14 +136,30 @@ const RightContent = defineComponent({
121 <span class="px-2 py-1 rounded-full bg-blue-100 text-blue-700 text-xs"> 136 <span class="px-2 py-1 rounded-full bg-blue-100 text-blue-700 text-xs">
122 教育 137 教育
123 </span> 138 </span>
124 - <span :class="['px-2 py-1 rounded-full text-xs', getStatusColorClass(activity?.status)]"> 139 + <span
140 + :class="[
141 + 'px-2 py-1 rounded-full text-xs',
142 + getStatusColorClass(activity?.status),
143 + ]"
144 + >
125 {{ activity?.status }} 145 {{ activity?.status }}
126 </span> 146 </span>
127 </div> 147 </div>
128 <div> 148 <div>
129 <button class="p-1"> 149 <button class="p-1">
130 - <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-pink-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"> 150 + <svg
131 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" /> 151 + xmlns="http://www.w3.org/2000/svg"
152 + class="h-6 w-6 text-pink-500"
153 + fill="none"
154 + viewBox="0 0 24 24"
155 + stroke="currentColor"
156 + >
157 + <path
158 + stroke-linecap="round"
159 + stroke-linejoin="round"
160 + stroke-width="2"
161 + d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
162 + />
132 </svg> 163 </svg>
133 </button> 164 </button>
134 </div> 165 </div>
...@@ -145,7 +176,7 @@ const RightContent = defineComponent({ ...@@ -145,7 +176,7 @@ const RightContent = defineComponent({
145 'pb-3 font-medium', 176 'pb-3 font-medium',
146 activeTab === tab 177 activeTab === tab
147 ? 'text-green-600 border-b-2 border-green-600' 178 ? 'text-green-600 border-b-2 border-green-600'
148 - : 'text-gray-500' 179 + : 'text-gray-500',
149 ]" 180 ]"
150 > 181 >
151 {{ tab }} 182 {{ tab }}
...@@ -161,9 +192,25 @@ const RightContent = defineComponent({ ...@@ -161,9 +192,25 @@ const RightContent = defineComponent({
161 <div class="space-y-4"> 192 <div class="space-y-4">
162 <div class="flex items-start"> 193 <div class="flex items-start">
163 <div class="p-2 rounded-full bg-green-100 mr-3"> 194 <div class="p-2 rounded-full bg-green-100 mr-3">
164 - <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor"> 195 + <svg
165 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" /> 196 + xmlns="http://www.w3.org/2000/svg"
166 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" /> 197 + class="h-5 w-5 text-green-600"
198 + fill="none"
199 + viewBox="0 0 24 24"
200 + stroke="currentColor"
201 + >
202 + <path
203 + stroke-linecap="round"
204 + stroke-linejoin="round"
205 + stroke-width="2"
206 + d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"
207 + />
208 + <path
209 + stroke-linecap="round"
210 + stroke-linejoin="round"
211 + stroke-width="2"
212 + d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"
213 + />
167 </svg> 214 </svg>
168 </div> 215 </div>
169 <div class="flex-1"> 216 <div class="flex-1">
...@@ -174,8 +221,19 @@ const RightContent = defineComponent({ ...@@ -174,8 +221,19 @@ const RightContent = defineComponent({
174 221
175 <div class="flex items-start"> 222 <div class="flex items-start">
176 <div class="p-2 rounded-full bg-blue-100 mr-3"> 223 <div class="p-2 rounded-full bg-blue-100 mr-3">
177 - <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor"> 224 + <svg
178 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" /> 225 + xmlns="http://www.w3.org/2000/svg"
226 + class="h-5 w-5 text-blue-600"
227 + fill="none"
228 + viewBox="0 0 24 24"
229 + stroke="currentColor"
230 + >
231 + <path
232 + stroke-linecap="round"
233 + stroke-linejoin="round"
234 + stroke-width="2"
235 + d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
236 + />
179 </svg> 237 </svg>
180 </div> 238 </div>
181 <div class="flex-1"> 239 <div class="flex-1">
...@@ -187,28 +245,64 @@ const RightContent = defineComponent({ ...@@ -187,28 +245,64 @@ const RightContent = defineComponent({
187 245
188 <div class="flex items-start"> 246 <div class="flex items-start">
189 <div class="p-2 rounded-full bg-purple-100 mr-3"> 247 <div class="p-2 rounded-full bg-purple-100 mr-3">
190 - <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-purple-600" fill="none" viewBox="0 0 24 24" stroke="currentColor"> 248 + <svg
191 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" /> 249 + xmlns="http://www.w3.org/2000/svg"
250 + class="h-5 w-5 text-purple-600"
251 + fill="none"
252 + viewBox="0 0 24 24"
253 + stroke="currentColor"
254 + >
255 + <path
256 + stroke-linecap="round"
257 + stroke-linejoin="round"
258 + stroke-width="2"
259 + d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"
260 + />
192 </svg> 261 </svg>
193 </div> 262 </div>
194 <div class="flex-1"> 263 <div class="flex-1">
195 <p class="font-medium">参与人数</p> 264 <p class="font-medium">参与人数</p>
196 - <p class="text-gray-600 text-sm mt-1">{{ activity?.participantsCount }}人 / 上限{{ activity?.maxParticipants }}人</p> 265 + <p class="text-gray-600 text-sm mt-1">
266 + {{ activity?.participantsCount }}人 / 上限{{
267 + activity?.maxParticipants
268 + }}人
269 + </p>
197 <div class="w-full bg-gray-200 rounded-full h-1.5 mt-2"> 270 <div class="w-full bg-gray-200 rounded-full h-1.5 mt-2">
198 - <div class="bg-green-600 h-1.5 rounded-full" :style="{ width: (activity?.participantsCount / activity?.maxParticipants * 100) + '%' }"></div> 271 + <div
272 + class="bg-green-600 h-1.5 rounded-full"
273 + :style="{
274 + width:
275 + (activity?.participantsCount / activity?.maxParticipants) *
276 + 100 +
277 + '%',
278 + }"
279 + ></div>
199 </div> 280 </div>
200 </div> 281 </div>
201 </div> 282 </div>
202 283
203 <div class="flex items-start"> 284 <div class="flex items-start">
204 <div class="p-2 rounded-full bg-orange-100 mr-3"> 285 <div class="p-2 rounded-full bg-orange-100 mr-3">
205 - <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-orange-600" fill="none" viewBox="0 0 24 24" stroke="currentColor"> 286 + <svg
206 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> 287 + xmlns="http://www.w3.org/2000/svg"
288 + class="h-5 w-5 text-orange-600"
289 + fill="none"
290 + viewBox="0 0 24 24"
291 + stroke="currentColor"
292 + >
293 + <path
294 + stroke-linecap="round"
295 + stroke-linejoin="round"
296 + stroke-width="2"
297 + d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
298 + />
207 </svg> 299 </svg>
208 </div> 300 </div>
209 <div class="flex-1"> 301 <div class="flex-1">
210 <p class="font-medium">活动费用</p> 302 <p class="font-medium">活动费用</p>
211 - <p class="text-gray-600 text-sm mt-1">¥{{ activity?.price }}/人(包含材料费、场地费)</p> 303 + <p class="text-gray-600 text-sm mt-1">
304 + ¥{{ activity?.price }}/人(包含材料费、场地费)
305 + </p>
212 </div> 306 </div>
213 </div> 307 </div>
214 </div> 308 </div>
...@@ -218,15 +312,13 @@ const RightContent = defineComponent({ ...@@ -218,15 +312,13 @@ const RightContent = defineComponent({
218 <FrostedGlass class="p-4 mb-4 rounded-xl"> 312 <FrostedGlass class="p-4 mb-4 rounded-xl">
219 <h2 class="text-lg font-semibold mb-3">活动介绍</h2> 313 <h2 class="text-lg font-semibold mb-3">活动介绍</h2>
220 <p class="text-gray-700 whitespace-pre-line"> 314 <p class="text-gray-700 whitespace-pre-line">
221 - {{ activity?.title }} 是一个精心设计的亲子互动活动,旨在增强家长与孩子之间的沟通与理解。 315 + {{
222 - 316 + activity?.title
317 + }}
318 + 是一个精心设计的亲子互动活动,旨在增强家长与孩子之间的沟通与理解。
223 通过一系列有趣的游戏和学习环节,让孩子在轻松愉快的氛围中学习知识,培养良好的学习习惯和价值观。同时,也给予家长更多指导和支持,帮助他们更好地理解孩子的需求和成长过程。 319 通过一系列有趣的游戏和学习环节,让孩子在轻松愉快的氛围中学习知识,培养良好的学习习惯和价值观。同时,也给予家长更多指导和支持,帮助他们更好地理解孩子的需求和成长过程。
224 - 320 + 活动特色: - 专业导师全程引导 - 互动性强,参与感高 - 寓教于乐,收获满满 -
225 - 活动特色: 321 + 结交志同道合的家庭
226 - - 专业导师全程引导
227 - - 互动性强,参与感高
228 - - 寓教于乐,收获满满
229 - - 结交志同道合的家庭
230 </p> 322 </p>
231 </FrostedGlass> 323 </FrostedGlass>
232 </div> 324 </div>
...@@ -237,17 +329,26 @@ const RightContent = defineComponent({ ...@@ -237,17 +329,26 @@ const RightContent = defineComponent({
237 <div class="grid grid-cols-4 gap-4"> 329 <div class="grid grid-cols-4 gap-4">
238 <div v-for="index in 12" :key="index" class="flex flex-col items-center"> 330 <div v-for="index in 12" :key="index" class="flex flex-col items-center">
239 <div class="w-14 h-14 rounded-full bg-gray-200 overflow-hidden mb-1"> 331 <div class="w-14 h-14 rounded-full bg-gray-200 overflow-hidden mb-1">
240 - <svg xmlns="http://www.w3.org/2000/svg" class="h-full w-full text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"> 332 + <svg
241 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" /> 333 + xmlns="http://www.w3.org/2000/svg"
334 + class="h-full w-full text-gray-400"
335 + fill="none"
336 + viewBox="0 0 24 24"
337 + stroke="currentColor"
338 + >
339 + <path
340 + stroke-linecap="round"
341 + stroke-linejoin="round"
342 + stroke-width="1"
343 + d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
344 + />
242 </svg> 345 </svg>
243 </div> 346 </div>
244 <span class="text-xs text-gray-600">用户{{ index }}</span> 347 <span class="text-xs text-gray-600">用户{{ index }}</span>
245 </div> 348 </div>
246 </div> 349 </div>
247 <div class="mt-4 flex justify-center"> 350 <div class="mt-4 flex justify-center">
248 - <button class="text-green-600 text-sm"> 351 + <button class="text-green-600 text-sm">查看全部参与者</button>
249 - 查看全部参与者
250 - </button>
251 </div> 352 </div>
252 </FrostedGlass> 353 </FrostedGlass>
253 </div> 354 </div>
...@@ -310,6 +411,28 @@ const RightContent = defineComponent({ ...@@ -310,6 +411,28 @@ const RightContent = defineComponent({
310 </div> 411 </div>
311 </div> 412 </div>
312 </div> 413 </div>
414 +
415 + <!-- Bottom Action Bar -->
416 + <div
417 + class="fixed bottom-16 left-0 right-0 bg-white p-3 shadow-lg flex justify-between items-center"
418 + >
419 + <div class="flex items-end">
420 + <div class="text-red-500 text-lg font-bold">¥366</div>
421 + <div class="text-xs text-gray-500 ml-1 line-through">原价¥468</div>
422 + </div>
423 + <div class="flex space-x-3">
424 + <button
425 + class="px-4 py-2 border border-green-600 text-green-600 rounded-full text-sm"
426 + >
427 + 咨询详情
428 + </button>
429 + <button
430 + class="px-6 py-2 bg-gradient-to-r from-green-500 to-green-600 text-white rounded-full text-sm font-medium shadow-md"
431 + >
432 + 立即报名
433 + </button>
434 + </div>
435 + </div>
313 </div> 436 </div>
314 </AppLayout> 437 </AppLayout>
315 </template> 438 </template>
......
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>
2 + <AppLayout :title="orderComplete ? '支付成功' : '结账'" :show-back-button="!orderComplete" @back-click="router.back()">
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">
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">
6 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="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" />
7 + </svg>
8 + <h2 class="text-xl font-bold mb-2">购物车为空</h2>
9 + <p class="text-gray-600 mb-6">您的购物车中没有任何商品</p>
10 + <button
11 + @click="router.push('/courses')"
12 + class="w-full bg-gradient-to-r from-green-500 to-green-600 text-white py-3 rounded-xl font-medium"
13 + >
14 + 浏览课程
15 + </button>
16 + </FrostedGlass>
17 + </div>
18 +
19 + <div v-else-if="orderComplete" class="h-screen flex flex-col items-center justify-center px-4">
20 + <FrostedGlass class="p-6 rounded-xl text-center">
21 + <div class="w-20 h-20 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
22 + <svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
23 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
24 + </svg>
25 + </div>
26 + <h2 class="text-2xl font-bold mb-2">支付成功!</h2>
27 + <p class="text-gray-600 mb-2">您的订单已经成功提交</p>
28 + <p class="text-gray-500 text-sm mb-6">订单号: {{ orderId }}</p>
29 + <button
30 + @click="handleBackToHome"
31 + class="w-full bg-gradient-to-r from-green-500 to-green-600 text-white py-3 rounded-xl font-medium"
32 + >
33 + 返回首页
34 + </button>
35 + </FrostedGlass>
36 + </div>
37 +
38 + <div v-else class="pb-20">
39 + <!-- Order Summary -->
40 + <div class="p-4 bg-gradient-to-r from-green-500/10 to-blue-500/10">
41 + <FrostedGlass class="rounded-xl p-4">
42 + <h3 class="font-medium mb-3">订单摘要</h3>
43 + <div class="space-y-3">
44 + <div v-for="item in cartItems" :key="`${item.type}-${item.id}`" class="flex justify-between">
45 + <div class="flex flex-1">
46 + <div class="w-12 h-12 rounded-lg overflow-hidden mr-3 flex-shrink-0">
47 + <img
48 + :src="item.imageUrl || '/assets/images/course-placeholder.jpg'"
49 + :alt="item.title"
50 + class="w-full h-full object-cover"
51 + @error="handleImageError"
52 + />
53 + </div>
54 + <div>
55 + <p class="font-medium text-sm line-clamp-1">{{ item.title }}</p>
56 + <p class="text-xs text-gray-500">
57 + {{ item.type === 'course' ? '课程' : '活动' }} · {{ item.quantity }} 份
58 + </p>
59 + </div>
60 + </div>
61 + <div class="ml-2 text-right">
62 + <p class="font-medium text-sm">{{ formatPrice(item.price * item.quantity) }}</p>
63 + <p class="text-xs text-gray-500">{{ formatPrice(item.price) }} / 份</p>
64 + </div>
65 + </div>
66 + </div>
67 +
68 + <div class="mt-4 pt-3 border-t border-gray-200">
69 + <div class="flex justify-between items-center text-sm">
70 + <span class="text-gray-600">小计</span>
71 + <span class="font-medium">{{ formatPrice(getTotalPrice()) }}</span>
72 + </div>
73 + <div class="flex justify-between items-center text-sm mt-1">
74 + <span class="text-gray-600">优惠</span>
75 + <span class="text-red-500">- ¥0.00</span>
76 + </div>
77 + <div class="flex justify-between items-center mt-2 font-medium">
78 + <span>总计</span>
79 + <span class="text-lg text-green-600">{{ formatPrice(getTotalPrice()) }}</span>
80 + </div>
81 + </div>
82 + </FrostedGlass>
83 + </div>
84 +
85 + <!-- Checkout Form -->
86 + <div class="px-4 pt-4">
87 + <form @submit.prevent="handleSubmit">
88 + <FrostedGlass class="rounded-xl p-4 mb-4">
89 + <h3 class="font-medium mb-3">个人信息</h3>
90 +
91 + <div class="space-y-3">
92 + <div>
93 + <label class="block text-sm text-gray-600 mb-1">姓名 *</label>
94 + <input
95 + v-model="formData.name"
96 + type="text"
97 + class="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm"
98 + placeholder="请输入您的姓名"
99 + required
100 + />
101 + </div>
102 +
103 + <div>
104 + <label class="block text-sm text-gray-600 mb-1">手机号码 *</label>
105 + <input
106 + v-model="formData.phone"
107 + type="tel"
108 + class="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm"
109 + placeholder="请输入您的手机号码"
110 + required
111 + />
112 + </div>
113 +
114 + <div>
115 + <label class="block text-sm text-gray-600 mb-1">电子邮箱</label>
116 + <input
117 + v-model="formData.email"
118 + type="email"
119 + class="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm"
120 + placeholder="请输入您的邮箱(选填)"
121 + />
122 + </div>
123 +
124 + <div>
125 + <label class="block text-sm text-gray-600 mb-1">联系地址 *</label>
126 + <input
127 + v-model="formData.address"
128 + type="text"
129 + class="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm"
130 + placeholder="请输入您的详细地址"
131 + required
132 + />
133 + </div>
134 +
135 + <div>
136 + <label class="block text-sm text-gray-600 mb-1">备注</label>
137 + <textarea
138 + v-model="formData.notes"
139 + class="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm resize-none h-20"
140 + placeholder="有什么需要我们注意的事项?(选填)"
141 + />
142 + </div>
143 + </div>
144 + </FrostedGlass>
145 +
146 + <FrostedGlass class="rounded-xl p-4 mb-6">
147 + <h3 class="font-medium mb-3">支付方式</h3>
148 + <div class="space-y-2">
149 + <label class="flex items-center p-3 border border-gray-200 rounded-lg bg-white/50">
150 + <input
151 + v-model="formData.paymentMethod"
152 + type="radio"
153 + value="wechat"
154 + class="mr-3"
155 + />
156 + <span class="flex-1">微信支付</span>
157 + <svg class="h-6 w-6 text-green-500" viewBox="0 0 24 24" fill="currentColor">
158 + <path d="M9.5,8.5m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" />
159 + <path d="M14.5,8.5m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" />
160 + <path d="M9.5,14.5m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" />
161 + <path d="M14.5,14.5m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" />
162 + <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" />
163 + </svg>
164 + </label>
165 +
166 + <label class="flex items-center p-3 border border-gray-200 rounded-lg bg-white/50">
167 + <input
168 + v-model="formData.paymentMethod"
169 + type="radio"
170 + value="alipay"
171 + class="mr-3"
172 + />
173 + <span class="flex-1">支付宝</span>
174 + <svg class="h-6 w-6 text-blue-500" viewBox="0 0 24 24" fill="currentColor">
175 + <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" />
176 + <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" />
177 + </svg>
178 + </label>
179 +
180 + <label class="flex items-center p-3 border border-gray-200 rounded-lg bg-white/50">
181 + <input
182 + v-model="formData.paymentMethod"
183 + type="radio"
184 + value="bank"
185 + class="mr-3"
186 + />
187 + <span class="flex-1">银行卡</span>
188 + <svg class="h-6 w-6 text-gray-500" viewBox="0 0 24 24" fill="currentColor">
189 + <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" />
190 + <path d="M5,13h2v2h-2z" />
191 + <path d="M9,13h2v2h-2z" />
192 + <path d="M13,13h2v2h-2z" />
193 + <path d="M17,13h2v2h-2z" />
194 + </svg>
195 + </label>
196 + </div>
197 + </FrostedGlass>
198 +
199 + <div class="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-100 shadow-md px-4 py-3">
200 + <div class="flex justify-between items-center mb-3">
201 + <div>
202 + <span class="text-sm text-gray-500">总计:</span>
203 + <span class="text-lg font-bold text-green-600">{{ formatPrice(getTotalPrice()) }}</span>
204 + </div>
205 + <button
206 + type="button"
207 + @click="clearCart"
208 + class="text-sm text-red-500"
209 + >
210 + 清空购物车
211 + </button>
212 + </div>
213 + <button
214 + type="submit"
215 + :disabled="isProcessing"
216 + class="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"
217 + >
218 + <template v-if="isProcessing">
219 + <div class="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin mr-2"></div>
220 + 处理中...
221 + </template>
222 + <template v-else>
223 + 确认支付
224 + </template>
225 + </button>
226 + </div>
227 + </form>
228 + </div>
229 + </div>
230 + </AppLayout>
231 +</template>
232 +
233 +<script setup>
234 +import { ref } from 'vue'
235 +import { useRouter } from 'vue-router'
236 +import AppLayout from '@/components/layout/AppLayout.vue'
237 +import FrostedGlass from '@/components/ui/FrostedGlass.vue'
238 +import { useCart } from '@/contexts/cart'
239 +
240 +const router = useRouter()
241 +const { items: cartItems, getTotalPrice, handleCheckout, clearCart } = useCart()
242 +
243 +// Form state
244 +const formData = ref({
245 + name: '',
246 + phone: '',
247 + email: '',
248 + address: '',
249 + notes: '',
250 + paymentMethod: 'wechat'
251 +})
252 +
253 +// Loading and success states
254 +const isProcessing = ref(false)
255 +const orderComplete = ref(false)
256 +const orderId = ref('')
257 +
258 +// Format price with Chinese Yuan symbol
259 +const formatPrice = (price) => {
260 + return `¥${price.toFixed(2)}`
261 +}
262 +
263 +// Handle image error
264 +const handleImageError = (e) => {
265 + e.target.src = '/assets/images/course-placeholder.jpg'
266 +}
267 +
268 +// Handle form submission
269 +const handleSubmit = async (e) => {
270 + if (!formData.value.name || !formData.value.phone || !formData.value.address) {
271 + alert('请填写必要信息')
272 + return
273 + }
274 +
275 + isProcessing.value = true
276 +
277 + try {
278 + // Process checkout
279 + const result = await handleCheckout(formData.value)
280 +
281 + if (result.success) {
282 + orderId.value = result.orderId
283 + orderComplete.value = true
284 + }
285 + } catch (error) {
286 + console.error('Checkout failed:', error)
287 + alert('支付失败,请重试')
288 + } finally {
289 + isProcessing.value = false
290 + }
291 +}
292 +
293 +// Handle navigation back to home after order completion
294 +const handleBackToHome = () => {
295 + router.push('/')
296 +}