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
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.history
node_modules
.vscode
# Vue 3 + Vite
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.
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Vue</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
This diff could not be displayed because it is too large.
{
"name": "vue-vite",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@vant/use": "^1.6.0",
"vant": "^4.9.18",
"vue": "^3.5.13",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.1",
"@vitejs/plugin-vue-jsx": "^4.1.2",
"autoprefixer": "^10.4.19",
"postcss": "^8.4.35",
"tailwindcss": "^3.4.1",
"vite": "^6.2.0"
}
}
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
<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
<!--
* @Date: 2025-03-20 19:53:12
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-03-20 20:06:10
* @FilePath: /vue-vite/src/App.vue
* @Description: 文件描述
-->
<script setup>
import { RouterView } from "vue-router";
</script>
<template>
<div>
<router-view>
<template v-slot="{ Component }">
<Suspense>
<template #default>
<div>
<component :is="Component" />
</div>
</template>
<template #fallback>
<div
class="flex items-center justify-center h-screen bg-gradient-to-br from-green-50 via-teal-50 to-blue-50"
>
<div class="bg-white/20 backdrop-blur-md rounded-xl p-6 shadow-lg">
<div
class="animate-spin rounded-full h-12 w-12 border-b-2 border-green-500 mx-auto"
></div>
<p class="mt-4 text-gray-700">加载中...</p>
</div>
</div>
</template>
</Suspense>
</template>
</router-view>
</div>
</template>
<style>
#app {
width: 100%;
min-height: 100vh;
}
</style>
<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
<script setup>
import { ref } from 'vue'
defineProps({
msg: String,
})
const count = ref(0)
</script>
<template>
<h1>{{ msg }}</h1>
<div class="card">
<button type="button" @click="count++">count is {{ count }}</button>
<p>
Edit
<code>components/HelloWorld.vue</code> to test HMR
</p>
</div>
<p>
Check out
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
>create-vue</a
>, the official Vue + Vite starter
</p>
<p>
Learn more about IDE Support for Vue in the
<a
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
target="_blank"
>Vue Docs Scaling up Guide</a
>.
</p>
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
</template>
<style scoped>
.read-the-docs {
color: #888;
}
</style>
import React from 'react';
import BottomNav from './BottomNav';
import GradientHeader from '../ui/GradientHeader';
/**
* AppLayout component provides consistent layout across the app
*
* @param {Object} props - Component props
* @param {ReactNode} props.children - Child elements
* @param {string} props.title - Page title
* @param {boolean} props.showBackButton - Whether to display back button
* @param {Function} props.onBack - Back button click handler
* @param {ReactNode} props.rightContent - Content to display on the right side of header
* @returns {JSX.Element} AppLayout component
*/
const AppLayout = ({ children, title, showBackButton, onBack, rightContent }) => {
const handleBack = () => {
if (onBack) {
onBack();
} else {
window.history.back();
}
};
return (
<div className="bg-gradient-to-br from-green-50 via-teal-50 to-blue-50 min-h-screen pb-16">
<GradientHeader
title={title}
showBackButton={showBackButton}
onBack={handleBack}
rightContent={rightContent}
/>
<main className="pb-16">
{children}
</main>
<BottomNav />
</div>
);
};
export default AppLayout;
\ No newline at end of file
<template>
<div class="bg-gradient-to-br from-green-50 via-teal-50 to-blue-50 min-h-screen pb-16">
<GradientHeader
:title="title"
:showBackButton="showBackButton"
:onBack="handleBack"
:rightContent="rightContent"
/>
<main class="pb-16">
<slot></slot>
</main>
<BottomNav />
</div>
</template>
<script setup>
import { defineProps } from 'vue'
import BottomNav from './BottomNav.vue'
import GradientHeader from '../ui/GradientHeader.vue'
const props = defineProps({
title: {
type: String,
required: true
},
showBackButton: {
type: Boolean,
default: false
},
onBack: {
type: Function,
default: null
},
rightContent: {
type: Object,
default: null
}
})
const handleBack = () => {
if (props.onBack) {
props.onBack()
} else {
window.history.back()
}
}
</script>
import React from 'react';
import { Link, useLocation } from 'react-router-dom';
/**
* BottomNav component for app navigation
*
* @returns {JSX.Element} BottomNav component
*/
const BottomNav = () => {
const location = useLocation();
const navItems = [
{
name: '首页',
path: '/',
icon: (
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 mb-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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" />
</svg>
)
},
{
name: '课程',
path: '/courses',
icon: (
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 mb-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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" />
</svg>
)
},
{
name: '空间',
path: '/community',
icon: (
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 mb-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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" />
</svg>
)
},
{
name: '我的',
path: '/profile',
icon: (
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 mb-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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" />
</svg>
)
}
];
return (
<nav className="fixed bottom-0 left-0 right-0 bg-white/70 backdrop-blur-lg border-t border-gray-100 z-50">
<div className="flex justify-around items-center h-16">
{navItems.map((item) => {
const isActive = location.pathname === item.path ||
(item.path !== '/' && location.pathname.startsWith(item.path));
return (
<Link
key={item.name}
to={item.path}
className={`flex flex-col items-center justify-center w-1/4 h-full ${
isActive ? 'text-green-500' : 'text-gray-500'
}`}
>
{React.cloneElement(item.icon, {
className: `${item.icon.props.className} ${isActive ? 'text-green-500' : 'text-gray-500'}`
})}
<span className="text-xs">{item.name}</span>
</Link>
);
})}
</div>
</nav>
);
};
export default BottomNav;
\ No newline at end of file
<template>
<nav class="fixed bottom-0 left-0 right-0 bg-white/70 backdrop-blur-lg border-t border-gray-100 z-50">
<div class="flex justify-around items-center h-16">
<router-link
v-for="item in navItems"
:key="item.name"
:to="item.path"
class="flex flex-col items-center justify-center w-1/4 h-full"
:class="{
'text-green-500': isActive(item.path),
'text-gray-500': !isActive(item.path)
}"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 mb-1"
:class="{
'text-green-500': isActive(item.path),
'text-gray-500': !isActive(item.path)
}"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
v-html="item.icon"
/>
<span class="text-xs">{{ item.name }}</span>
</router-link>
</div>
</nav>
</template>
<script setup>
import { computed } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const navItems = [
{
name: '首页',
path: '/',
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" />'
},
{
name: '课程',
path: '/courses',
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" />'
},
{
name: '空间',
path: '/community',
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" />'
},
{
name: '我的',
path: '/profile',
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" />'
}
]
const isActive = (path) => {
return route.path === path || (path !== '/' && route.path.startsWith(path))
}
</script>
import React from 'react';
import { Link } from 'react-router-dom';
import FrostedGlass from './FrostedGlass';
/**
* ActivityCard component displays an activity item in the activities list
*
* @param {Object} props - Component props
* @param {Object} props.activity - Activity data
* @returns {JSX.Element} ActivityCard component
*/
const ActivityCard = ({ activity }) => {
// Function to get the appropriate status class
const getStatusClass = (status) => {
switch (status) {
case '活动中':
return 'bg-blue-100 text-blue-600';
case '进行中':
return 'bg-green-100 text-green-600';
case '即将开始':
return 'bg-orange-100 text-orange-600';
case '已结束':
return 'bg-gray-100 text-gray-600';
default:
return 'bg-gray-100 text-gray-600';
}
};
return (
<Link to={`/activities/${activity.id}`}>
<FrostedGlass className="flex overflow-hidden rounded-xl shadow-sm">
{/* Activity Image */}
<div className="w-1/3 h-28 relative">
<img
src={activity.imageUrl}
alt={activity.title}
className="w-full h-full object-cover"
/>
{activity.isHot && (
<div className="absolute top-0 left-0 bg-red-500 text-white text-xs px-2 py-0.5">
热门
</div>
)}
</div>
{/* Activity Info */}
<div className="flex-1 p-3 flex flex-col justify-between">
<div>
<h3 className="font-medium text-base mb-1 line-clamp-1">{activity.title}</h3>
{/* Status Tags */}
<div className="flex items-center space-x-2 mb-1">
<span className={`px-2 py-0.5 rounded-full text-xs ${getStatusClass(activity.status)}`}>
{activity.status}
</span>
{activity.isFree && (
<span className="px-2 py-0.5 rounded-full text-xs bg-green-100 text-green-600">
免费
</span>
)}
</div>
</div>
{/* Location and Time */}
<div className="text-xs text-gray-500 space-y-1">
<div className="flex items-center">
<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">
<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" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<span>{activity.location}</span>
</div>
<div className="flex items-center">
<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">
<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" />
</svg>
<span>{activity.period}</span>
</div>
</div>
{/* Bottom Info Section */}
<div className="mt-1 flex items-center justify-between">
{activity.price ? (
<div className="flex items-baseline">
<span className="text-red-500 font-medium">¥{activity.price}</span>
{activity.originalPrice && (
<span className="text-xs text-gray-400 ml-1 line-through">¥{activity.originalPrice}</span>
)}
</div>
) : (
<div></div> // Empty div for spacing when no price
)}
<div className="flex items-center text-xs text-gray-500">
<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">
<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" />
</svg>
<span>{activity.participantsCount || '15'}/{activity.maxParticipants || '30'}</span>
</div>
</div>
</div>
</FrostedGlass>
</Link>
);
};
export default ActivityCard;
\ No newline at end of file
<template>
<router-link :to="`/activities/${activity.id}`">
<FrostedGlass class="flex overflow-hidden rounded-xl shadow-sm">
<!-- Activity Image -->
<div class="w-1/3 h-28 relative">
<img
:src="activity.imageUrl"
:alt="activity.title"
class="w-full h-full object-cover"
/>
<div v-if="activity.isHot" class="absolute top-0 left-0 bg-red-500 text-white text-xs px-2 py-0.5">
热门
</div>
</div>
<!-- Activity Info -->
<div class="flex-1 p-3 flex flex-col justify-between">
<div>
<h3 class="font-medium text-base mb-1 line-clamp-1">{{ activity.title }}</h3>
<!-- Status Tags -->
<div class="flex items-center space-x-2 mb-1">
<span :class="['px-2 py-0.5 rounded-full text-xs', getStatusClass(activity.status)]">
{{ activity.status }}
</span>
<span v-if="activity.isFree" class="px-2 py-0.5 rounded-full text-xs bg-green-100 text-green-600">
免费
</span>
</div>
</div>
<!-- Location and Time -->
<div class="text-xs text-gray-500 space-y-1">
<div class="flex items-center">
<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">
<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" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<span>{{ activity.location }}</span>
</div>
<div class="flex items-center">
<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">
<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" />
</svg>
<span>{{ activity.period }}</span>
</div>
</div>
<!-- Bottom Info Section -->
<div class="mt-1 flex items-center justify-between">
<div v-if="activity.price" class="flex items-baseline">
<span class="text-red-500 font-medium">¥{{ activity.price }}</span>
<span v-if="activity.originalPrice" class="text-xs text-gray-400 ml-1 line-through">¥{{ activity.originalPrice }}</span>
</div>
<div v-else></div>
<div class="flex items-center text-xs text-gray-500">
<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">
<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" />
</svg>
<span>{{ activity.participantsCount || '15' }}/{{ activity.maxParticipants || '30' }}</span>
</div>
</div>
</div>
</FrostedGlass>
</router-link>
</template>
<script setup>
import { defineProps } from 'vue'
import FrostedGlass from './FrostedGlass.vue'
defineProps({
activity: {
type: Object,
required: true
}
})
const getStatusClass = (status) => {
switch (status) {
case '活动中':
return 'bg-blue-100 text-blue-600'
case '进行中':
return 'bg-green-100 text-green-600'
case '即将开始':
return 'bg-orange-100 text-orange-600'
case '已结束':
return 'bg-gray-100 text-gray-600'
default:
return 'bg-gray-100 text-gray-600'
}
}
</script>
import React from 'react';
import { Link } from 'react-router-dom';
/**
* CourseCard component displays a course item in the course list
*
* @param {Object} props - Component props
* @param {Object} props.course - Course data
* @returns {JSX.Element} CourseCard component
*/
const CourseCard = ({ course }) => {
return (
<Link to={`/courses/${course.id}`} className="flex bg-white rounded-lg overflow-hidden shadow-sm">
<div className="w-1/3 h-28">
<img
src={course.imageUrl}
alt={course.title}
className="w-full h-full object-cover"
/>
</div>
<div className="flex-1 p-3 flex flex-col justify-between">
<div>
<h3 className="font-medium text-sm mb-1 line-clamp-2">{course.title}</h3>
<div className="text-gray-500 text-xs">{course.subtitle}</div>
</div>
<div className="flex justify-between items-end mt-1">
<div className="text-orange-500 font-semibold">¥{course.price}</div>
<div className="text-gray-400 text-xs">
{course.subscribers}人订阅
</div>
</div>
<div className="text-gray-400 text-xs">
已更新{course.updatedLessons}期 | {course.subscribers}人订阅
</div>
</div>
</Link>
);
};
export default CourseCard;
\ No newline at end of file
<template>
<router-link :to="`/courses/${course.id}`" class="flex bg-white rounded-lg overflow-hidden shadow-sm">
<div class="w-1/3 h-28">
<img
:src="course.imageUrl"
:alt="course.title"
class="w-full h-full object-cover"
/>
</div>
<div class="flex-1 p-3 flex flex-col justify-between">
<div>
<h3 class="font-medium text-sm mb-1 line-clamp-2">{{ course.title }}</h3>
<div class="text-gray-500 text-xs">{{ course.subtitle }}</div>
</div>
<div class="flex justify-between items-end mt-1">
<div class="text-orange-500 font-semibold">¥{{ course.price }}</div>
<div class="text-gray-400 text-xs">
{{ course.subscribers }}人订阅
</div>
</div>
<div class="text-gray-400 text-xs">
已更新{{ course.updatedLessons }}期 | {{ course.subscribers }}人订阅
</div>
</div>
</router-link>
</template>
<script setup>
import { defineProps } from 'vue'
defineProps({
course: {
type: Object,
required: true
}
})
</script>
import React from 'react';
/**
* FrostedGlass component creates a container with a frosted glass effect
* using backdrop-filter blur and a semi-transparent white background.
*
* @param {Object} props - Component props
* @param {ReactNode} props.children - Child elements
* @param {string} props.className - Additional CSS classes
* @returns {JSX.Element} FrostedGlass component
*/
const FrostedGlass = ({ children, className = '' }) => {
return (
<div
className={`bg-white/20 backdrop-blur-md rounded-xl border border-white/30
shadow-lg ${className}`}
>
{children}
</div>
);
};
export default FrostedGlass;
\ No newline at end of file
<template>
<div
:class="[
'bg-white/20 backdrop-blur-md rounded-xl border border-white/30 shadow-lg',
className
]"
>
<slot></slot>
</div>
</template>
<script setup>
import { defineProps } from 'vue'
defineProps({
className: {
type: String,
default: ''
}
})
</script>
import React from 'react';
/**
* GradientHeader component for page headers with gradient background
* and navigation elements.
*
* @param {Object} props - Component props
* @param {string} props.title - Header title
* @param {boolean} props.showBackButton - Whether to show back button
* @param {Function} props.onBack - Back button click handler
* @param {ReactNode} props.rightContent - Content to display on the right side
* @returns {JSX.Element} GradientHeader component
*/
const GradientHeader = ({ title, showBackButton = false, onBack, rightContent }) => {
return (
<header className="bg-gradient-to-r from-green-50 to-blue-50 p-4 relative">
<div className="flex items-center justify-between">
{showBackButton && (
<button
onClick={onBack}
className="p-2 rounded-full bg-white/30 backdrop-blur-sm"
aria-label="返回"
>
<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">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
</button>
)}
<h1 className={`text-xl font-medium text-center ${showBackButton ? 'flex-1' : ''}`}>
{title}
</h1>
{rightContent && <div>{rightContent}</div>}
</div>
</header>
);
};
export default GradientHeader;
\ No newline at end of file
<template>
<header class="bg-gradient-to-r from-green-50 to-blue-50 p-4 relative">
<div class="flex items-center justify-between">
<button
v-if="showBackButton"
@click="onBack"
class="p-2 rounded-full bg-white/30 backdrop-blur-sm"
aria-label="返回"
>
<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">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
</button>
<h1 :class="['text-xl font-medium text-center', showBackButton ? 'flex-1' : '']">
{{ title }}
</h1>
<div v-if="rightContent" v-html="rightContent.content"></div>
</div>
</header>
</template>
<script setup>
import { defineProps } from 'vue'
defineProps({
title: {
type: String,
required: true
},
showBackButton: {
type: Boolean,
default: false
},
onBack: {
type: Function,
default: () => {}
},
rightContent: {
type: Object,
default: null
}
})
</script>
import React from 'react';
import { Link } from 'react-router-dom';
/**
* LiveStreamCard component displays a live stream in the courses page
*
* @param {Object} props - Component props
* @param {Object} props.stream - Stream data
* @returns {JSX.Element} LiveStreamCard component
*/
const LiveStreamCard = ({ stream }) => {
return (
<div className="relative">
{/* Live indicator */}
<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">
<div className="w-2 h-2 bg-white rounded-full mr-1 animate-pulse"></div>
直播中
</div>
<Link to={`/courses/${stream.id}`} className="block rounded-lg overflow-hidden shadow-sm relative">
<img
src={stream.imageUrl}
alt={stream.title}
className="w-full h-28 object-cover"
/>
{/* Gradient overlay */}
<div className="absolute inset-0 bg-gradient-to-b from-transparent to-black/60"></div>
{/* Stream info */}
<div className="absolute bottom-2 left-2 right-2">
<h3 className="text-white text-sm font-medium">{stream.title}{stream.subtitle}</h3>
<div className="flex items-center mt-1">
<div className="flex -space-x-2">
<div className="w-5 h-5 rounded-full bg-gray-300 border border-white"></div>
<div className="w-5 h-5 rounded-full bg-gray-400 border border-white"></div>
<div className="w-5 h-5 rounded-full bg-gray-500 border border-white"></div>
</div>
<span className="text-white text-xs ml-1">{stream.viewers}人在看</span>
<button className="ml-auto bg-green-500 text-white text-xs px-2 py-1 rounded">
立即观看
</button>
</div>
</div>
</Link>
</div>
);
};
export default LiveStreamCard;
\ No newline at end of file
<!--
* @Date: 2025-03-20 15:33:07
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-03-20 15:33:08
* @FilePath: /react_template_1/src/components/ui/LiveStreamCard.vue
* @Description: 文件描述
-->
<template>
<div class="relative">
<!-- Live indicator -->
<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">
<div class="w-2 h-2 bg-white rounded-full mr-1 animate-pulse"></div>
直播中
</div>
<router-link :to="`/courses/${stream.id}`" class="block rounded-lg overflow-hidden shadow-sm relative">
<img
:src="stream.imageUrl"
:alt="stream.title"
class="w-full h-28 object-cover"
/>
<!-- Gradient overlay -->
<div class="absolute inset-0 bg-gradient-to-b from-transparent to-black/60"></div>
<!-- Stream info -->
<div class="absolute bottom-2 left-2 right-2">
<h3 class="text-white text-sm font-medium">「{{ stream.title }}」{{ stream.subtitle }}</h3>
<div class="flex items-center mt-1">
<div class="flex -space-x-2">
<div class="w-5 h-5 rounded-full bg-gray-300 border border-white"></div>
<div class="w-5 h-5 rounded-full bg-gray-400 border border-white"></div>
<div class="w-5 h-5 rounded-full bg-gray-500 border border-white"></div>
</div>
<span class="text-white text-xs ml-1">{{ stream.viewers }}人在看</span>
<button class="ml-auto bg-green-500 text-white text-xs px-2 py-1 rounded">
立即观看
</button>
</div>
</div>
</router-link>
</div>
</template>
<script setup>
import { defineProps } from 'vue'
defineProps({
stream: {
type: Object,
required: true
}
})
</script>
import React from 'react';
import FrostedGlass from './FrostedGlass';
/**
* SearchBar component with frosted glass effect
*
* @param {Object} props - Component props
* @param {string} props.placeholder - Placeholder text
* @param {Function} props.onSearch - Search callback function
* @returns {JSX.Element} SearchBar component
*/
const SearchBar = ({ placeholder = '搜索', onSearch }) => {
return (
<FrostedGlass className="px-4 py-2 mx-4 my-3 flex items-center">
<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">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
<input
type="text"
placeholder={placeholder}
className="bg-transparent outline-none flex-1 text-gray-700 placeholder-gray-400"
onChange={(e) => onSearch && onSearch(e.target.value)}
/>
</FrostedGlass>
);
};
export default SearchBar;
\ No newline at end of file
<template>
<FrostedGlass class="px-4 py-2 mx-4 my-3 flex items-center">
<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">
<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" />
</svg>
<input
type="text"
:placeholder="placeholder"
class="bg-transparent outline-none flex-1 text-gray-700 placeholder-gray-400"
@input="handleSearch"
/>
</FrostedGlass>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue'
import FrostedGlass from './FrostedGlass.vue'
const props = defineProps({
placeholder: {
type: String,
default: '搜索'
}
})
const emit = defineEmits(['search'])
const handleSearch = (e) => {
emit('search', e.target.value)
}
</script>
import React from 'react';
import PropTypes from 'prop-types';
/**
* SummerCampCard component - displays summer camp information with image background
* @param {Object} props - Component props
* @returns {JSX.Element} - Rendered component
*/
const SummerCampCard = ({
title = "大国少年-世界正东方",
subtitle = "亲子夏令营",
badge = "亲子夏令营",
price = "¥1280",
discount = "限时优惠",
episodes = 16,
subscribers = 1140
}) => {
return (
<div className="relative overflow-hidden rounded-b-3xl shadow-lg">
{/* Background image with overlay */}
<div
className="absolute inset-0 z-0 bg-cover bg-center"
style={{
backgroundImage: `url('/assets/images/summer-camp.jpg')`,
filter: 'brightness(0.4)'
}}
></div>
{/* Gradient overlay */}
<div className="absolute inset-0 z-1 bg-gradient-to-b from-red-500/70 to-red-600/90"></div>
{/* Content */}
<div className="relative z-10 p-4">
<div className="bg-white/10 backdrop-blur-sm rounded-lg p-3 mb-3 inline-block">
<div className="text-white font-semibold">{badge}</div>
</div>
<h1 className="text-2xl text-white font-bold mb-1">{title}</h1>
<h2 className="text-lg text-white/90">{subtitle}</h2>
<div className="mt-4 flex justify-between items-center">
<div className="text-orange-300 font-bold text-2xl">{price}</div>
<div className="bg-orange-500/30 text-orange-100 text-xs px-3 py-1 rounded-full">{discount}</div>
</div>
<div className="flex justify-between text-xs text-white/80 mt-3">
<div>已更新{episodes}</div>
<div>{subscribers}人订阅</div>
</div>
</div>
</div>
);
};
SummerCampCard.propTypes = {
title: PropTypes.string,
subtitle: PropTypes.string,
badge: PropTypes.string,
price: PropTypes.string,
discount: PropTypes.string,
episodes: PropTypes.number,
subscribers: PropTypes.number
};
export default SummerCampCard;
\ No newline at end of file
<template>
<div class="relative overflow-hidden rounded-b-3xl shadow-lg">
<!-- Background image with overlay -->
<div
class="absolute inset-0 z-0 bg-cover bg-center"
:style="{
backgroundImage: `url('https://cdn.ipadbiz.cn/mlaj/images/summer-camp.jpg')`,
filter: 'brightness(0.4)'
}"
></div>
<!-- Gradient overlay -->
<div class="absolute inset-0 z-1 bg-gradient-to-b from-red-500/70 to-red-600/90"></div>
<!-- Content -->
<div class="relative z-10 p-4">
<div class="bg-white/10 backdrop-blur-sm rounded-lg p-3 mb-3 inline-block">
<div class="text-white font-semibold">{{ badge }}</div>
</div>
<h1 class="text-2xl text-white font-bold mb-1">{{ title }}</h1>
<h2 class="text-lg text-white/90">{{ subtitle }}</h2>
<div class="mt-4 flex justify-between items-center">
<div class="text-orange-300 font-bold text-2xl">{{ price }}</div>
<div class="bg-orange-500/30 text-orange-100 text-xs px-3 py-1 rounded-full">{{ discount }}</div>
</div>
<div class="flex justify-between text-xs text-white/80 mt-3">
<div>已更新{{ episodes }}期</div>
<div>{{ subscribers }}人订阅</div>
</div>
</div>
</div>
</template>
<script setup>
import { defineProps } from 'vue'
defineProps({
title: {
type: String,
default: '大国少年-世界正东方'
},
subtitle: {
type: String,
default: '亲子夏令营'
},
badge: {
type: String,
default: '亲子夏令营'
},
price: {
type: String,
default: '¥1280'
},
discount: {
type: String,
default: '限时优惠'
},
episodes: {
type: Number,
default: 16
},
subscribers: {
type: Number,
default: 1140
}
})
</script>
<!-- src/layouts/AppLayout.vue -->
<template>
<div class="app-layout">
<!-- Header -->
<header class="app-header">
<div v-if="showBack" class="header-back" @click="goBack">
<van-icon name="arrow-left" size="20" />
</div>
<h1 class="header-title">{{ title }}</h1>
<div class="header-right">
<slot name="header-right"></slot>
</div>
</header>
<!-- Main Content -->
<main class="app-content" :class="{ 'has-bottom-nav': showBottomNav }">
<slot></slot>
</main>
<!-- Bottom Navigation -->
<van-tabbar v-if="showBottomNav" route safe-area-inset-bottom>
<van-tabbar-item to="/home" icon="home-o">首页</van-tabbar-item>
<van-tabbar-item to="/courses" icon="orders-o">课程</van-tabbar-item>
<van-tabbar-item to="/activities" icon="friends-o">活动</van-tabbar-item>
<van-tabbar-item to="/community" icon="chat-o">社区</van-tabbar-item>
<van-tabbar-item to="/profile" icon="user-o">我的</van-tabbar-item>
</van-tabbar>
</div>
</template>
<script>
import { ref, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
export default {
name: 'AppLayout',
props: {
title: {
type: String,
default: '亲子学院'
},
showBack: {
type: Boolean,
default: false
},
showBottomNav: {
type: Boolean,
default: true
}
},
setup(props) {
const router = useRouter()
const route = useRoute()
const goBack = () => {
if (window.history.length > 1) {
router.back()
} else {
router.push('/')
}
}
return {
goBack
}
}
}
</script>
<style scoped>
.app-layout {
display: flex;
flex-direction: column;
min-height: 100vh;
background-color: var(--background-color);
}
.app-header {
position: sticky;
top: 0;
z-index: 100;
display: flex;
align-items: center;
justify-content: center;
height: 46px;
padding: 0 16px;
background-color: var(--white);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
}
.header-back {
position: absolute;
left: 16px;
display: flex;
align-items: center;
justify-content: center;
}
.header-title {
font-size: 18px;
font-weight: 600;
margin: 0;
}
.header-right {
position: absolute;
right: 16px;
display: flex;
align-items: center;
}
.app-content {
flex: 1;
overflow-y: auto;
padding-bottom: 20px;
-webkit-overflow-scrolling: touch;
}
.app-content.has-bottom-nav {
padding-bottom: 50px;
}
</style>
\ No newline at end of file
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import router from './router'
// 导入Vant组件和样式
import { Button, NavBar, Tabbar, TabbarItem } from 'vant'
import 'vant/lib/index.css'
const app = createApp(App)
app.use(router)
// 注册Vant组件
app.use(Button)
app.use(NavBar)
app.use(Tabbar)
app.use(TabbarItem)
app.mount('#app')
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
path: '/',
name: 'home',
component: () => import('../views/Home.vue')
},
{
path: '/about',
name: 'about',
component: () => import('../views/About.vue')
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
This diff is collapsed. Click to expand it.
/**
* Mock data for the parent-child education app
* This file contains sample data for courses, activities, and users
*/
// Featured course for the top banner
export const featuredCourse = {
id: 'featured-1',
title: '传承之道',
subtitle: '大理鸡足山游学',
imageUrl: 'https://cdn.ipadbiz.cn/mlaj/images/zMRLZh40kms.jpg',
liveTime: '14:00:00'
};
// User recommendations
export const userRecommendations = [
{ title: "亲子阅读技巧入门", duration: "15分钟", image: "https://cdn.ipadbiz.cn/mlaj/images/zMRLZh40kms.jpg" },
{ title: "3-6岁孩子的情绪管理", duration: "20分钟", image: "https://cdn.ipadbiz.cn/mlaj/images/27kCu7bXGEI.jpg" },
{ title: "趣味数学启蒙课", duration: "12分钟", image: "https://cdn.ipadbiz.cn/mlaj/images/jbwr0qZvpD4.jpg" },
{ title: "儿童英语绘本故事", duration: "18分钟", image: "https://cdn.ipadbiz.cn/mlaj/images/GGCP6vshpPY.jpg" }
];
// Live streaming sessions
export const liveStreams = [
{
id: 'live-1',
title: '无界之世',
subtitle: '敦煌行',
imageUrl: 'https://cdn.ipadbiz.cn/mlaj/images/2JIvboGLeho.jpg',
isLive: true,
viewers: 272
},
{
id: 'live-2',
title: '慧眼读书会',
subtitle: '',
imageUrl: 'https://cdn.ipadbiz.cn/mlaj/images/_6HzPU9Hyfg (2).jpg',
isLive: true,
viewers: 272
}
];
// Course list data
export const courses = [
{
id: 'course-1',
title: '好分凭借力,陪你跃龙门!',
subtitle: '4.17-6.18 美乐考前赋能营',
imageUrl: 'https://cdn.ipadbiz.cn/mlaj/images/jbwr0qZvpD4.jpg', // Updated with new image
price: 365,
updatedLessons: 16,
subscribers: 1140,
expireDate: '2025-04-17 00:00:00',
description: `【美乐考前赋能营】
结合平台多年亲子教育的实践经验,
针对近在眼前的高考,
和为学业考试而焦虑的群体
精心打造、最为定制的专项课程。
旨在帮助家长更好地理解,支持孩子,
有效管理自己的情绪和压力;
进而引导孩子的情绪状态,
以最佳的心理状态迎接挑战。
在考试的最后冲刺阶段,
给世界一个爱赏识他们的理由,
共同助力每个学子梦想成真!`,
sections: [
{ title: '课程介绍', content: '课程详细描述内容...' },
{ title: '课程目录', content: '第1章:心态准备\n第2章:考前减压\n第3章:家庭支持' }
]
},
{
id: 'course-2',
title: '大国少年-梦想嘉年华',
subtitle: '亲子冬令营',
imageUrl: 'https://cdn.ipadbiz.cn/mlaj/images/GGCP6vshpPY.jpg', // Updated with new image
price: 3665,
updatedLessons: 16,
subscribers: 1140
},
{
id: 'course-3',
title: '大国少年-世界正东方',
subtitle: '亲子夏令营',
imageUrl: 'https://cdn.ipadbiz.cn/mlaj/images/2Juj2cXWB7U.jpg', // Updated with new image
price: 1280,
updatedLessons: 16,
subscribers: 1140
}
];
// Activity data
export const activities = [
{
id: 'activity-1',
title: '慧眼读书 | 《论语》',
subtitle: '亲子共读',
imageUrl: 'https://cdn.ipadbiz.cn/mlaj/images/2JIvboGLeho.jpg', // Updated with new image
location: '北京',
status: '活动中',
period: '2024.12.09-2025.01.17',
price: 99,
originalPrice: 120,
isHot: true,
isFree: false,
participantsCount: 18,
maxParticipants: 30
},
{
id: 'activity-2',
title: '好分凭借力,陪你跃龙门!',
subtitle: '4.17-6.18 美乐考前赋能营',
imageUrl: 'https://cdn.ipadbiz.cn/mlaj/images/_6HzPU9Hyfg (2).jpg', // Updated with new image
location: '线上',
status: '活动中',
period: '2024.12.17-2025.01.26',
price: 366,
originalPrice: 468,
isHot: false,
isFree: false,
participantsCount: 25,
maxParticipants: 50
},
{
id: 'activity-3',
title: '7.29-8.4 敦煌: 【青云之路】',
subtitle: '亲子游学营',
imageUrl: 'https://cdn.ipadbiz.cn/mlaj/images/Y17FE9Fuw4Y.jpg', // Updated with new image
location: '敦煌',
status: '即将开始',
period: '2025.01.07-2025.01.26',
price: 3980,
originalPrice: 4200,
isHot: true,
isFree: false,
participantsCount: 12,
maxParticipants: 20
},
{
id: 'activity-4',
title: '【大国少年·梦想嘉年华】',
subtitle: '亲子冬令营',
imageUrl: 'https://cdn.ipadbiz.cn/mlaj/images/-G3rw6Y02D0.jpg',
location: '上海',
status: '进行中',
period: '2025.01.09-2025.01.22',
price: 1280,
originalPrice: 1380,
isHot: false,
isFree: false,
participantsCount: 24,
maxParticipants: 40
},
{
id: 'activity-5',
title: '慧眼读书会 |《零极限》',
subtitle: '共读',
imageUrl: 'https://cdn.ipadbiz.cn/mlaj/images/Oalh2MojUuk.jpg',
location: '线上',
status: '进行中',
period: '2025.01.09-2025.01.22',
price: 0,
originalPrice: 0,
isHot: false,
isFree: true,
participantsCount: 45,
maxParticipants: 100
}
];
// User profile data
export const userProfile = {
name: '李玉红',
avatar: 'https://cdn.ipadbiz.cn/mlaj/images/user-avatar-2.jpg',
activities: [],
courses: [],
children: [],
checkIns: {
totalDays: 45,
currentStreak: 7,
longestStreak: 15
}
};
// Daily check-in data
export const checkInTypes = [
{ id: 'reading', name: '阅读打卡', icon: 'book' },
{ id: 'exercise', name: '运动打卡', icon: 'running' },
{ id: 'study', name: '学习打卡', icon: 'graduation-cap' },
{ id: 'reflection', name: '反思打卡', icon: 'pencil-alt' }
];
// Community posts data
export const communityPosts = [
{
id: 'post-1',
author: {
id: 'user-1',
name: '王小明',
avatar: 'https://cdn.ipadbiz.cn/mlaj/images/user-avatar-3.jpg'
},
content: '今天和孩子一起完成了《论语》的共读,收获颇丰!孩子对"己所不欲勿施于人"这句话有了更深的理解。',
images: ['https://cdn.ipadbiz.cn/mlaj/images/post-1-1.jpg', 'https://cdn.ipadbiz.cn/mlaj/images/post-1-2.jpg'],
likes: 24,
comments: 5,
createdAt: '2023-03-15T08:30:00Z'
},
{
id: 'post-2',
author: {
id: 'user-2',
name: '李晓华',
avatar: 'https://cdn.ipadbiz.cn/mlaj/images/user-avatar-1.jpg'
},
content: '冬令营第三天,孩子们参观了科技馆,学习了很多有趣的知识,晚上的篝火晚会也很精彩!',
images: ['https://cdn.ipadbiz.cn/mlaj/images/post-2-1.jpg'],
likes: 36,
comments: 8,
createdAt: '2023-03-14T15:45:00Z'
}
];
<script setup>
import { Button, NavBar, Tabbar, TabbarItem } from 'vant';
</script>
<template>
<div class="min-h-screen flex flex-col bg-gray-100">
<van-nav-bar
title="关于"
left-text="返回"
right-text="菜单"
left-arrow
@click-left="onClickLeft"
@click-right="onClickRight"
/>
<div class="flex-1 p-4">
<div class="bg-white rounded-lg shadow p-4 mb-4">
<h2 class="text-xl font-bold mb-2">关于我们</h2>
<p class="text-gray-600 mb-4">这是一个示例项目的关于页面,展示了如何使用Vue3、Vite、Vant4和TailwindCSS构建现代化的Web应用。</p>
<van-button type="primary" block>了解更多</van-button>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="bg-white rounded-lg shadow p-4">
<h3 class="text-lg font-semibold mb-2">联系方式</h3>
<ul class="text-gray-600">
<li>邮箱:example@example.com</li>
<li>电话:123-456-7890</li>
<li>地址:示例地址</li>
</ul>
</div>
<div class="bg-white rounded-lg shadow p-4">
<h3 class="text-lg font-semibold mb-2">版本信息</h3>
<ul class="text-gray-600">
<li>当前版本:1.0.0</li>
<li>更新日期:2024-03-20</li>
<li>开源协议:MIT</li>
</ul>
</div>
</div>
</div>
<van-tabbar v-model="active">
<van-tabbar-item icon="home-o">首页</van-tabbar-item>
<van-tabbar-item icon="search">搜索</van-tabbar-item>
<van-tabbar-item icon="friends-o">好友</van-tabbar-item>
<van-tabbar-item icon="setting-o">设置</van-tabbar-item>
</van-tabbar>
</div>
</template>
<script>
export default {
data() {
return {
active: 0
}
},
methods: {
onClickLeft() {
this.$router.back()
},
onClickRight() {
// 处理右侧按钮点击
}
}
}
</script>
This diff is collapsed. Click to expand it.
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{vue,js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}
/*
* @Date: 2025-03-20 19:53:12
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-03-20 20:27:29
* @FilePath: /vue-vite/vite.config.js
* @Description: 文件描述
*/
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import path from "path";
// https://vite.dev/config/
export default defineConfig({
plugins: [vue(), vueJsx()],
resolve: {
alias: { // 将会被传递到 @rollup/plugin-alias 作为 entries 的选项。也可以是一个对象,或一个 { find, replacement } 的数组. 当使用文件系统路径的别名时,请始终使用绝对路径。相对路径的别名值会被原封不动地使用,因此无法被正常解析。 更高级的自定义解析方法可以通过 插件 实现。
"@": path.resolve(__dirname, "src"),
"@components": path.resolve(__dirname, "src/components"),
"@composables": path.resolve(__dirname, "src/composables"),
"@utils": path.resolve(__dirname, "src/utils"),
"@images": path.resolve(__dirname, "src/assets/images"),
"@css": path.resolve(__dirname, "src/assets/css"),
"@mock": path.resolve(__dirname, "src/assets/mock"),
"common": path.resolve(__dirname, "src/common"),
"@api": path.resolve(__dirname, "src/api"),
},
dedupe: ['vue'], // 如果你在你的应用程序中有相同依赖的副本(比如 monorepos),使用这个选项来强制 Vite 总是将列出的依赖关系解析到相同的副本(从项目根目录)。
// conditions: [''], // 在解析包的 情景导出 时允许的附加条件。
// mainFields: [''], // package.json 中,在解析包的入口点时尝试的字段列表。注意,这比从 exports 字段解析的情景导出优先级低:如果一个入口点从 exports 成功解析,主字段将被忽略。
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue'], // 导入时想要省略的扩展名列表。注意,不 建议忽略自定义导入类型的扩展名(例如:.vue),因为它会干扰 IDE 和类型支持。
},
})