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
.gitignore
0 → 100644
| 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 |
README copy.md
0 → 100644
| 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). |
index.html
0 → 100644
| 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> |
package-lock.json
0 → 100644
This diff could not be displayed because it is too large.
package.json
0 → 100644
| 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 | +} |
postcss.config.js
0 → 100644
public/vite.svg
0 → 100644
| 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 |
src/App.vue
0 → 100644
| 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> |
src/assets/images/-G3rw6Y02D0.jpg
0 → 100644
2.56 MB
src/assets/images/1-aA2Fadydc.jpg
0 → 100644
1.27 MB
src/assets/images/27kCu7bXGEI.jpg
0 → 100644
4.22 MB
src/assets/images/2JIvboGLeho.jpg
0 → 100644
2.25 MB
src/assets/images/2Juj2cXWB7U.jpg
0 → 100644
2.66 MB
src/assets/images/GGCP6vshpPY.jpg
0 → 100644
4.48 MB
src/assets/images/IcI3FizU9Cw.jpg
0 → 100644
1.77 MB
src/assets/images/Oalh2MojUuk.jpg
0 → 100644
751 KB
src/assets/images/Y17FE9Fuw4Y.jpg
0 → 100644
2.44 MB
src/assets/images/_6HzPU9Hyfg (1).jpg
0 → 100644
210 KB
src/assets/images/_6HzPU9Hyfg (2).jpg
0 → 100644
210 KB
src/assets/images/_6HzPU9Hyfg.jpg
0 → 100644
210 KB
src/assets/images/activity-1.jpg
0 → 100644
2.25 MB
src/assets/images/activity-2.jpg
0 → 100644
210 KB
src/assets/images/activity-3.jpg
0 → 100644
2.44 MB
src/assets/images/avatar1.jpg
0 → 100644
File mode changed
src/assets/images/avatar2.jpg
0 → 100644
File mode changed
src/assets/images/avatar3.jpg
0 → 100644
File mode changed
src/assets/images/avatar4.jpg
0 → 100644
File mode changed
src/assets/images/avatar5.jpg
0 → 100644
File mode changed
src/assets/images/avatar6.jpg
0 → 100644
File mode changed
src/assets/images/avatar7.jpg
0 → 100644
File mode changed
src/assets/images/avatar8.jpg
0 → 100644
File mode changed
src/assets/images/banner-1.jpg
0 → 100644
File mode changed
src/assets/images/banner-2.jpg
0 → 100644
File mode changed
src/assets/images/banner-3.jpg
0 → 100644
File mode changed
src/assets/images/course-1.jpg
0 → 100644
3.81 MB
src/assets/images/course-2.jpg
0 → 100644
4.48 MB
src/assets/images/course-3.jpg
0 → 100644
2.66 MB
src/assets/images/error-placeholder.jpg
0 → 100644
File mode changed
src/assets/images/featured-course-1.jpg
0 → 100644
File mode changed
src/assets/images/featured-course-2.jpg
0 → 100644
4.22 MB
src/assets/images/featured-course.jpg
0 → 100644
2.26 MB
src/assets/images/jbwr0qZvpD4.jpg
0 → 100644
3.81 MB
src/assets/images/loading-placeholder.jpg
0 → 100644
File mode changed
src/assets/images/phIFdC6lA4E.jpg
0 → 100644
2.76 MB
src/assets/images/placeholder.jpg
0 → 100644
File mode changed
src/assets/images/sfL_QOnmy00.jpg
0 → 100644
3.21 MB
src/assets/images/summer-camp.jpg
0 → 100644
2.56 MB
src/assets/images/teacher-1.jpg
0 → 100644
File mode changed
src/assets/images/user-avatar-1.jpg
0 → 100644
1.44 MB
src/assets/images/user-avatar-2.jpg
0 → 100644
3.4 MB
src/assets/images/user-avatar-3.jpg
0 → 100644
553 KB
src/assets/images/zMRLZh40kms.jpg
0 → 100644
2.26 MB
src/assets/vue.svg
0 → 100644
| 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 |
src/components/HelloWorld.vue
0 → 100644
| 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> |
src/components/layout/AppLayout.jsx
0 → 100644
| 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 |
src/components/layout/AppLayout.vue
0 → 100644
| 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> |
src/components/layout/BottomNav.jsx
0 → 100644
| 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 |
src/components/layout/BottomNav.vue
0 → 100644
| 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> |
src/components/ui/ActivityCard.jsx
0 → 100644
| 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 |
src/components/ui/ActivityCard.vue
0 → 100644
| 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> |
src/components/ui/CourseCard.jsx
0 → 100644
| 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 |
src/components/ui/CourseCard.vue
0 → 100644
| 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> |
src/components/ui/FrostedGlass.jsx
0 → 100644
| 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 |
src/components/ui/FrostedGlass.vue
0 → 100644
| 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> |
src/components/ui/GradientHeader.jsx
0 → 100644
| 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 |
src/components/ui/GradientHeader.vue
0 → 100644
| 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> |
src/components/ui/LiveStreamCard.jsx
0 → 100644
| 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 |
src/components/ui/LiveStreamCard.vue
0 → 100644
| 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> |
src/components/ui/SearchBar.jsx
0 → 100644
| 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 |
src/components/ui/SearchBar.vue
0 → 100644
| 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> |
src/components/ui/SummerCampCard.jsx
0 → 100644
| 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 |
src/components/ui/SummerCampCard.vue
0 → 100644
| 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> |
src/layouts/AppLayout.vue
0 → 100644
| 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 |
src/main.js
0 → 100644
| 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') |
src/router/index.js
0 → 100644
| 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 |
src/style.css
0 → 100644
This diff is collapsed. Click to expand it.
src/utils/mockData.js
0 → 100644
| 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 | +]; |
src/views/About.vue
0 → 100644
| 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> |
src/views/Home.vue
0 → 100644
This diff is collapsed. Click to expand it.
tailwind.config.js
0 → 100644
vite.config.js
0 → 100644
| 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 | +}) |
-
Please register or login to post a comment