hookehuyr

feat: 添加新图片资源并初始化项目基础结构

添加了多个新图片资源,包括用户头像、课程封面和活动图片。同时,初始化了项目的基础结构,包括配置文件的设置(如postcss.config.js、tailwind.config.js、.gitignore等)、主入口文件(main.js)的创建,以及基础组件和页面的实现(如App.vue、HelloWorld.vue、FrostedGlass.vue等)。这些更改为后续开发提供了必要的资源支持和基础框架。
Showing 81 changed files with 1595 additions and 0 deletions
1 +# Logs
2 +logs
3 +*.log
4 +npm-debug.log*
5 +yarn-debug.log*
6 +yarn-error.log*
7 +pnpm-debug.log*
8 +lerna-debug.log*
9 +
10 +node_modules
11 +dist
12 +dist-ssr
13 +*.local
14 +
15 +# Editor directories and files
16 +.vscode/*
17 +!.vscode/extensions.json
18 +.idea
19 +.DS_Store
20 +*.suo
21 +*.ntvs*
22 +*.njsproj
23 +*.sln
24 +*.sw?
25 +
26 +.history
27 +node_modules
28 +.vscode
1 +# Vue 3 + Vite
2 +
3 +This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
4 +
5 +Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).
1 +<!doctype html>
2 +<html lang="en">
3 + <head>
4 + <meta charset="UTF-8" />
5 + <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6 + <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7 + <title>Vite + Vue</title>
8 + </head>
9 + <body>
10 + <div id="app"></div>
11 + <script type="module" src="/src/main.js"></script>
12 + </body>
13 +</html>
This diff could not be displayed because it is too large.
1 +{
2 + "name": "vue-vite",
3 + "private": true,
4 + "version": "0.0.0",
5 + "type": "module",
6 + "scripts": {
7 + "dev": "vite",
8 + "build": "vite build",
9 + "preview": "vite preview"
10 + },
11 + "dependencies": {
12 + "@vant/use": "^1.6.0",
13 + "vant": "^4.9.18",
14 + "vue": "^3.5.13",
15 + "vue-router": "^4.5.0"
16 + },
17 + "devDependencies": {
18 + "@vitejs/plugin-vue": "^5.2.1",
19 + "@vitejs/plugin-vue-jsx": "^4.1.2",
20 + "autoprefixer": "^10.4.19",
21 + "postcss": "^8.4.35",
22 + "tailwindcss": "^3.4.1",
23 + "vite": "^6.2.0"
24 + }
25 +}
1 +export default {
2 + plugins: {
3 + tailwindcss: {},
4 + autoprefixer: {},
5 + },
6 +}
1 +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
...\ No newline at end of file ...\ No newline at end of file
1 +<!--
2 + * @Date: 2025-03-20 19:53:12
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2025-03-20 20:06:10
5 + * @FilePath: /vue-vite/src/App.vue
6 + * @Description: 文件描述
7 +-->
8 +<script setup>
9 +import { RouterView } from "vue-router";
10 +</script>
11 +
12 +<template>
13 + <div>
14 + <router-view>
15 + <template v-slot="{ Component }">
16 + <Suspense>
17 + <template #default>
18 + <div>
19 + <component :is="Component" />
20 + </div>
21 + </template>
22 + <template #fallback>
23 + <div
24 + class="flex items-center justify-center h-screen bg-gradient-to-br from-green-50 via-teal-50 to-blue-50"
25 + >
26 + <div class="bg-white/20 backdrop-blur-md rounded-xl p-6 shadow-lg">
27 + <div
28 + class="animate-spin rounded-full h-12 w-12 border-b-2 border-green-500 mx-auto"
29 + ></div>
30 + <p class="mt-4 text-gray-700">加载中...</p>
31 + </div>
32 + </div>
33 + </template>
34 + </Suspense>
35 + </template>
36 + </router-view>
37 + </div>
38 +</template>
39 +
40 +<style>
41 +#app {
42 + width: 100%;
43 + min-height: 100vh;
44 +}
45 +</style>
1 +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
...\ No newline at end of file ...\ No newline at end of file
1 +<script setup>
2 +import { ref } from 'vue'
3 +
4 +defineProps({
5 + msg: String,
6 +})
7 +
8 +const count = ref(0)
9 +</script>
10 +
11 +<template>
12 + <h1>{{ msg }}</h1>
13 +
14 + <div class="card">
15 + <button type="button" @click="count++">count is {{ count }}</button>
16 + <p>
17 + Edit
18 + <code>components/HelloWorld.vue</code> to test HMR
19 + </p>
20 + </div>
21 +
22 + <p>
23 + Check out
24 + <a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
25 + >create-vue</a
26 + >, the official Vue + Vite starter
27 + </p>
28 + <p>
29 + Learn more about IDE Support for Vue in the
30 + <a
31 + href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
32 + target="_blank"
33 + >Vue Docs Scaling up Guide</a
34 + >.
35 + </p>
36 + <p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
37 +</template>
38 +
39 +<style scoped>
40 +.read-the-docs {
41 + color: #888;
42 +}
43 +</style>
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
1 +<template>
2 + <div class="bg-gradient-to-br from-green-50 via-teal-50 to-blue-50 min-h-screen pb-16">
3 + <GradientHeader
4 + :title="title"
5 + :showBackButton="showBackButton"
6 + :onBack="handleBack"
7 + :rightContent="rightContent"
8 + />
9 + <main class="pb-16">
10 + <slot></slot>
11 + </main>
12 + <BottomNav />
13 + </div>
14 +</template>
15 +
16 +<script setup>
17 +import { defineProps } from 'vue'
18 +import BottomNav from './BottomNav.vue'
19 +import GradientHeader from '../ui/GradientHeader.vue'
20 +
21 +const props = defineProps({
22 + title: {
23 + type: String,
24 + required: true
25 + },
26 + showBackButton: {
27 + type: Boolean,
28 + default: false
29 + },
30 + onBack: {
31 + type: Function,
32 + default: null
33 + },
34 + rightContent: {
35 + type: Object,
36 + default: null
37 + }
38 +})
39 +
40 +const handleBack = () => {
41 + if (props.onBack) {
42 + props.onBack()
43 + } else {
44 + window.history.back()
45 + }
46 +}
47 +</script>
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
1 +<template>
2 + <nav class="fixed bottom-0 left-0 right-0 bg-white/70 backdrop-blur-lg border-t border-gray-100 z-50">
3 + <div class="flex justify-around items-center h-16">
4 + <router-link
5 + v-for="item in navItems"
6 + :key="item.name"
7 + :to="item.path"
8 + class="flex flex-col items-center justify-center w-1/4 h-full"
9 + :class="{
10 + 'text-green-500': isActive(item.path),
11 + 'text-gray-500': !isActive(item.path)
12 + }"
13 + >
14 + <svg
15 + xmlns="http://www.w3.org/2000/svg"
16 + class="h-6 w-6 mb-1"
17 + :class="{
18 + 'text-green-500': isActive(item.path),
19 + 'text-gray-500': !isActive(item.path)
20 + }"
21 + fill="none"
22 + viewBox="0 0 24 24"
23 + stroke="currentColor"
24 + v-html="item.icon"
25 + />
26 + <span class="text-xs">{{ item.name }}</span>
27 + </router-link>
28 + </div>
29 + </nav>
30 +</template>
31 +
32 +<script setup>
33 +import { computed } from 'vue'
34 +import { useRoute } from 'vue-router'
35 +
36 +const route = useRoute()
37 +
38 +const navItems = [
39 + {
40 + name: '首页',
41 + path: '/',
42 + icon: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="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" />'
43 + },
44 + {
45 + name: '课程',
46 + path: '/courses',
47 + icon: '<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" />'
48 + },
49 + {
50 + name: '空间',
51 + path: '/community',
52 + icon: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="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" />'
53 + },
54 + {
55 + name: '我的',
56 + path: '/profile',
57 + icon: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="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" />'
58 + }
59 +]
60 +
61 +const isActive = (path) => {
62 + return route.path === path || (path !== '/' && route.path.startsWith(path))
63 +}
64 +</script>
1 +import React from 'react';
2 +import { Link } from 'react-router-dom';
3 +import FrostedGlass from './FrostedGlass';
4 +
5 +/**
6 + * ActivityCard component displays an activity item in the activities list
7 + *
8 + * @param {Object} props - Component props
9 + * @param {Object} props.activity - Activity data
10 + * @returns {JSX.Element} ActivityCard component
11 + */
12 +const ActivityCard = ({ activity }) => {
13 + // Function to get the appropriate status class
14 + const getStatusClass = (status) => {
15 + switch (status) {
16 + case '活动中':
17 + return 'bg-blue-100 text-blue-600';
18 + case '进行中':
19 + return 'bg-green-100 text-green-600';
20 + case '即将开始':
21 + return 'bg-orange-100 text-orange-600';
22 + case '已结束':
23 + return 'bg-gray-100 text-gray-600';
24 + default:
25 + return 'bg-gray-100 text-gray-600';
26 + }
27 + };
28 +
29 + return (
30 + <Link to={`/activities/${activity.id}`}>
31 + <FrostedGlass className="flex overflow-hidden rounded-xl shadow-sm">
32 + {/* Activity Image */}
33 + <div className="w-1/3 h-28 relative">
34 + <img
35 + src={activity.imageUrl}
36 + alt={activity.title}
37 + className="w-full h-full object-cover"
38 + />
39 + {activity.isHot && (
40 + <div className="absolute top-0 left-0 bg-red-500 text-white text-xs px-2 py-0.5">
41 + 热门
42 + </div>
43 + )}
44 + </div>
45 +
46 + {/* Activity Info */}
47 + <div className="flex-1 p-3 flex flex-col justify-between">
48 + <div>
49 + <h3 className="font-medium text-base mb-1 line-clamp-1">{activity.title}</h3>
50 +
51 + {/* Status Tags */}
52 + <div className="flex items-center space-x-2 mb-1">
53 + <span className={`px-2 py-0.5 rounded-full text-xs ${getStatusClass(activity.status)}`}>
54 + {activity.status}
55 + </span>
56 + {activity.isFree && (
57 + <span className="px-2 py-0.5 rounded-full text-xs bg-green-100 text-green-600">
58 + 免费
59 + </span>
60 + )}
61 + </div>
62 + </div>
63 +
64 + {/* Location and Time */}
65 + <div className="text-xs text-gray-500 space-y-1">
66 + <div className="flex items-center">
67 + <svg xmlns="http://www.w3.org/2000/svg" className="h-3.5 w-3.5 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
68 + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={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" />
69 + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
70 + </svg>
71 + <span>{activity.location}</span>
72 + </div>
73 +
74 + <div className="flex items-center">
75 + <svg xmlns="http://www.w3.org/2000/svg" className="h-3.5 w-3.5 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
76 + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={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" />
77 + </svg>
78 + <span>{activity.period}</span>
79 + </div>
80 + </div>
81 +
82 + {/* Bottom Info Section */}
83 + <div className="mt-1 flex items-center justify-between">
84 + {activity.price ? (
85 + <div className="flex items-baseline">
86 + <span className="text-red-500 font-medium">¥{activity.price}</span>
87 + {activity.originalPrice && (
88 + <span className="text-xs text-gray-400 ml-1 line-through">¥{activity.originalPrice}</span>
89 + )}
90 + </div>
91 + ) : (
92 + <div></div> // Empty div for spacing when no price
93 + )}
94 +
95 + <div className="flex items-center text-xs text-gray-500">
96 + <svg xmlns="http://www.w3.org/2000/svg" className="h-3.5 w-3.5 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
97 + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={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" />
98 + </svg>
99 + <span>{activity.participantsCount || '15'}/{activity.maxParticipants || '30'}</span>
100 + </div>
101 + </div>
102 + </div>
103 + </FrostedGlass>
104 + </Link>
105 + );
106 +};
107 +
108 +export default ActivityCard;
...\ No newline at end of file ...\ No newline at end of file
1 +<template>
2 + <router-link :to="`/activities/${activity.id}`">
3 + <FrostedGlass class="flex overflow-hidden rounded-xl shadow-sm">
4 + <!-- Activity Image -->
5 + <div class="w-1/3 h-28 relative">
6 + <img
7 + :src="activity.imageUrl"
8 + :alt="activity.title"
9 + class="w-full h-full object-cover"
10 + />
11 + <div v-if="activity.isHot" class="absolute top-0 left-0 bg-red-500 text-white text-xs px-2 py-0.5">
12 + 热门
13 + </div>
14 + </div>
15 +
16 + <!-- Activity Info -->
17 + <div class="flex-1 p-3 flex flex-col justify-between">
18 + <div>
19 + <h3 class="font-medium text-base mb-1 line-clamp-1">{{ activity.title }}</h3>
20 +
21 + <!-- Status Tags -->
22 + <div class="flex items-center space-x-2 mb-1">
23 + <span :class="['px-2 py-0.5 rounded-full text-xs', getStatusClass(activity.status)]">
24 + {{ activity.status }}
25 + </span>
26 + <span v-if="activity.isFree" class="px-2 py-0.5 rounded-full text-xs bg-green-100 text-green-600">
27 + 免费
28 + </span>
29 + </div>
30 + </div>
31 +
32 + <!-- Location and Time -->
33 + <div class="text-xs text-gray-500 space-y-1">
34 + <div class="flex items-center">
35 + <svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
36 + <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" />
37 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
38 + </svg>
39 + <span>{{ activity.location }}</span>
40 + </div>
41 +
42 + <div class="flex items-center">
43 + <svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
44 + <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" />
45 + </svg>
46 + <span>{{ activity.period }}</span>
47 + </div>
48 + </div>
49 +
50 + <!-- Bottom Info Section -->
51 + <div class="mt-1 flex items-center justify-between">
52 + <div v-if="activity.price" class="flex items-baseline">
53 + <span class="text-red-500 font-medium">¥{{ activity.price }}</span>
54 + <span v-if="activity.originalPrice" class="text-xs text-gray-400 ml-1 line-through">¥{{ activity.originalPrice }}</span>
55 + </div>
56 + <div v-else></div>
57 +
58 + <div class="flex items-center text-xs text-gray-500">
59 + <svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
60 + <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" />
61 + </svg>
62 + <span>{{ activity.participantsCount || '15' }}/{{ activity.maxParticipants || '30' }}</span>
63 + </div>
64 + </div>
65 + </div>
66 + </FrostedGlass>
67 + </router-link>
68 +</template>
69 +
70 +<script setup>
71 +import { defineProps } from 'vue'
72 +import FrostedGlass from './FrostedGlass.vue'
73 +
74 +defineProps({
75 + activity: {
76 + type: Object,
77 + required: true
78 + }
79 +})
80 +
81 +const getStatusClass = (status) => {
82 + switch (status) {
83 + case '活动中':
84 + return 'bg-blue-100 text-blue-600'
85 + case '进行中':
86 + return 'bg-green-100 text-green-600'
87 + case '即将开始':
88 + return 'bg-orange-100 text-orange-600'
89 + case '已结束':
90 + return 'bg-gray-100 text-gray-600'
91 + default:
92 + return 'bg-gray-100 text-gray-600'
93 + }
94 +}
95 +</script>
1 +import React from 'react';
2 +import { Link } from 'react-router-dom';
3 +
4 +/**
5 + * CourseCard component displays a course item in the course list
6 + *
7 + * @param {Object} props - Component props
8 + * @param {Object} props.course - Course data
9 + * @returns {JSX.Element} CourseCard component
10 + */
11 +const CourseCard = ({ course }) => {
12 + return (
13 + <Link to={`/courses/${course.id}`} className="flex bg-white rounded-lg overflow-hidden shadow-sm">
14 + <div className="w-1/3 h-28">
15 + <img
16 + src={course.imageUrl}
17 + alt={course.title}
18 + className="w-full h-full object-cover"
19 + />
20 + </div>
21 + <div className="flex-1 p-3 flex flex-col justify-between">
22 + <div>
23 + <h3 className="font-medium text-sm mb-1 line-clamp-2">{course.title}</h3>
24 + <div className="text-gray-500 text-xs">{course.subtitle}</div>
25 + </div>
26 + <div className="flex justify-between items-end mt-1">
27 + <div className="text-orange-500 font-semibold">¥{course.price}</div>
28 + <div className="text-gray-400 text-xs">
29 + {course.subscribers}人订阅
30 + </div>
31 + </div>
32 + <div className="text-gray-400 text-xs">
33 + 已更新{course.updatedLessons}期 | {course.subscribers}人订阅
34 + </div>
35 + </div>
36 + </Link>
37 + );
38 +};
39 +
40 +export default CourseCard;
...\ No newline at end of file ...\ No newline at end of file
1 +<template>
2 + <router-link :to="`/courses/${course.id}`" class="flex bg-white rounded-lg overflow-hidden shadow-sm">
3 + <div class="w-1/3 h-28">
4 + <img
5 + :src="course.imageUrl"
6 + :alt="course.title"
7 + class="w-full h-full object-cover"
8 + />
9 + </div>
10 + <div class="flex-1 p-3 flex flex-col justify-between">
11 + <div>
12 + <h3 class="font-medium text-sm mb-1 line-clamp-2">{{ course.title }}</h3>
13 + <div class="text-gray-500 text-xs">{{ course.subtitle }}</div>
14 + </div>
15 + <div class="flex justify-between items-end mt-1">
16 + <div class="text-orange-500 font-semibold">¥{{ course.price }}</div>
17 + <div class="text-gray-400 text-xs">
18 + {{ course.subscribers }}人订阅
19 + </div>
20 + </div>
21 + <div class="text-gray-400 text-xs">
22 + 已更新{{ course.updatedLessons }}期 | {{ course.subscribers }}人订阅
23 + </div>
24 + </div>
25 + </router-link>
26 +</template>
27 +
28 +<script setup>
29 +import { defineProps } from 'vue'
30 +
31 +defineProps({
32 + course: {
33 + type: Object,
34 + required: true
35 + }
36 +})
37 +</script>
1 +import React from 'react';
2 +
3 +/**
4 + * FrostedGlass component creates a container with a frosted glass effect
5 + * using backdrop-filter blur and a semi-transparent white background.
6 + *
7 + * @param {Object} props - Component props
8 + * @param {ReactNode} props.children - Child elements
9 + * @param {string} props.className - Additional CSS classes
10 + * @returns {JSX.Element} FrostedGlass component
11 + */
12 +const FrostedGlass = ({ children, className = '' }) => {
13 + return (
14 + <div
15 + className={`bg-white/20 backdrop-blur-md rounded-xl border border-white/30
16 + shadow-lg ${className}`}
17 + >
18 + {children}
19 + </div>
20 + );
21 +};
22 +
23 +export default FrostedGlass;
...\ No newline at end of file ...\ No newline at end of file
1 +<template>
2 + <div
3 + :class="[
4 + 'bg-white/20 backdrop-blur-md rounded-xl border border-white/30 shadow-lg',
5 + className
6 + ]"
7 + >
8 + <slot></slot>
9 + </div>
10 +</template>
11 +
12 +<script setup>
13 +import { defineProps } from 'vue'
14 +
15 +defineProps({
16 + className: {
17 + type: String,
18 + default: ''
19 + }
20 +})
21 +</script>
1 +import React from 'react';
2 +
3 +/**
4 + * GradientHeader component for page headers with gradient background
5 + * and navigation elements.
6 + *
7 + * @param {Object} props - Component props
8 + * @param {string} props.title - Header title
9 + * @param {boolean} props.showBackButton - Whether to show back button
10 + * @param {Function} props.onBack - Back button click handler
11 + * @param {ReactNode} props.rightContent - Content to display on the right side
12 + * @returns {JSX.Element} GradientHeader component
13 + */
14 +const GradientHeader = ({ title, showBackButton = false, onBack, rightContent }) => {
15 + return (
16 + <header className="bg-gradient-to-r from-green-50 to-blue-50 p-4 relative">
17 + <div className="flex items-center justify-between">
18 + {showBackButton && (
19 + <button
20 + onClick={onBack}
21 + className="p-2 rounded-full bg-white/30 backdrop-blur-sm"
22 + aria-label="返回"
23 + >
24 + <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 text-gray-700" fill="none" viewBox="0 0 24 24" stroke="currentColor">
25 + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
26 + </svg>
27 + </button>
28 + )}
29 + <h1 className={`text-xl font-medium text-center ${showBackButton ? 'flex-1' : ''}`}>
30 + {title}
31 + </h1>
32 + {rightContent && <div>{rightContent}</div>}
33 + </div>
34 + </header>
35 + );
36 +};
37 +
38 +export default GradientHeader;
...\ No newline at end of file ...\ No newline at end of file
1 +<template>
2 + <header class="bg-gradient-to-r from-green-50 to-blue-50 p-4 relative">
3 + <div class="flex items-center justify-between">
4 + <button
5 + v-if="showBackButton"
6 + @click="onBack"
7 + class="p-2 rounded-full bg-white/30 backdrop-blur-sm"
8 + aria-label="返回"
9 + >
10 + <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-700" fill="none" viewBox="0 0 24 24" stroke="currentColor">
11 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
12 + </svg>
13 + </button>
14 + <h1 :class="['text-xl font-medium text-center', showBackButton ? 'flex-1' : '']">
15 + {{ title }}
16 + </h1>
17 + <div v-if="rightContent" v-html="rightContent.content"></div>
18 + </div>
19 + </header>
20 +</template>
21 +
22 +<script setup>
23 +import { defineProps } from 'vue'
24 +
25 +defineProps({
26 + title: {
27 + type: String,
28 + required: true
29 + },
30 + showBackButton: {
31 + type: Boolean,
32 + default: false
33 + },
34 + onBack: {
35 + type: Function,
36 + default: () => {}
37 + },
38 + rightContent: {
39 + type: Object,
40 + default: null
41 + }
42 +})
43 +</script>
1 +import React from 'react';
2 +import { Link } from 'react-router-dom';
3 +
4 +/**
5 + * LiveStreamCard component displays a live stream in the courses page
6 + *
7 + * @param {Object} props - Component props
8 + * @param {Object} props.stream - Stream data
9 + * @returns {JSX.Element} LiveStreamCard component
10 + */
11 +const LiveStreamCard = ({ stream }) => {
12 + return (
13 + <div className="relative">
14 + {/* Live indicator */}
15 + <div className="absolute top-2 left-2 bg-red-500/90 text-white text-xs px-2 py-1 rounded flex items-center z-10">
16 + <div className="w-2 h-2 bg-white rounded-full mr-1 animate-pulse"></div>
17 + 直播中
18 + </div>
19 +
20 + <Link to={`/courses/${stream.id}`} className="block rounded-lg overflow-hidden shadow-sm relative">
21 + <img
22 + src={stream.imageUrl}
23 + alt={stream.title}
24 + className="w-full h-28 object-cover"
25 + />
26 +
27 + {/* Gradient overlay */}
28 + <div className="absolute inset-0 bg-gradient-to-b from-transparent to-black/60"></div>
29 +
30 + {/* Stream info */}
31 + <div className="absolute bottom-2 left-2 right-2">
32 + <h3 className="text-white text-sm font-medium">{stream.title}{stream.subtitle}</h3>
33 + <div className="flex items-center mt-1">
34 + <div className="flex -space-x-2">
35 + <div className="w-5 h-5 rounded-full bg-gray-300 border border-white"></div>
36 + <div className="w-5 h-5 rounded-full bg-gray-400 border border-white"></div>
37 + <div className="w-5 h-5 rounded-full bg-gray-500 border border-white"></div>
38 + </div>
39 + <span className="text-white text-xs ml-1">{stream.viewers}人在看</span>
40 + <button className="ml-auto bg-green-500 text-white text-xs px-2 py-1 rounded">
41 + 立即观看
42 + </button>
43 + </div>
44 + </div>
45 + </Link>
46 + </div>
47 + );
48 +};
49 +
50 +export default LiveStreamCard;
...\ No newline at end of file ...\ No newline at end of file
1 +<!--
2 + * @Date: 2025-03-20 15:33:07
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2025-03-20 15:33:08
5 + * @FilePath: /react_template_1/src/components/ui/LiveStreamCard.vue
6 + * @Description: 文件描述
7 +-->
8 +<template>
9 + <div class="relative">
10 + <!-- Live indicator -->
11 + <div class="absolute top-2 left-2 bg-red-500/90 text-white text-xs px-2 py-1 rounded flex items-center z-10">
12 + <div class="w-2 h-2 bg-white rounded-full mr-1 animate-pulse"></div>
13 + 直播中
14 + </div>
15 +
16 + <router-link :to="`/courses/${stream.id}`" class="block rounded-lg overflow-hidden shadow-sm relative">
17 + <img
18 + :src="stream.imageUrl"
19 + :alt="stream.title"
20 + class="w-full h-28 object-cover"
21 + />
22 +
23 + <!-- Gradient overlay -->
24 + <div class="absolute inset-0 bg-gradient-to-b from-transparent to-black/60"></div>
25 +
26 + <!-- Stream info -->
27 + <div class="absolute bottom-2 left-2 right-2">
28 + <h3 class="text-white text-sm font-medium">「{{ stream.title }}」{{ stream.subtitle }}</h3>
29 + <div class="flex items-center mt-1">
30 + <div class="flex -space-x-2">
31 + <div class="w-5 h-5 rounded-full bg-gray-300 border border-white"></div>
32 + <div class="w-5 h-5 rounded-full bg-gray-400 border border-white"></div>
33 + <div class="w-5 h-5 rounded-full bg-gray-500 border border-white"></div>
34 + </div>
35 + <span class="text-white text-xs ml-1">{{ stream.viewers }}人在看</span>
36 + <button class="ml-auto bg-green-500 text-white text-xs px-2 py-1 rounded">
37 + 立即观看
38 + </button>
39 + </div>
40 + </div>
41 + </router-link>
42 + </div>
43 +</template>
44 +
45 +<script setup>
46 +import { defineProps } from 'vue'
47 +
48 +defineProps({
49 + stream: {
50 + type: Object,
51 + required: true
52 + }
53 +})
54 +</script>
1 +import React from 'react';
2 +import FrostedGlass from './FrostedGlass';
3 +
4 +/**
5 + * SearchBar component with frosted glass effect
6 + *
7 + * @param {Object} props - Component props
8 + * @param {string} props.placeholder - Placeholder text
9 + * @param {Function} props.onSearch - Search callback function
10 + * @returns {JSX.Element} SearchBar component
11 + */
12 +const SearchBar = ({ placeholder = '搜索', onSearch }) => {
13 + return (
14 + <FrostedGlass className="px-4 py-2 mx-4 my-3 flex items-center">
15 + <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 text-gray-400 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
16 + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
17 + </svg>
18 + <input
19 + type="text"
20 + placeholder={placeholder}
21 + className="bg-transparent outline-none flex-1 text-gray-700 placeholder-gray-400"
22 + onChange={(e) => onSearch && onSearch(e.target.value)}
23 + />
24 + </FrostedGlass>
25 + );
26 +};
27 +
28 +export default SearchBar;
...\ No newline at end of file ...\ No newline at end of file
1 +<template>
2 + <FrostedGlass class="px-4 py-2 mx-4 my-3 flex items-center">
3 + <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-400 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
4 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
5 + </svg>
6 + <input
7 + type="text"
8 + :placeholder="placeholder"
9 + class="bg-transparent outline-none flex-1 text-gray-700 placeholder-gray-400"
10 + @input="handleSearch"
11 + />
12 + </FrostedGlass>
13 +</template>
14 +
15 +<script setup>
16 +import { defineProps, defineEmits } from 'vue'
17 +import FrostedGlass from './FrostedGlass.vue'
18 +
19 +const props = defineProps({
20 + placeholder: {
21 + type: String,
22 + default: '搜索'
23 + }
24 +})
25 +
26 +const emit = defineEmits(['search'])
27 +
28 +const handleSearch = (e) => {
29 + emit('search', e.target.value)
30 +}
31 +</script>
1 +import React from 'react';
2 +import PropTypes from 'prop-types';
3 +
4 +/**
5 + * SummerCampCard component - displays summer camp information with image background
6 + * @param {Object} props - Component props
7 + * @returns {JSX.Element} - Rendered component
8 + */
9 +const SummerCampCard = ({
10 + title = "大国少年-世界正东方",
11 + subtitle = "亲子夏令营",
12 + badge = "亲子夏令营",
13 + price = "¥1280",
14 + discount = "限时优惠",
15 + episodes = 16,
16 + subscribers = 1140
17 +}) => {
18 + return (
19 + <div className="relative overflow-hidden rounded-b-3xl shadow-lg">
20 + {/* Background image with overlay */}
21 + <div
22 + className="absolute inset-0 z-0 bg-cover bg-center"
23 + style={{
24 + backgroundImage: `url('/assets/images/summer-camp.jpg')`,
25 + filter: 'brightness(0.4)'
26 + }}
27 + ></div>
28 +
29 + {/* Gradient overlay */}
30 + <div className="absolute inset-0 z-1 bg-gradient-to-b from-red-500/70 to-red-600/90"></div>
31 +
32 + {/* Content */}
33 + <div className="relative z-10 p-4">
34 + <div className="bg-white/10 backdrop-blur-sm rounded-lg p-3 mb-3 inline-block">
35 + <div className="text-white font-semibold">{badge}</div>
36 + </div>
37 +
38 + <h1 className="text-2xl text-white font-bold mb-1">{title}</h1>
39 + <h2 className="text-lg text-white/90">{subtitle}</h2>
40 +
41 + <div className="mt-4 flex justify-between items-center">
42 + <div className="text-orange-300 font-bold text-2xl">{price}</div>
43 + <div className="bg-orange-500/30 text-orange-100 text-xs px-3 py-1 rounded-full">{discount}</div>
44 + </div>
45 +
46 + <div className="flex justify-between text-xs text-white/80 mt-3">
47 + <div>已更新{episodes}</div>
48 + <div>{subscribers}人订阅</div>
49 + </div>
50 + </div>
51 + </div>
52 + );
53 +};
54 +
55 +SummerCampCard.propTypes = {
56 + title: PropTypes.string,
57 + subtitle: PropTypes.string,
58 + badge: PropTypes.string,
59 + price: PropTypes.string,
60 + discount: PropTypes.string,
61 + episodes: PropTypes.number,
62 + subscribers: PropTypes.number
63 +};
64 +
65 +export default SummerCampCard;
...\ No newline at end of file ...\ No newline at end of file
1 +<template>
2 + <div class="relative overflow-hidden rounded-b-3xl shadow-lg">
3 + <!-- Background image with overlay -->
4 + <div
5 + class="absolute inset-0 z-0 bg-cover bg-center"
6 + :style="{
7 + backgroundImage: `url('https://cdn.ipadbiz.cn/mlaj/images/summer-camp.jpg')`,
8 + filter: 'brightness(0.4)'
9 + }"
10 + ></div>
11 +
12 + <!-- Gradient overlay -->
13 + <div class="absolute inset-0 z-1 bg-gradient-to-b from-red-500/70 to-red-600/90"></div>
14 +
15 + <!-- Content -->
16 + <div class="relative z-10 p-4">
17 + <div class="bg-white/10 backdrop-blur-sm rounded-lg p-3 mb-3 inline-block">
18 + <div class="text-white font-semibold">{{ badge }}</div>
19 + </div>
20 +
21 + <h1 class="text-2xl text-white font-bold mb-1">{{ title }}</h1>
22 + <h2 class="text-lg text-white/90">{{ subtitle }}</h2>
23 +
24 + <div class="mt-4 flex justify-between items-center">
25 + <div class="text-orange-300 font-bold text-2xl">{{ price }}</div>
26 + <div class="bg-orange-500/30 text-orange-100 text-xs px-3 py-1 rounded-full">{{ discount }}</div>
27 + </div>
28 +
29 + <div class="flex justify-between text-xs text-white/80 mt-3">
30 + <div>已更新{{ episodes }}期</div>
31 + <div>{{ subscribers }}人订阅</div>
32 + </div>
33 + </div>
34 + </div>
35 +</template>
36 +
37 +<script setup>
38 +import { defineProps } from 'vue'
39 +
40 +defineProps({
41 + title: {
42 + type: String,
43 + default: '大国少年-世界正东方'
44 + },
45 + subtitle: {
46 + type: String,
47 + default: '亲子夏令营'
48 + },
49 + badge: {
50 + type: String,
51 + default: '亲子夏令营'
52 + },
53 + price: {
54 + type: String,
55 + default: '¥1280'
56 + },
57 + discount: {
58 + type: String,
59 + default: '限时优惠'
60 + },
61 + episodes: {
62 + type: Number,
63 + default: 16
64 + },
65 + subscribers: {
66 + type: Number,
67 + default: 1140
68 + }
69 +})
70 +</script>
1 +<!-- src/layouts/AppLayout.vue -->
2 +<template>
3 + <div class="app-layout">
4 + <!-- Header -->
5 + <header class="app-header">
6 + <div v-if="showBack" class="header-back" @click="goBack">
7 + <van-icon name="arrow-left" size="20" />
8 + </div>
9 + <h1 class="header-title">{{ title }}</h1>
10 + <div class="header-right">
11 + <slot name="header-right"></slot>
12 + </div>
13 + </header>
14 +
15 + <!-- Main Content -->
16 + <main class="app-content" :class="{ 'has-bottom-nav': showBottomNav }">
17 + <slot></slot>
18 + </main>
19 +
20 + <!-- Bottom Navigation -->
21 + <van-tabbar v-if="showBottomNav" route safe-area-inset-bottom>
22 + <van-tabbar-item to="/home" icon="home-o">首页</van-tabbar-item>
23 + <van-tabbar-item to="/courses" icon="orders-o">课程</van-tabbar-item>
24 + <van-tabbar-item to="/activities" icon="friends-o">活动</van-tabbar-item>
25 + <van-tabbar-item to="/community" icon="chat-o">社区</van-tabbar-item>
26 + <van-tabbar-item to="/profile" icon="user-o">我的</van-tabbar-item>
27 + </van-tabbar>
28 + </div>
29 +</template>
30 +
31 +<script>
32 +import { ref, onMounted } from 'vue'
33 +import { useRouter, useRoute } from 'vue-router'
34 +
35 +export default {
36 + name: 'AppLayout',
37 + props: {
38 + title: {
39 + type: String,
40 + default: '亲子学院'
41 + },
42 + showBack: {
43 + type: Boolean,
44 + default: false
45 + },
46 + showBottomNav: {
47 + type: Boolean,
48 + default: true
49 + }
50 + },
51 + setup(props) {
52 + const router = useRouter()
53 + const route = useRoute()
54 +
55 + const goBack = () => {
56 + if (window.history.length > 1) {
57 + router.back()
58 + } else {
59 + router.push('/')
60 + }
61 + }
62 +
63 + return {
64 + goBack
65 + }
66 + }
67 +}
68 +</script>
69 +
70 +<style scoped>
71 +.app-layout {
72 + display: flex;
73 + flex-direction: column;
74 + min-height: 100vh;
75 + background-color: var(--background-color);
76 +}
77 +
78 +.app-header {
79 + position: sticky;
80 + top: 0;
81 + z-index: 100;
82 + display: flex;
83 + align-items: center;
84 + justify-content: center;
85 + height: 46px;
86 + padding: 0 16px;
87 + background-color: var(--white);
88 + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
89 +}
90 +
91 +.header-back {
92 + position: absolute;
93 + left: 16px;
94 + display: flex;
95 + align-items: center;
96 + justify-content: center;
97 +}
98 +
99 +.header-title {
100 + font-size: 18px;
101 + font-weight: 600;
102 + margin: 0;
103 +}
104 +
105 +.header-right {
106 + position: absolute;
107 + right: 16px;
108 + display: flex;
109 + align-items: center;
110 +}
111 +
112 +.app-content {
113 + flex: 1;
114 + overflow-y: auto;
115 + padding-bottom: 20px;
116 + -webkit-overflow-scrolling: touch;
117 +}
118 +
119 +.app-content.has-bottom-nav {
120 + padding-bottom: 50px;
121 +}
122 +</style>
...\ No newline at end of file ...\ No newline at end of file
1 +import { createApp } from 'vue'
2 +import './style.css'
3 +import App from './App.vue'
4 +import router from './router'
5 +
6 +// 导入Vant组件和样式
7 +import { Button, NavBar, Tabbar, TabbarItem } from 'vant'
8 +import 'vant/lib/index.css'
9 +
10 +const app = createApp(App)
11 +app.use(router)
12 +
13 +// 注册Vant组件
14 +app.use(Button)
15 +app.use(NavBar)
16 +app.use(Tabbar)
17 +app.use(TabbarItem)
18 +
19 +app.mount('#app')
1 +import { createRouter, createWebHistory } from 'vue-router'
2 +
3 +const routes = [
4 + {
5 + path: '/',
6 + name: 'home',
7 + component: () => import('../views/Home.vue')
8 + },
9 + {
10 + path: '/about',
11 + name: 'about',
12 + component: () => import('../views/About.vue')
13 + }
14 +]
15 +
16 +const router = createRouter({
17 + history: createWebHistory(),
18 + routes
19 +})
20 +
21 +export default router
This diff is collapsed. Click to expand it.
1 +/**
2 + * Mock data for the parent-child education app
3 + * This file contains sample data for courses, activities, and users
4 + */
5 +
6 +// Featured course for the top banner
7 +export const featuredCourse = {
8 + id: 'featured-1',
9 + title: '传承之道',
10 + subtitle: '大理鸡足山游学',
11 + imageUrl: 'https://cdn.ipadbiz.cn/mlaj/images/zMRLZh40kms.jpg',
12 + liveTime: '14:00:00'
13 +};
14 +
15 +// User recommendations
16 +export const userRecommendations = [
17 + { title: "亲子阅读技巧入门", duration: "15分钟", image: "https://cdn.ipadbiz.cn/mlaj/images/zMRLZh40kms.jpg" },
18 + { title: "3-6岁孩子的情绪管理", duration: "20分钟", image: "https://cdn.ipadbiz.cn/mlaj/images/27kCu7bXGEI.jpg" },
19 + { title: "趣味数学启蒙课", duration: "12分钟", image: "https://cdn.ipadbiz.cn/mlaj/images/jbwr0qZvpD4.jpg" },
20 + { title: "儿童英语绘本故事", duration: "18分钟", image: "https://cdn.ipadbiz.cn/mlaj/images/GGCP6vshpPY.jpg" }
21 +];
22 +
23 +// Live streaming sessions
24 +export const liveStreams = [
25 + {
26 + id: 'live-1',
27 + title: '无界之世',
28 + subtitle: '敦煌行',
29 + imageUrl: 'https://cdn.ipadbiz.cn/mlaj/images/2JIvboGLeho.jpg',
30 + isLive: true,
31 + viewers: 272
32 + },
33 + {
34 + id: 'live-2',
35 + title: '慧眼读书会',
36 + subtitle: '',
37 + imageUrl: 'https://cdn.ipadbiz.cn/mlaj/images/_6HzPU9Hyfg (2).jpg',
38 + isLive: true,
39 + viewers: 272
40 + }
41 +];
42 +
43 +// Course list data
44 +export const courses = [
45 + {
46 + id: 'course-1',
47 + title: '好分凭借力,陪你跃龙门!',
48 + subtitle: '4.17-6.18 美乐考前赋能营',
49 + imageUrl: 'https://cdn.ipadbiz.cn/mlaj/images/jbwr0qZvpD4.jpg', // Updated with new image
50 + price: 365,
51 + updatedLessons: 16,
52 + subscribers: 1140,
53 + expireDate: '2025-04-17 00:00:00',
54 + description: `【美乐考前赋能营】
55 + 结合平台多年亲子教育的实践经验,
56 + 针对近在眼前的高考,
57 + 和为学业考试而焦虑的群体
58 + 精心打造、最为定制的专项课程。
59 +
60 + 旨在帮助家长更好地理解,支持孩子,
61 + 有效管理自己的情绪和压力;
62 + 进而引导孩子的情绪状态,
63 + 以最佳的心理状态迎接挑战。
64 +
65 + 在考试的最后冲刺阶段,
66 + 给世界一个爱赏识他们的理由,
67 + 共同助力每个学子梦想成真!`,
68 + sections: [
69 + { title: '课程介绍', content: '课程详细描述内容...' },
70 + { title: '课程目录', content: '第1章:心态准备\n第2章:考前减压\n第3章:家庭支持' }
71 + ]
72 + },
73 + {
74 + id: 'course-2',
75 + title: '大国少年-梦想嘉年华',
76 + subtitle: '亲子冬令营',
77 + imageUrl: 'https://cdn.ipadbiz.cn/mlaj/images/GGCP6vshpPY.jpg', // Updated with new image
78 + price: 3665,
79 + updatedLessons: 16,
80 + subscribers: 1140
81 + },
82 + {
83 + id: 'course-3',
84 + title: '大国少年-世界正东方',
85 + subtitle: '亲子夏令营',
86 + imageUrl: 'https://cdn.ipadbiz.cn/mlaj/images/2Juj2cXWB7U.jpg', // Updated with new image
87 + price: 1280,
88 + updatedLessons: 16,
89 + subscribers: 1140
90 + }
91 +];
92 +
93 +// Activity data
94 +export const activities = [
95 + {
96 + id: 'activity-1',
97 + title: '慧眼读书 | 《论语》',
98 + subtitle: '亲子共读',
99 + imageUrl: 'https://cdn.ipadbiz.cn/mlaj/images/2JIvboGLeho.jpg', // Updated with new image
100 + location: '北京',
101 + status: '活动中',
102 + period: '2024.12.09-2025.01.17',
103 + price: 99,
104 + originalPrice: 120,
105 + isHot: true,
106 + isFree: false,
107 + participantsCount: 18,
108 + maxParticipants: 30
109 + },
110 + {
111 + id: 'activity-2',
112 + title: '好分凭借力,陪你跃龙门!',
113 + subtitle: '4.17-6.18 美乐考前赋能营',
114 + imageUrl: 'https://cdn.ipadbiz.cn/mlaj/images/_6HzPU9Hyfg (2).jpg', // Updated with new image
115 + location: '线上',
116 + status: '活动中',
117 + period: '2024.12.17-2025.01.26',
118 + price: 366,
119 + originalPrice: 468,
120 + isHot: false,
121 + isFree: false,
122 + participantsCount: 25,
123 + maxParticipants: 50
124 + },
125 + {
126 + id: 'activity-3',
127 + title: '7.29-8.4 敦煌: 【青云之路】',
128 + subtitle: '亲子游学营',
129 + imageUrl: 'https://cdn.ipadbiz.cn/mlaj/images/Y17FE9Fuw4Y.jpg', // Updated with new image
130 + location: '敦煌',
131 + status: '即将开始',
132 + period: '2025.01.07-2025.01.26',
133 + price: 3980,
134 + originalPrice: 4200,
135 + isHot: true,
136 + isFree: false,
137 + participantsCount: 12,
138 + maxParticipants: 20
139 + },
140 + {
141 + id: 'activity-4',
142 + title: '【大国少年·梦想嘉年华】',
143 + subtitle: '亲子冬令营',
144 + imageUrl: 'https://cdn.ipadbiz.cn/mlaj/images/-G3rw6Y02D0.jpg',
145 + location: '上海',
146 + status: '进行中',
147 + period: '2025.01.09-2025.01.22',
148 + price: 1280,
149 + originalPrice: 1380,
150 + isHot: false,
151 + isFree: false,
152 + participantsCount: 24,
153 + maxParticipants: 40
154 + },
155 + {
156 + id: 'activity-5',
157 + title: '慧眼读书会 |《零极限》',
158 + subtitle: '共读',
159 + imageUrl: 'https://cdn.ipadbiz.cn/mlaj/images/Oalh2MojUuk.jpg',
160 + location: '线上',
161 + status: '进行中',
162 + period: '2025.01.09-2025.01.22',
163 + price: 0,
164 + originalPrice: 0,
165 + isHot: false,
166 + isFree: true,
167 + participantsCount: 45,
168 + maxParticipants: 100
169 + }
170 +];
171 +
172 +// User profile data
173 +export const userProfile = {
174 + name: '李玉红',
175 + avatar: 'https://cdn.ipadbiz.cn/mlaj/images/user-avatar-2.jpg',
176 + activities: [],
177 + courses: [],
178 + children: [],
179 + checkIns: {
180 + totalDays: 45,
181 + currentStreak: 7,
182 + longestStreak: 15
183 + }
184 +};
185 +
186 +// Daily check-in data
187 +export const checkInTypes = [
188 + { id: 'reading', name: '阅读打卡', icon: 'book' },
189 + { id: 'exercise', name: '运动打卡', icon: 'running' },
190 + { id: 'study', name: '学习打卡', icon: 'graduation-cap' },
191 + { id: 'reflection', name: '反思打卡', icon: 'pencil-alt' }
192 +];
193 +
194 +// Community posts data
195 +export const communityPosts = [
196 + {
197 + id: 'post-1',
198 + author: {
199 + id: 'user-1',
200 + name: '王小明',
201 + avatar: 'https://cdn.ipadbiz.cn/mlaj/images/user-avatar-3.jpg'
202 + },
203 + content: '今天和孩子一起完成了《论语》的共读,收获颇丰!孩子对"己所不欲勿施于人"这句话有了更深的理解。',
204 + images: ['https://cdn.ipadbiz.cn/mlaj/images/post-1-1.jpg', 'https://cdn.ipadbiz.cn/mlaj/images/post-1-2.jpg'],
205 + likes: 24,
206 + comments: 5,
207 + createdAt: '2023-03-15T08:30:00Z'
208 + },
209 + {
210 + id: 'post-2',
211 + author: {
212 + id: 'user-2',
213 + name: '李晓华',
214 + avatar: 'https://cdn.ipadbiz.cn/mlaj/images/user-avatar-1.jpg'
215 + },
216 + content: '冬令营第三天,孩子们参观了科技馆,学习了很多有趣的知识,晚上的篝火晚会也很精彩!',
217 + images: ['https://cdn.ipadbiz.cn/mlaj/images/post-2-1.jpg'],
218 + likes: 36,
219 + comments: 8,
220 + createdAt: '2023-03-14T15:45:00Z'
221 + }
222 +];
1 +<script setup>
2 +import { Button, NavBar, Tabbar, TabbarItem } from 'vant';
3 +</script>
4 +
5 +<template>
6 + <div class="min-h-screen flex flex-col bg-gray-100">
7 + <van-nav-bar
8 + title="关于"
9 + left-text="返回"
10 + right-text="菜单"
11 + left-arrow
12 + @click-left="onClickLeft"
13 + @click-right="onClickRight"
14 + />
15 +
16 + <div class="flex-1 p-4">
17 + <div class="bg-white rounded-lg shadow p-4 mb-4">
18 + <h2 class="text-xl font-bold mb-2">关于我们</h2>
19 + <p class="text-gray-600 mb-4">这是一个示例项目的关于页面,展示了如何使用Vue3、Vite、Vant4和TailwindCSS构建现代化的Web应用。</p>
20 + <van-button type="primary" block>了解更多</van-button>
21 + </div>
22 +
23 + <div class="grid grid-cols-2 gap-4">
24 + <div class="bg-white rounded-lg shadow p-4">
25 + <h3 class="text-lg font-semibold mb-2">联系方式</h3>
26 + <ul class="text-gray-600">
27 + <li>邮箱:example@example.com</li>
28 + <li>电话:123-456-7890</li>
29 + <li>地址:示例地址</li>
30 + </ul>
31 + </div>
32 + <div class="bg-white rounded-lg shadow p-4">
33 + <h3 class="text-lg font-semibold mb-2">版本信息</h3>
34 + <ul class="text-gray-600">
35 + <li>当前版本:1.0.0</li>
36 + <li>更新日期:2024-03-20</li>
37 + <li>开源协议:MIT</li>
38 + </ul>
39 + </div>
40 + </div>
41 + </div>
42 +
43 + <van-tabbar v-model="active">
44 + <van-tabbar-item icon="home-o">首页</van-tabbar-item>
45 + <van-tabbar-item icon="search">搜索</van-tabbar-item>
46 + <van-tabbar-item icon="friends-o">好友</van-tabbar-item>
47 + <van-tabbar-item icon="setting-o">设置</van-tabbar-item>
48 + </van-tabbar>
49 + </div>
50 +</template>
51 +
52 +<script>
53 +export default {
54 + data() {
55 + return {
56 + active: 0
57 + }
58 + },
59 + methods: {
60 + onClickLeft() {
61 + this.$router.back()
62 + },
63 + onClickRight() {
64 + // 处理右侧按钮点击
65 + }
66 + }
67 +}
68 +</script>
This diff is collapsed. Click to expand it.
1 +/** @type {import('tailwindcss').Config} */
2 +export default {
3 + content: [
4 + "./index.html",
5 + "./src/**/*.{vue,js,ts,jsx,tsx}",
6 + ],
7 + theme: {
8 + extend: {},
9 + },
10 + plugins: [],
11 +}
1 +/*
2 + * @Date: 2025-03-20 19:53:12
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2025-03-20 20:27:29
5 + * @FilePath: /vue-vite/vite.config.js
6 + * @Description: 文件描述
7 + */
8 +import { defineConfig } from 'vite'
9 +import vue from '@vitejs/plugin-vue'
10 +import vueJsx from '@vitejs/plugin-vue-jsx'
11 +import path from "path";
12 +
13 +// https://vite.dev/config/
14 +export default defineConfig({
15 + plugins: [vue(), vueJsx()],
16 + resolve: {
17 + alias: { // 将会被传递到 @rollup/plugin-alias 作为 entries 的选项。也可以是一个对象,或一个 { find, replacement } 的数组. 当使用文件系统路径的别名时,请始终使用绝对路径。相对路径的别名值会被原封不动地使用,因此无法被正常解析。 更高级的自定义解析方法可以通过 插件 实现。
18 + "@": path.resolve(__dirname, "src"),
19 + "@components": path.resolve(__dirname, "src/components"),
20 + "@composables": path.resolve(__dirname, "src/composables"),
21 + "@utils": path.resolve(__dirname, "src/utils"),
22 + "@images": path.resolve(__dirname, "src/assets/images"),
23 + "@css": path.resolve(__dirname, "src/assets/css"),
24 + "@mock": path.resolve(__dirname, "src/assets/mock"),
25 + "common": path.resolve(__dirname, "src/common"),
26 + "@api": path.resolve(__dirname, "src/api"),
27 + },
28 + dedupe: ['vue'], // 如果你在你的应用程序中有相同依赖的副本(比如 monorepos),使用这个选项来强制 Vite 总是将列出的依赖关系解析到相同的副本(从项目根目录)。
29 + // conditions: [''], // 在解析包的 情景导出 时允许的附加条件。
30 + // mainFields: [''], // package.json 中,在解析包的入口点时尝试的字段列表。注意,这比从 exports 字段解析的情景导出优先级低:如果一个入口点从 exports 成功解析,主字段将被忽略。
31 + extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue'], // 导入时想要省略的扩展名列表。注意,不 建议忽略自定义导入类型的扩展名(例如:.vue),因为它会干扰 IDE 和类型支持。
32 + },
33 +})