hookehuyr

feat: 添加认证上下文、购物车上下文及个人主页功能

添加了认证上下文 `auth.js` 和 `AuthContext.jsx`,用于管理用户登录和登出状态。同时引入了购物车上下文 `CartContext.jsx`,支持购物车商品的增删改查操作。新增了个人主页 `ProfilePage.vue`,展示用户信息、打卡统计及菜单选项。
...@@ -7,6 +7,10 @@ ...@@ -7,6 +7,10 @@
7 --> 7 -->
8 <script setup> 8 <script setup>
9 import { RouterView } from "vue-router"; 9 import { RouterView } from "vue-router";
10 +import { provideAuth } from '@/contexts/auth';
11 +
12 +// 提供认证上下文
13 +provideAuth();
10 </script> 14 </script>
11 15
12 <template> 16 <template>
......
1 +<template>
2 + <div
3 + class="flex items-center justify-between p-4 border-b border-gray-100 last:border-b-0 cursor-pointer hover:bg-white/30 active:bg-white/50 transition-colors"
4 + @click="$emit('click')"
5 + >
6 + <div class="flex items-center">
7 + <div class="text-gray-500 mr-3">
8 + <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
9 + <path
10 + v-if="icon === 'clock'"
11 + stroke-linecap="round"
12 + stroke-linejoin="round"
13 + stroke-width="2"
14 + d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
15 + />
16 + <path
17 + v-if="icon === 'user'"
18 + stroke-linecap="round"
19 + stroke-linejoin="round"
20 + stroke-width="2"
21 + d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
22 + />
23 + <path
24 + v-if="icon === 'book'"
25 + stroke-linecap="round"
26 + stroke-linejoin="round"
27 + stroke-width="2"
28 + 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"
29 + />
30 + <path
31 + v-if="icon === 'heart'"
32 + stroke-linecap="round"
33 + stroke-linejoin="round"
34 + stroke-width="2"
35 + 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"
36 + />
37 + <path
38 + v-if="icon === 'wallet'"
39 + stroke-linecap="round"
40 + stroke-linejoin="round"
41 + stroke-width="2"
42 + d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z"
43 + />
44 + <path
45 + v-if="icon === 'document'"
46 + stroke-linecap="round"
47 + stroke-linejoin="round"
48 + stroke-width="2"
49 + d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01"
50 + />
51 + <path
52 + v-if="icon === 'chat'"
53 + stroke-linecap="round"
54 + stroke-linejoin="round"
55 + stroke-width="2"
56 + 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"
57 + />
58 + <path
59 + v-if="icon === 'email'"
60 + stroke-linecap="round"
61 + stroke-linejoin="round"
62 + stroke-width="2"
63 + d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
64 + />
65 + <path
66 + v-if="icon === 'question'"
67 + stroke-linecap="round"
68 + stroke-linejoin="round"
69 + stroke-width="2"
70 + d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
71 + />
72 + <path
73 + v-if="icon === 'settings'"
74 + stroke-linecap="round"
75 + stroke-linejoin="round"
76 + stroke-width="2"
77 + d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z M15 12a3 3 0 11-6 0 3 3 0 016 0z"
78 + />
79 + </svg>
80 + </div>
81 + <span class="font-medium">{{ title }}</span>
82 + <div
83 + v-if="badge"
84 + class="ml-2 px-1.5 py-0.5 bg-red-500 rounded-full text-white text-xs font-medium min-w-[18px] text-center"
85 + >
86 + {{ badge }}
87 + </div>
88 + </div>
89 + <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-400" viewBox="0 0 20 20" fill="currentColor">
90 + <path
91 + fill-rule="evenodd"
92 + d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
93 + clip-rule="evenodd"
94 + />
95 + </svg>
96 + </div>
97 +</template>
98 +
99 +<script setup>
100 +defineProps({
101 + icon: {
102 + type: String,
103 + required: true
104 + },
105 + title: {
106 + type: String,
107 + required: true
108 + },
109 + path: {
110 + type: String,
111 + required: true
112 + },
113 + badge: {
114 + type: String,
115 + default: ''
116 + }
117 +})
118 +
119 +defineEmits(['click'])
120 +</script>
1 +import React, { createContext, useContext, useState, useEffect } from 'react';
2 +
3 +/**
4 + * Authentication Context for user login/logout functionality
5 + */
6 +const AuthContext = createContext();
7 +
8 +/**
9 + * AuthProvider component to manage authentication state
10 + *
11 + * @param {Object} props - Component props
12 + * @param {ReactNode} props.children - Child elements
13 + * @returns {JSX.Element} AuthProvider component
14 + */
15 +export const AuthProvider = ({ children }) => {
16 + const [currentUser, setCurrentUser] = useState(null);
17 + const [loading, setLoading] = useState(true);
18 +
19 + // Check for saved user on mount
20 + useEffect(() => {
21 + const savedUser = localStorage.getItem('currentUser');
22 + if (savedUser) {
23 + setCurrentUser(JSON.parse(savedUser));
24 + }
25 + setLoading(false);
26 + }, []);
27 +
28 + // Login function
29 + const login = (userData) => {
30 + setCurrentUser(userData);
31 + localStorage.setItem('currentUser', JSON.stringify(userData));
32 + return true;
33 + };
34 +
35 + // Logout function
36 + const logout = () => {
37 + setCurrentUser(null);
38 + localStorage.removeItem('currentUser');
39 + };
40 +
41 + // Context value
42 + const value = {
43 + currentUser,
44 + loading,
45 + login,
46 + logout
47 + };
48 +
49 + return (
50 + <AuthContext.Provider value={value}>
51 + {!loading && children}
52 + </AuthContext.Provider>
53 + );
54 +};
55 +
56 +/**
57 + * Hook to use auth functionality throughout the app
58 + * @returns {Object} Auth context value
59 + */
60 +export const useAuth = () => {
61 + const context = useContext(AuthContext);
62 + if (!context) {
63 + throw new Error("useAuth must be used within an AuthProvider");
64 + }
65 + return context;
66 +};
67 +
68 +export default AuthContext;
...\ No newline at end of file ...\ No newline at end of file
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, onMounted } from 'vue'
2 +
3 +// 创建认证上下文的key
4 +const authKey = Symbol('auth')
5 +
6 +/**
7 + * 提供认证功能的组合式函数
8 + */
9 +export function provideAuth() {
10 + const currentUser = ref(null)
11 + const loading = ref(true)
12 +
13 + // 初始化时检查本地存储的用户信息
14 + onMounted(() => {
15 + const savedUser = localStorage.getItem('currentUser')
16 + if (savedUser) {
17 + currentUser.value = JSON.parse(savedUser)
18 + }
19 + loading.value = false
20 + })
21 +
22 + // 登录函数
23 + const login = (userData) => {
24 + currentUser.value = userData
25 + localStorage.setItem('currentUser', JSON.stringify(userData))
26 + return true
27 + }
28 +
29 + // 登出函数
30 + const logout = () => {
31 + currentUser.value = null
32 + localStorage.removeItem('currentUser')
33 + }
34 +
35 + // 提供认证上下文
36 + provide(authKey, {
37 + currentUser,
38 + loading,
39 + login,
40 + logout
41 + })
42 +
43 + return {
44 + currentUser,
45 + loading,
46 + login,
47 + logout
48 + }
49 +}
50 +
51 +/**
52 + * 使用认证功能的组合式函数
53 + * @returns {Object} 认证上下文值
54 + */
55 +export function useAuth() {
56 + const auth = inject(authKey)
57 + if (!auth) {
58 + throw new Error('useAuth 必须在 provideAuth 的范围内使用')
59 + }
60 + return auth
61 +}
1 /* 1 /*
2 * @Date: 2025-03-20 20:36:36 2 * @Date: 2025-03-20 20:36:36
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2025-03-20 20:50:41 4 + * @LastEditTime: 2025-03-20 21:04:41
5 * @FilePath: /mlaj/src/router/index.js 5 * @FilePath: /mlaj/src/router/index.js
6 * @Description: 文件描述 6 * @Description: 文件描述
7 */ 7 */
...@@ -26,6 +26,12 @@ const routes = [ ...@@ -26,6 +26,12 @@ const routes = [
26 component: () => import('../views/courses/CourseDetailPage.vue'), 26 component: () => import('../views/courses/CourseDetailPage.vue'),
27 meta: { title: 'CourseDetail' }, 27 meta: { title: 'CourseDetail' },
28 }, 28 },
29 + {
30 + path: '/profile',
31 + name: 'Profile',
32 + component: () => import('../views/profile/ProfilePage.vue'),
33 + meta: { title: 'Profile' },
34 + },
29 ] 35 ]
30 36
31 const router = createRouter({ 37 const router = createRouter({
......
1 +<template>
2 + <AppLayout title="我的" :right-content="rightContent">
3 + <div class="bg-gradient-to-br from-green-50 via-green-100/30 to-blue-50/30 min-h-screen">
4 + <!-- User Profile Header with Enhanced Design -->
5 + <div class="pt-6 pb-8 relative">
6 + <div class="absolute inset-0 bg-gradient-to-r from-green-500 to-blue-500 opacity-15"></div>
7 + <div class="relative z-10 flex flex-col items-center">
8 + <div class="w-24 h-24 rounded-full overflow-hidden border-4 border-white shadow-lg mb-4">
9 + <img
10 + :src="profile.avatar || '/assets/images/user-avatar-1.jpg'"
11 + :alt="profile.name"
12 + class="w-full h-full object-cover"
13 + @error="handleImageError"
14 + />
15 + </div>
16 + <h2 class="text-2xl font-bold mb-1">{{ profile.name }}</h2>
17 + <div class="flex items-center text-sm text-gray-600">
18 + <span>会员等级: 普通会员</span>
19 + <span class="mx-2">|</span>
20 + <span>ID: 88361425</span>
21 + </div>
22 + </div>
23 + </div>
24 +
25 + <!-- Check-in Statistics -->
26 + <div class="px-4 mb-5">
27 + <FrostedGlass class="p-4 rounded-xl">
28 + <div class="flex justify-between items-center mb-4">
29 + <h3 class="font-semibold text-base">打卡统计</h3>
30 + <span class="text-xs text-blue-500">查看更多</span>
31 + </div>
32 +
33 + <div class="flex justify-between">
34 + <div class="flex flex-col items-center">
35 + <div class="text-2xl font-bold text-gray-800">{{ profile.checkIns?.totalDays || 0 }}</div>
36 + <div class="text-xs text-gray-500 mt-1">累计打卡</div>
37 + </div>
38 + <div class="flex flex-col items-center">
39 + <div class="text-2xl font-bold text-green-600">{{ profile.checkIns?.currentStreak || 0 }}</div>
40 + <div class="text-xs text-gray-500 mt-1">连续打卡</div>
41 + </div>
42 + <div class="flex flex-col items-center">
43 + <div class="text-2xl font-bold text-blue-600">{{ profile.checkIns?.longestStreak || 0 }}</div>
44 + <div class="text-xs text-gray-500 mt-1">最长连续</div>
45 + </div>
46 + <div>
47 + <button class="bg-gradient-to-r from-green-500 to-green-600 text-white py-2 px-6 rounded-full text-sm shadow-sm">
48 + 立即打卡
49 + </button>
50 + </div>
51 + </div>
52 + </FrostedGlass>
53 + </div>
54 +
55 + <!-- Check-in Types -->
56 + <div class="px-4 mb-5">
57 + <FrostedGlass class="p-4 rounded-xl">
58 + <h3 class="font-semibold text-base mb-4">打卡项目</h3>
59 + <div class="grid grid-cols-4 gap-2">
60 + <div v-for="type in checkInTypes" :key="type.id" class="flex flex-col items-center">
61 + <div class="w-12 h-12 rounded-full bg-green-100 flex items-center justify-center mb-1">
62 + <svg v-if="type.icon === 'book'" xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
63 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="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" />
64 + </svg>
65 + <svg v-if="type.icon === 'running'" xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
66 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
67 + </svg>
68 + <svg v-if="type.icon === 'graduation-cap'" xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
69 + <path d="M12 14l9-5-9-5-9 5 9 5z" />
70 + <path d="M12 14l6.16-3.422a12.083 12.083 0 01.665 6.479A11.952 11.952 0 0012 20.055a11.952 11.952 0 00-6.824-2.998 12.078 12.078 0 01.665-6.479L12 14z" />
71 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 14l9-5-9-5-9 5 9 5zm0 0l6.16-3.422a12.083 12.083 0 01.665 6.479A11.952 11.952 0 0012 20.055a11.952 11.952 0 00-6.824-2.998 12.078 12.078 0 01.665-6.479L12 14zm-4 6v-7.5l4-2.222" />
72 + </svg>
73 + <svg v-if="type.icon === 'pencil-alt'" xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
74 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
75 + </svg>
76 + </div>
77 + <span class="text-xs">{{ type.name }}</span>
78 + </div>
79 + </div>
80 + </FrostedGlass>
81 + </div>
82 +
83 + <!-- User Menu Options -->
84 + <div class="px-4 pb-16">
85 + <FrostedGlass class="rounded-xl overflow-hidden mb-5">
86 + <MenuItem
87 + v-for="item in menuItems1"
88 + :key="item.path"
89 + v-bind="item"
90 + @click="handleMenuClick(item.path)"
91 + />
92 + </FrostedGlass>
93 +
94 + <FrostedGlass class="rounded-xl overflow-hidden mb-5">
95 + <MenuItem
96 + v-for="item in menuItems2"
97 + :key="item.path"
98 + v-bind="item"
99 + @click="handleMenuClick(item.path)"
100 + />
101 + </FrostedGlass>
102 +
103 + <FrostedGlass class="rounded-xl overflow-hidden mb-5">
104 + <MenuItem
105 + v-for="item in menuItems3"
106 + :key="item.path"
107 + v-bind="item"
108 + @click="handleMenuClick(item.path)"
109 + />
110 + </FrostedGlass>
111 +
112 + <!-- Version Info -->
113 + <div class="text-center text-xs text-gray-400 mb-4">
114 + 亲子教育 App v1.2.0
115 + </div>
116 +
117 + <!-- Logout Button -->
118 + <button
119 + @click="handleLogout"
120 + class="w-full bg-white/70 backdrop-blur-sm text-red-500 py-3 rounded-xl mb-6 font-medium shadow-sm"
121 + >
122 + 退出登录
123 + </button>
124 + </div>
125 + </div>
126 + </AppLayout>
127 +</template>
128 +
129 +<script setup>
130 +import { ref, h } from 'vue'
131 +import { useRouter } from 'vue-router'
132 +import AppLayout from '@/components/layout/AppLayout.vue'
133 +import FrostedGlass from '@/components/ui/FrostedGlass.vue'
134 +import MenuItem from '@/components/ui/MenuItem.vue'
135 +import { useAuth } from '@/contexts/auth'
136 +import { userProfile, checkInTypes } from '@/utils/mockData'
137 +
138 +const router = useRouter()
139 +const { currentUser, logout } = useAuth()
140 +const profile = ref(userProfile)
141 +
142 +// Handle logout
143 +const handleLogout = () => {
144 + logout()
145 + router.push('/login')
146 +}
147 +
148 +// Handle menu item click
149 +const handleMenuClick = (path) => {
150 + router.push(path)
151 +}
152 +
153 +// Handle image error
154 +const handleImageError = (e) => {
155 + e.target.onerror = null
156 + e.target.src = '/assets/images/user-avatar-1.jpg'
157 +}
158 +
159 +// Right content component
160 +const rightContent = h('div', { class: 'flex items-center' }, [
161 + h('button', { class: 'p-2' }, [
162 + h('svg', {
163 + xmlns: 'http://www.w3.org/2000/svg',
164 + class: 'h-6 w-6 text-gray-700',
165 + fill: 'none',
166 + viewBox: '0 0 24 24',
167 + stroke: 'currentColor'
168 + }, [
169 + h('path', {
170 + 'stroke-linecap': 'round',
171 + 'stroke-linejoin': 'round',
172 + 'stroke-width': '2',
173 + d: 'M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9'
174 + })
175 + ])
176 + ])
177 +])
178 +
179 +// Menu items
180 +const menuItems1 = [
181 + {
182 + icon: 'clock',
183 + title: '学习记录',
184 + path: '/profile/learning-records',
185 + badge: 'NEW'
186 + },
187 + {
188 + icon: 'user',
189 + title: '我的活动',
190 + path: '/profile/activities'
191 + },
192 + {
193 + icon: 'book',
194 + title: '我的课程',
195 + path: '/profile/courses'
196 + },
197 + {
198 + icon: 'heart',
199 + title: '我的收藏',
200 + path: '/profile/favorites'
201 + }
202 +]
203 +
204 +const menuItems2 = [
205 + {
206 + icon: 'wallet',
207 + title: '我的钱包',
208 + path: '/profile/wallet'
209 + },
210 + {
211 + icon: 'document',
212 + title: '我的订单',
213 + path: '/profile/orders'
214 + },
215 + {
216 + icon: 'chat',
217 + title: '我的圈子',
218 + path: '/profile/community'
219 + }
220 +]
221 +
222 +const menuItems3 = [
223 + {
224 + icon: 'email',
225 + title: '消息中心',
226 + path: '/profile/messages',
227 + badge: '3'
228 + },
229 + {
230 + icon: 'question',
231 + title: '帮助中心',
232 + path: '/profile/help'
233 + },
234 + {
235 + icon: 'settings',
236 + title: '设置',
237 + path: '/profile/settings'
238 + }
239 +]
240 +</script>