hookehuyr

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

添加了多个新图片资源,包括用户头像、课程封面和活动图片。同时,初始化了项目的基础结构,包括配置文件的设置(如postcss.config.js、tailwind.config.js、.gitignore等)、主入口文件(main.js)的创建,以及基础组件和页面的实现(如App.vue、HelloWorld.vue、FrostedGlass.vue等)。这些更改为后续开发提供了必要的资源支持和基础框架。
Showing 81 changed files with 4183 additions and 0 deletions
1 +# Logs
2 +logs
3 +*.log
4 +npm-debug.log*
5 +yarn-debug.log*
6 +yarn-error.log*
7 +pnpm-debug.log*
8 +lerna-debug.log*
9 +
10 +node_modules
11 +dist
12 +dist-ssr
13 +*.local
14 +
15 +# Editor directories and files
16 +.vscode/*
17 +!.vscode/extensions.json
18 +.idea
19 +.DS_Store
20 +*.suo
21 +*.ntvs*
22 +*.njsproj
23 +*.sln
24 +*.sw?
25 +
26 +.history
27 +node_modules
28 +.vscode
1 +# Vue 3 + Vite
2 +
3 +This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
4 +
5 +Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).
1 +<!doctype html>
2 +<html lang="en">
3 + <head>
4 + <meta charset="UTF-8" />
5 + <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6 + <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7 + <title>Vite + Vue</title>
8 + </head>
9 + <body>
10 + <div id="app"></div>
11 + <script type="module" src="/src/main.js"></script>
12 + </body>
13 +</html>
This diff could not be displayed because it is too large.
1 +{
2 + "name": "vue-vite",
3 + "private": true,
4 + "version": "0.0.0",
5 + "type": "module",
6 + "scripts": {
7 + "dev": "vite",
8 + "build": "vite build",
9 + "preview": "vite preview"
10 + },
11 + "dependencies": {
12 + "@vant/use": "^1.6.0",
13 + "vant": "^4.9.18",
14 + "vue": "^3.5.13",
15 + "vue-router": "^4.5.0"
16 + },
17 + "devDependencies": {
18 + "@vitejs/plugin-vue": "^5.2.1",
19 + "@vitejs/plugin-vue-jsx": "^4.1.2",
20 + "autoprefixer": "^10.4.19",
21 + "postcss": "^8.4.35",
22 + "tailwindcss": "^3.4.1",
23 + "vite": "^6.2.0"
24 + }
25 +}
1 +export default {
2 + plugins: {
3 + tailwindcss: {},
4 + autoprefixer: {},
5 + },
6 +}
1 +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
...\ No newline at end of file ...\ No newline at end of file
1 +<!--
2 + * @Date: 2025-03-20 19:53:12
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2025-03-20 20:06:10
5 + * @FilePath: /vue-vite/src/App.vue
6 + * @Description: 文件描述
7 +-->
8 +<script setup>
9 +import { RouterView } from "vue-router";
10 +</script>
11 +
12 +<template>
13 + <div>
14 + <router-view>
15 + <template v-slot="{ Component }">
16 + <Suspense>
17 + <template #default>
18 + <div>
19 + <component :is="Component" />
20 + </div>
21 + </template>
22 + <template #fallback>
23 + <div
24 + class="flex items-center justify-center h-screen bg-gradient-to-br from-green-50 via-teal-50 to-blue-50"
25 + >
26 + <div class="bg-white/20 backdrop-blur-md rounded-xl p-6 shadow-lg">
27 + <div
28 + class="animate-spin rounded-full h-12 w-12 border-b-2 border-green-500 mx-auto"
29 + ></div>
30 + <p class="mt-4 text-gray-700">加载中...</p>
31 + </div>
32 + </div>
33 + </template>
34 + </Suspense>
35 + </template>
36 + </router-view>
37 + </div>
38 +</template>
39 +
40 +<style>
41 +#app {
42 + width: 100%;
43 + min-height: 100vh;
44 +}
45 +</style>
1 +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
...\ No newline at end of file ...\ No newline at end of file
1 +<script setup>
2 +import { ref } from 'vue'
3 +
4 +defineProps({
5 + msg: String,
6 +})
7 +
8 +const count = ref(0)
9 +</script>
10 +
11 +<template>
12 + <h1>{{ msg }}</h1>
13 +
14 + <div class="card">
15 + <button type="button" @click="count++">count is {{ count }}</button>
16 + <p>
17 + Edit
18 + <code>components/HelloWorld.vue</code> to test HMR
19 + </p>
20 + </div>
21 +
22 + <p>
23 + Check out
24 + <a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
25 + >create-vue</a
26 + >, the official Vue + Vite starter
27 + </p>
28 + <p>
29 + Learn more about IDE Support for Vue in the
30 + <a
31 + href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
32 + target="_blank"
33 + >Vue Docs Scaling up Guide</a
34 + >.
35 + </p>
36 + <p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
37 +</template>
38 +
39 +<style scoped>
40 +.read-the-docs {
41 + color: #888;
42 +}
43 +</style>
1 +import React from 'react';
2 +import BottomNav from './BottomNav';
3 +import GradientHeader from '../ui/GradientHeader';
4 +
5 +/**
6 + * AppLayout component provides consistent layout across the app
7 + *
8 + * @param {Object} props - Component props
9 + * @param {ReactNode} props.children - Child elements
10 + * @param {string} props.title - Page title
11 + * @param {boolean} props.showBackButton - Whether to display back button
12 + * @param {Function} props.onBack - Back button click handler
13 + * @param {ReactNode} props.rightContent - Content to display on the right side of header
14 + * @returns {JSX.Element} AppLayout component
15 + */
16 +const AppLayout = ({ children, title, showBackButton, onBack, rightContent }) => {
17 + const handleBack = () => {
18 + if (onBack) {
19 + onBack();
20 + } else {
21 + window.history.back();
22 + }
23 + };
24 +
25 + return (
26 + <div className="bg-gradient-to-br from-green-50 via-teal-50 to-blue-50 min-h-screen pb-16">
27 + <GradientHeader
28 + title={title}
29 + showBackButton={showBackButton}
30 + onBack={handleBack}
31 + rightContent={rightContent}
32 + />
33 + <main className="pb-16">
34 + {children}
35 + </main>
36 + <BottomNav />
37 + </div>
38 + );
39 +};
40 +
41 +export default AppLayout;
...\ No newline at end of file ...\ No newline at end of file
1 +<template>
2 + <div class="bg-gradient-to-br from-green-50 via-teal-50 to-blue-50 min-h-screen pb-16">
3 + <GradientHeader
4 + :title="title"
5 + :showBackButton="showBackButton"
6 + :onBack="handleBack"
7 + :rightContent="rightContent"
8 + />
9 + <main class="pb-16">
10 + <slot></slot>
11 + </main>
12 + <BottomNav />
13 + </div>
14 +</template>
15 +
16 +<script setup>
17 +import { defineProps } from 'vue'
18 +import BottomNav from './BottomNav.vue'
19 +import GradientHeader from '../ui/GradientHeader.vue'
20 +
21 +const props = defineProps({
22 + title: {
23 + type: String,
24 + required: true
25 + },
26 + showBackButton: {
27 + type: Boolean,
28 + default: false
29 + },
30 + onBack: {
31 + type: Function,
32 + default: null
33 + },
34 + rightContent: {
35 + type: Object,
36 + default: null
37 + }
38 +})
39 +
40 +const handleBack = () => {
41 + if (props.onBack) {
42 + props.onBack()
43 + } else {
44 + window.history.back()
45 + }
46 +}
47 +</script>
1 +import React from 'react';
2 +import { Link, useLocation } from 'react-router-dom';
3 +
4 +/**
5 + * BottomNav component for app navigation
6 + *
7 + * @returns {JSX.Element} BottomNav component
8 + */
9 +const BottomNav = () => {
10 + const location = useLocation();
11 +
12 + const navItems = [
13 + {
14 + name: '首页',
15 + path: '/',
16 + icon: (
17 + <svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 mb-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
18 + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
19 + </svg>
20 + )
21 + },
22 + {
23 + name: '课程',
24 + path: '/courses',
25 + icon: (
26 + <svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 mb-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
27 + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
28 + </svg>
29 + )
30 + },
31 + {
32 + name: '空间',
33 + path: '/community',
34 + icon: (
35 + <svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 mb-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
36 + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
37 + </svg>
38 + )
39 + },
40 + {
41 + name: '我的',
42 + path: '/profile',
43 + icon: (
44 + <svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 mb-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
45 + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
46 + </svg>
47 + )
48 + }
49 + ];
50 +
51 + return (
52 + <nav className="fixed bottom-0 left-0 right-0 bg-white/70 backdrop-blur-lg border-t border-gray-100 z-50">
53 + <div className="flex justify-around items-center h-16">
54 + {navItems.map((item) => {
55 + const isActive = location.pathname === item.path ||
56 + (item.path !== '/' && location.pathname.startsWith(item.path));
57 + return (
58 + <Link
59 + key={item.name}
60 + to={item.path}
61 + className={`flex flex-col items-center justify-center w-1/4 h-full ${
62 + isActive ? 'text-green-500' : 'text-gray-500'
63 + }`}
64 + >
65 + {React.cloneElement(item.icon, {
66 + className: `${item.icon.props.className} ${isActive ? 'text-green-500' : 'text-gray-500'}`
67 + })}
68 + <span className="text-xs">{item.name}</span>
69 + </Link>
70 + );
71 + })}
72 + </div>
73 + </nav>
74 + );
75 +};
76 +
77 +export default BottomNav;
...\ No newline at end of file ...\ No newline at end of file
1 +<template>
2 + <nav class="fixed bottom-0 left-0 right-0 bg-white/70 backdrop-blur-lg border-t border-gray-100 z-50">
3 + <div class="flex justify-around items-center h-16">
4 + <router-link
5 + v-for="item in navItems"
6 + :key="item.name"
7 + :to="item.path"
8 + class="flex flex-col items-center justify-center w-1/4 h-full"
9 + :class="{
10 + 'text-green-500': isActive(item.path),
11 + 'text-gray-500': !isActive(item.path)
12 + }"
13 + >
14 + <svg
15 + xmlns="http://www.w3.org/2000/svg"
16 + class="h-6 w-6 mb-1"
17 + :class="{
18 + 'text-green-500': isActive(item.path),
19 + 'text-gray-500': !isActive(item.path)
20 + }"
21 + fill="none"
22 + viewBox="0 0 24 24"
23 + stroke="currentColor"
24 + v-html="item.icon"
25 + />
26 + <span class="text-xs">{{ item.name }}</span>
27 + </router-link>
28 + </div>
29 + </nav>
30 +</template>
31 +
32 +<script setup>
33 +import { computed } from 'vue'
34 +import { useRoute } from 'vue-router'
35 +
36 +const route = useRoute()
37 +
38 +const navItems = [
39 + {
40 + name: '首页',
41 + path: '/',
42 + icon: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />'
43 + },
44 + {
45 + name: '课程',
46 + path: '/courses',
47 + icon: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />'
48 + },
49 + {
50 + name: '空间',
51 + path: '/community',
52 + icon: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />'
53 + },
54 + {
55 + name: '我的',
56 + path: '/profile',
57 + icon: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />'
58 + }
59 +]
60 +
61 +const isActive = (path) => {
62 + return route.path === path || (path !== '/' && route.path.startsWith(path))
63 +}
64 +</script>
1 +import React from 'react';
2 +import { Link } from 'react-router-dom';
3 +import FrostedGlass from './FrostedGlass';
4 +
5 +/**
6 + * ActivityCard component displays an activity item in the activities list
7 + *
8 + * @param {Object} props - Component props
9 + * @param {Object} props.activity - Activity data
10 + * @returns {JSX.Element} ActivityCard component
11 + */
12 +const ActivityCard = ({ activity }) => {
13 + // Function to get the appropriate status class
14 + const getStatusClass = (status) => {
15 + switch (status) {
16 + case '活动中':
17 + return 'bg-blue-100 text-blue-600';
18 + case '进行中':
19 + return 'bg-green-100 text-green-600';
20 + case '即将开始':
21 + return 'bg-orange-100 text-orange-600';
22 + case '已结束':
23 + return 'bg-gray-100 text-gray-600';
24 + default:
25 + return 'bg-gray-100 text-gray-600';
26 + }
27 + };
28 +
29 + return (
30 + <Link to={`/activities/${activity.id}`}>
31 + <FrostedGlass className="flex overflow-hidden rounded-xl shadow-sm">
32 + {/* Activity Image */}
33 + <div className="w-1/3 h-28 relative">
34 + <img
35 + src={activity.imageUrl}
36 + alt={activity.title}
37 + className="w-full h-full object-cover"
38 + />
39 + {activity.isHot && (
40 + <div className="absolute top-0 left-0 bg-red-500 text-white text-xs px-2 py-0.5">
41 + 热门
42 + </div>
43 + )}
44 + </div>
45 +
46 + {/* Activity Info */}
47 + <div className="flex-1 p-3 flex flex-col justify-between">
48 + <div>
49 + <h3 className="font-medium text-base mb-1 line-clamp-1">{activity.title}</h3>
50 +
51 + {/* Status Tags */}
52 + <div className="flex items-center space-x-2 mb-1">
53 + <span className={`px-2 py-0.5 rounded-full text-xs ${getStatusClass(activity.status)}`}>
54 + {activity.status}
55 + </span>
56 + {activity.isFree && (
57 + <span className="px-2 py-0.5 rounded-full text-xs bg-green-100 text-green-600">
58 + 免费
59 + </span>
60 + )}
61 + </div>
62 + </div>
63 +
64 + {/* Location and Time */}
65 + <div className="text-xs text-gray-500 space-y-1">
66 + <div className="flex items-center">
67 + <svg xmlns="http://www.w3.org/2000/svg" className="h-3.5 w-3.5 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
68 + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
69 + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
70 + </svg>
71 + <span>{activity.location}</span>
72 + </div>
73 +
74 + <div className="flex items-center">
75 + <svg xmlns="http://www.w3.org/2000/svg" className="h-3.5 w-3.5 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
76 + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
77 + </svg>
78 + <span>{activity.period}</span>
79 + </div>
80 + </div>
81 +
82 + {/* Bottom Info Section */}
83 + <div className="mt-1 flex items-center justify-between">
84 + {activity.price ? (
85 + <div className="flex items-baseline">
86 + <span className="text-red-500 font-medium">¥{activity.price}</span>
87 + {activity.originalPrice && (
88 + <span className="text-xs text-gray-400 ml-1 line-through">¥{activity.originalPrice}</span>
89 + )}
90 + </div>
91 + ) : (
92 + <div></div> // Empty div for spacing when no price
93 + )}
94 +
95 + <div className="flex items-center text-xs text-gray-500">
96 + <svg xmlns="http://www.w3.org/2000/svg" className="h-3.5 w-3.5 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
97 + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
98 + </svg>
99 + <span>{activity.participantsCount || '15'}/{activity.maxParticipants || '30'}</span>
100 + </div>
101 + </div>
102 + </div>
103 + </FrostedGlass>
104 + </Link>
105 + );
106 +};
107 +
108 +export default ActivityCard;
...\ No newline at end of file ...\ No newline at end of file
1 +<template>
2 + <router-link :to="`/activities/${activity.id}`">
3 + <FrostedGlass class="flex overflow-hidden rounded-xl shadow-sm">
4 + <!-- Activity Image -->
5 + <div class="w-1/3 h-28 relative">
6 + <img
7 + :src="activity.imageUrl"
8 + :alt="activity.title"
9 + class="w-full h-full object-cover"
10 + />
11 + <div v-if="activity.isHot" class="absolute top-0 left-0 bg-red-500 text-white text-xs px-2 py-0.5">
12 + 热门
13 + </div>
14 + </div>
15 +
16 + <!-- Activity Info -->
17 + <div class="flex-1 p-3 flex flex-col justify-between">
18 + <div>
19 + <h3 class="font-medium text-base mb-1 line-clamp-1">{{ activity.title }}</h3>
20 +
21 + <!-- Status Tags -->
22 + <div class="flex items-center space-x-2 mb-1">
23 + <span :class="['px-2 py-0.5 rounded-full text-xs', getStatusClass(activity.status)]">
24 + {{ activity.status }}
25 + </span>
26 + <span v-if="activity.isFree" class="px-2 py-0.5 rounded-full text-xs bg-green-100 text-green-600">
27 + 免费
28 + </span>
29 + </div>
30 + </div>
31 +
32 + <!-- Location and Time -->
33 + <div class="text-xs text-gray-500 space-y-1">
34 + <div class="flex items-center">
35 + <svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
36 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
37 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
38 + </svg>
39 + <span>{{ activity.location }}</span>
40 + </div>
41 +
42 + <div class="flex items-center">
43 + <svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
44 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
45 + </svg>
46 + <span>{{ activity.period }}</span>
47 + </div>
48 + </div>
49 +
50 + <!-- Bottom Info Section -->
51 + <div class="mt-1 flex items-center justify-between">
52 + <div v-if="activity.price" class="flex items-baseline">
53 + <span class="text-red-500 font-medium">¥{{ activity.price }}</span>
54 + <span v-if="activity.originalPrice" class="text-xs text-gray-400 ml-1 line-through">¥{{ activity.originalPrice }}</span>
55 + </div>
56 + <div v-else></div>
57 +
58 + <div class="flex items-center text-xs text-gray-500">
59 + <svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
60 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
61 + </svg>
62 + <span>{{ activity.participantsCount || '15' }}/{{ activity.maxParticipants || '30' }}</span>
63 + </div>
64 + </div>
65 + </div>
66 + </FrostedGlass>
67 + </router-link>
68 +</template>
69 +
70 +<script setup>
71 +import { defineProps } from 'vue'
72 +import FrostedGlass from './FrostedGlass.vue'
73 +
74 +defineProps({
75 + activity: {
76 + type: Object,
77 + required: true
78 + }
79 +})
80 +
81 +const getStatusClass = (status) => {
82 + switch (status) {
83 + case '活动中':
84 + return 'bg-blue-100 text-blue-600'
85 + case '进行中':
86 + return 'bg-green-100 text-green-600'
87 + case '即将开始':
88 + return 'bg-orange-100 text-orange-600'
89 + case '已结束':
90 + return 'bg-gray-100 text-gray-600'
91 + default:
92 + return 'bg-gray-100 text-gray-600'
93 + }
94 +}
95 +</script>
1 +import React from 'react';
2 +import { Link } from 'react-router-dom';
3 +
4 +/**
5 + * CourseCard component displays a course item in the course list
6 + *
7 + * @param {Object} props - Component props
8 + * @param {Object} props.course - Course data
9 + * @returns {JSX.Element} CourseCard component
10 + */
11 +const CourseCard = ({ course }) => {
12 + return (
13 + <Link to={`/courses/${course.id}`} className="flex bg-white rounded-lg overflow-hidden shadow-sm">
14 + <div className="w-1/3 h-28">
15 + <img
16 + src={course.imageUrl}
17 + alt={course.title}
18 + className="w-full h-full object-cover"
19 + />
20 + </div>
21 + <div className="flex-1 p-3 flex flex-col justify-between">
22 + <div>
23 + <h3 className="font-medium text-sm mb-1 line-clamp-2">{course.title}</h3>
24 + <div className="text-gray-500 text-xs">{course.subtitle}</div>
25 + </div>
26 + <div className="flex justify-between items-end mt-1">
27 + <div className="text-orange-500 font-semibold">¥{course.price}</div>
28 + <div className="text-gray-400 text-xs">
29 + {course.subscribers}人订阅
30 + </div>
31 + </div>
32 + <div className="text-gray-400 text-xs">
33 + 已更新{course.updatedLessons}期 | {course.subscribers}人订阅
34 + </div>
35 + </div>
36 + </Link>
37 + );
38 +};
39 +
40 +export default CourseCard;
...\ No newline at end of file ...\ No newline at end of file
1 +<template>
2 + <router-link :to="`/courses/${course.id}`" class="flex bg-white rounded-lg overflow-hidden shadow-sm">
3 + <div class="w-1/3 h-28">
4 + <img
5 + :src="course.imageUrl"
6 + :alt="course.title"
7 + class="w-full h-full object-cover"
8 + />
9 + </div>
10 + <div class="flex-1 p-3 flex flex-col justify-between">
11 + <div>
12 + <h3 class="font-medium text-sm mb-1 line-clamp-2">{{ course.title }}</h3>
13 + <div class="text-gray-500 text-xs">{{ course.subtitle }}</div>
14 + </div>
15 + <div class="flex justify-between items-end mt-1">
16 + <div class="text-orange-500 font-semibold">¥{{ course.price }}</div>
17 + <div class="text-gray-400 text-xs">
18 + {{ course.subscribers }}人订阅
19 + </div>
20 + </div>
21 + <div class="text-gray-400 text-xs">
22 + 已更新{{ course.updatedLessons }}期 | {{ course.subscribers }}人订阅
23 + </div>
24 + </div>
25 + </router-link>
26 +</template>
27 +
28 +<script setup>
29 +import { defineProps } from 'vue'
30 +
31 +defineProps({
32 + course: {
33 + type: Object,
34 + required: true
35 + }
36 +})
37 +</script>
1 +import React from 'react';
2 +
3 +/**
4 + * FrostedGlass component creates a container with a frosted glass effect
5 + * using backdrop-filter blur and a semi-transparent white background.
6 + *
7 + * @param {Object} props - Component props
8 + * @param {ReactNode} props.children - Child elements
9 + * @param {string} props.className - Additional CSS classes
10 + * @returns {JSX.Element} FrostedGlass component
11 + */
12 +const FrostedGlass = ({ children, className = '' }) => {
13 + return (
14 + <div
15 + className={`bg-white/20 backdrop-blur-md rounded-xl border border-white/30
16 + shadow-lg ${className}`}
17 + >
18 + {children}
19 + </div>
20 + );
21 +};
22 +
23 +export default FrostedGlass;
...\ No newline at end of file ...\ No newline at end of file
1 +<template>
2 + <div
3 + :class="[
4 + 'bg-white/20 backdrop-blur-md rounded-xl border border-white/30 shadow-lg',
5 + className
6 + ]"
7 + >
8 + <slot></slot>
9 + </div>
10 +</template>
11 +
12 +<script setup>
13 +import { defineProps } from 'vue'
14 +
15 +defineProps({
16 + className: {
17 + type: String,
18 + default: ''
19 + }
20 +})
21 +</script>
1 +import React from 'react';
2 +
3 +/**
4 + * GradientHeader component for page headers with gradient background
5 + * and navigation elements.
6 + *
7 + * @param {Object} props - Component props
8 + * @param {string} props.title - Header title
9 + * @param {boolean} props.showBackButton - Whether to show back button
10 + * @param {Function} props.onBack - Back button click handler
11 + * @param {ReactNode} props.rightContent - Content to display on the right side
12 + * @returns {JSX.Element} GradientHeader component
13 + */
14 +const GradientHeader = ({ title, showBackButton = false, onBack, rightContent }) => {
15 + return (
16 + <header className="bg-gradient-to-r from-green-50 to-blue-50 p-4 relative">
17 + <div className="flex items-center justify-between">
18 + {showBackButton && (
19 + <button
20 + onClick={onBack}
21 + className="p-2 rounded-full bg-white/30 backdrop-blur-sm"
22 + aria-label="返回"
23 + >
24 + <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 text-gray-700" fill="none" viewBox="0 0 24 24" stroke="currentColor">
25 + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
26 + </svg>
27 + </button>
28 + )}
29 + <h1 className={`text-xl font-medium text-center ${showBackButton ? 'flex-1' : ''}`}>
30 + {title}
31 + </h1>
32 + {rightContent && <div>{rightContent}</div>}
33 + </div>
34 + </header>
35 + );
36 +};
37 +
38 +export default GradientHeader;
...\ No newline at end of file ...\ No newline at end of file
1 +<template>
2 + <header class="bg-gradient-to-r from-green-50 to-blue-50 p-4 relative">
3 + <div class="flex items-center justify-between">
4 + <button
5 + v-if="showBackButton"
6 + @click="onBack"
7 + class="p-2 rounded-full bg-white/30 backdrop-blur-sm"
8 + aria-label="返回"
9 + >
10 + <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-700" fill="none" viewBox="0 0 24 24" stroke="currentColor">
11 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
12 + </svg>
13 + </button>
14 + <h1 :class="['text-xl font-medium text-center', showBackButton ? 'flex-1' : '']">
15 + {{ title }}
16 + </h1>
17 + <div v-if="rightContent" v-html="rightContent.content"></div>
18 + </div>
19 + </header>
20 +</template>
21 +
22 +<script setup>
23 +import { defineProps } from 'vue'
24 +
25 +defineProps({
26 + title: {
27 + type: String,
28 + required: true
29 + },
30 + showBackButton: {
31 + type: Boolean,
32 + default: false
33 + },
34 + onBack: {
35 + type: Function,
36 + default: () => {}
37 + },
38 + rightContent: {
39 + type: Object,
40 + default: null
41 + }
42 +})
43 +</script>
1 +import React from 'react';
2 +import { Link } from 'react-router-dom';
3 +
4 +/**
5 + * LiveStreamCard component displays a live stream in the courses page
6 + *
7 + * @param {Object} props - Component props
8 + * @param {Object} props.stream - Stream data
9 + * @returns {JSX.Element} LiveStreamCard component
10 + */
11 +const LiveStreamCard = ({ stream }) => {
12 + return (
13 + <div className="relative">
14 + {/* Live indicator */}
15 + <div className="absolute top-2 left-2 bg-red-500/90 text-white text-xs px-2 py-1 rounded flex items-center z-10">
16 + <div className="w-2 h-2 bg-white rounded-full mr-1 animate-pulse"></div>
17 + 直播中
18 + </div>
19 +
20 + <Link to={`/courses/${stream.id}`} className="block rounded-lg overflow-hidden shadow-sm relative">
21 + <img
22 + src={stream.imageUrl}
23 + alt={stream.title}
24 + className="w-full h-28 object-cover"
25 + />
26 +
27 + {/* Gradient overlay */}
28 + <div className="absolute inset-0 bg-gradient-to-b from-transparent to-black/60"></div>
29 +
30 + {/* Stream info */}
31 + <div className="absolute bottom-2 left-2 right-2">
32 + <h3 className="text-white text-sm font-medium">{stream.title}{stream.subtitle}</h3>
33 + <div className="flex items-center mt-1">
34 + <div className="flex -space-x-2">
35 + <div className="w-5 h-5 rounded-full bg-gray-300 border border-white"></div>
36 + <div className="w-5 h-5 rounded-full bg-gray-400 border border-white"></div>
37 + <div className="w-5 h-5 rounded-full bg-gray-500 border border-white"></div>
38 + </div>
39 + <span className="text-white text-xs ml-1">{stream.viewers}人在看</span>
40 + <button className="ml-auto bg-green-500 text-white text-xs px-2 py-1 rounded">
41 + 立即观看
42 + </button>
43 + </div>
44 + </div>
45 + </Link>
46 + </div>
47 + );
48 +};
49 +
50 +export default LiveStreamCard;
...\ No newline at end of file ...\ No newline at end of file
1 +<!--
2 + * @Date: 2025-03-20 15:33:07
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2025-03-20 15:33:08
5 + * @FilePath: /react_template_1/src/components/ui/LiveStreamCard.vue
6 + * @Description: 文件描述
7 +-->
8 +<template>
9 + <div class="relative">
10 + <!-- Live indicator -->
11 + <div class="absolute top-2 left-2 bg-red-500/90 text-white text-xs px-2 py-1 rounded flex items-center z-10">
12 + <div class="w-2 h-2 bg-white rounded-full mr-1 animate-pulse"></div>
13 + 直播中
14 + </div>
15 +
16 + <router-link :to="`/courses/${stream.id}`" class="block rounded-lg overflow-hidden shadow-sm relative">
17 + <img
18 + :src="stream.imageUrl"
19 + :alt="stream.title"
20 + class="w-full h-28 object-cover"
21 + />
22 +
23 + <!-- Gradient overlay -->
24 + <div class="absolute inset-0 bg-gradient-to-b from-transparent to-black/60"></div>
25 +
26 + <!-- Stream info -->
27 + <div class="absolute bottom-2 left-2 right-2">
28 + <h3 class="text-white text-sm font-medium">「{{ stream.title }}」{{ stream.subtitle }}</h3>
29 + <div class="flex items-center mt-1">
30 + <div class="flex -space-x-2">
31 + <div class="w-5 h-5 rounded-full bg-gray-300 border border-white"></div>
32 + <div class="w-5 h-5 rounded-full bg-gray-400 border border-white"></div>
33 + <div class="w-5 h-5 rounded-full bg-gray-500 border border-white"></div>
34 + </div>
35 + <span class="text-white text-xs ml-1">{{ stream.viewers }}人在看</span>
36 + <button class="ml-auto bg-green-500 text-white text-xs px-2 py-1 rounded">
37 + 立即观看
38 + </button>
39 + </div>
40 + </div>
41 + </router-link>
42 + </div>
43 +</template>
44 +
45 +<script setup>
46 +import { defineProps } from 'vue'
47 +
48 +defineProps({
49 + stream: {
50 + type: Object,
51 + required: true
52 + }
53 +})
54 +</script>
1 +import React from 'react';
2 +import FrostedGlass from './FrostedGlass';
3 +
4 +/**
5 + * SearchBar component with frosted glass effect
6 + *
7 + * @param {Object} props - Component props
8 + * @param {string} props.placeholder - Placeholder text
9 + * @param {Function} props.onSearch - Search callback function
10 + * @returns {JSX.Element} SearchBar component
11 + */
12 +const SearchBar = ({ placeholder = '搜索', onSearch }) => {
13 + return (
14 + <FrostedGlass className="px-4 py-2 mx-4 my-3 flex items-center">
15 + <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 text-gray-400 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
16 + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
17 + </svg>
18 + <input
19 + type="text"
20 + placeholder={placeholder}
21 + className="bg-transparent outline-none flex-1 text-gray-700 placeholder-gray-400"
22 + onChange={(e) => onSearch && onSearch(e.target.value)}
23 + />
24 + </FrostedGlass>
25 + );
26 +};
27 +
28 +export default SearchBar;
...\ No newline at end of file ...\ No newline at end of file
1 +<template>
2 + <FrostedGlass class="px-4 py-2 mx-4 my-3 flex items-center">
3 + <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-400 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
4 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
5 + </svg>
6 + <input
7 + type="text"
8 + :placeholder="placeholder"
9 + class="bg-transparent outline-none flex-1 text-gray-700 placeholder-gray-400"
10 + @input="handleSearch"
11 + />
12 + </FrostedGlass>
13 +</template>
14 +
15 +<script setup>
16 +import { defineProps, defineEmits } from 'vue'
17 +import FrostedGlass from './FrostedGlass.vue'
18 +
19 +const props = defineProps({
20 + placeholder: {
21 + type: String,
22 + default: '搜索'
23 + }
24 +})
25 +
26 +const emit = defineEmits(['search'])
27 +
28 +const handleSearch = (e) => {
29 + emit('search', e.target.value)
30 +}
31 +</script>
1 +import React from 'react';
2 +import PropTypes from 'prop-types';
3 +
4 +/**
5 + * SummerCampCard component - displays summer camp information with image background
6 + * @param {Object} props - Component props
7 + * @returns {JSX.Element} - Rendered component
8 + */
9 +const SummerCampCard = ({
10 + title = "大国少年-世界正东方",
11 + subtitle = "亲子夏令营",
12 + badge = "亲子夏令营",
13 + price = "¥1280",
14 + discount = "限时优惠",
15 + episodes = 16,
16 + subscribers = 1140
17 +}) => {
18 + return (
19 + <div className="relative overflow-hidden rounded-b-3xl shadow-lg">
20 + {/* Background image with overlay */}
21 + <div
22 + className="absolute inset-0 z-0 bg-cover bg-center"
23 + style={{
24 + backgroundImage: `url('/assets/images/summer-camp.jpg')`,
25 + filter: 'brightness(0.4)'
26 + }}
27 + ></div>
28 +
29 + {/* Gradient overlay */}
30 + <div className="absolute inset-0 z-1 bg-gradient-to-b from-red-500/70 to-red-600/90"></div>
31 +
32 + {/* Content */}
33 + <div className="relative z-10 p-4">
34 + <div className="bg-white/10 backdrop-blur-sm rounded-lg p-3 mb-3 inline-block">
35 + <div className="text-white font-semibold">{badge}</div>
36 + </div>
37 +
38 + <h1 className="text-2xl text-white font-bold mb-1">{title}</h1>
39 + <h2 className="text-lg text-white/90">{subtitle}</h2>
40 +
41 + <div className="mt-4 flex justify-between items-center">
42 + <div className="text-orange-300 font-bold text-2xl">{price}</div>
43 + <div className="bg-orange-500/30 text-orange-100 text-xs px-3 py-1 rounded-full">{discount}</div>
44 + </div>
45 +
46 + <div className="flex justify-between text-xs text-white/80 mt-3">
47 + <div>已更新{episodes}</div>
48 + <div>{subscribers}人订阅</div>
49 + </div>
50 + </div>
51 + </div>
52 + );
53 +};
54 +
55 +SummerCampCard.propTypes = {
56 + title: PropTypes.string,
57 + subtitle: PropTypes.string,
58 + badge: PropTypes.string,
59 + price: PropTypes.string,
60 + discount: PropTypes.string,
61 + episodes: PropTypes.number,
62 + subscribers: PropTypes.number
63 +};
64 +
65 +export default SummerCampCard;
...\ No newline at end of file ...\ No newline at end of file
1 +<template>
2 + <div class="relative overflow-hidden rounded-b-3xl shadow-lg">
3 + <!-- Background image with overlay -->
4 + <div
5 + class="absolute inset-0 z-0 bg-cover bg-center"
6 + :style="{
7 + backgroundImage: `url('https://cdn.ipadbiz.cn/mlaj/images/summer-camp.jpg')`,
8 + filter: 'brightness(0.4)'
9 + }"
10 + ></div>
11 +
12 + <!-- Gradient overlay -->
13 + <div class="absolute inset-0 z-1 bg-gradient-to-b from-red-500/70 to-red-600/90"></div>
14 +
15 + <!-- Content -->
16 + <div class="relative z-10 p-4">
17 + <div class="bg-white/10 backdrop-blur-sm rounded-lg p-3 mb-3 inline-block">
18 + <div class="text-white font-semibold">{{ badge }}</div>
19 + </div>
20 +
21 + <h1 class="text-2xl text-white font-bold mb-1">{{ title }}</h1>
22 + <h2 class="text-lg text-white/90">{{ subtitle }}</h2>
23 +
24 + <div class="mt-4 flex justify-between items-center">
25 + <div class="text-orange-300 font-bold text-2xl">{{ price }}</div>
26 + <div class="bg-orange-500/30 text-orange-100 text-xs px-3 py-1 rounded-full">{{ discount }}</div>
27 + </div>
28 +
29 + <div class="flex justify-between text-xs text-white/80 mt-3">
30 + <div>已更新{{ episodes }}期</div>
31 + <div>{{ subscribers }}人订阅</div>
32 + </div>
33 + </div>
34 + </div>
35 +</template>
36 +
37 +<script setup>
38 +import { defineProps } from 'vue'
39 +
40 +defineProps({
41 + title: {
42 + type: String,
43 + default: '大国少年-世界正东方'
44 + },
45 + subtitle: {
46 + type: String,
47 + default: '亲子夏令营'
48 + },
49 + badge: {
50 + type: String,
51 + default: '亲子夏令营'
52 + },
53 + price: {
54 + type: String,
55 + default: '¥1280'
56 + },
57 + discount: {
58 + type: String,
59 + default: '限时优惠'
60 + },
61 + episodes: {
62 + type: Number,
63 + default: 16
64 + },
65 + subscribers: {
66 + type: Number,
67 + default: 1140
68 + }
69 +})
70 +</script>
1 +<!-- src/layouts/AppLayout.vue -->
2 +<template>
3 + <div class="app-layout">
4 + <!-- Header -->
5 + <header class="app-header">
6 + <div v-if="showBack" class="header-back" @click="goBack">
7 + <van-icon name="arrow-left" size="20" />
8 + </div>
9 + <h1 class="header-title">{{ title }}</h1>
10 + <div class="header-right">
11 + <slot name="header-right"></slot>
12 + </div>
13 + </header>
14 +
15 + <!-- Main Content -->
16 + <main class="app-content" :class="{ 'has-bottom-nav': showBottomNav }">
17 + <slot></slot>
18 + </main>
19 +
20 + <!-- Bottom Navigation -->
21 + <van-tabbar v-if="showBottomNav" route safe-area-inset-bottom>
22 + <van-tabbar-item to="/home" icon="home-o">首页</van-tabbar-item>
23 + <van-tabbar-item to="/courses" icon="orders-o">课程</van-tabbar-item>
24 + <van-tabbar-item to="/activities" icon="friends-o">活动</van-tabbar-item>
25 + <van-tabbar-item to="/community" icon="chat-o">社区</van-tabbar-item>
26 + <van-tabbar-item to="/profile" icon="user-o">我的</van-tabbar-item>
27 + </van-tabbar>
28 + </div>
29 +</template>
30 +
31 +<script>
32 +import { ref, onMounted } from 'vue'
33 +import { useRouter, useRoute } from 'vue-router'
34 +
35 +export default {
36 + name: 'AppLayout',
37 + props: {
38 + title: {
39 + type: String,
40 + default: '亲子学院'
41 + },
42 + showBack: {
43 + type: Boolean,
44 + default: false
45 + },
46 + showBottomNav: {
47 + type: Boolean,
48 + default: true
49 + }
50 + },
51 + setup(props) {
52 + const router = useRouter()
53 + const route = useRoute()
54 +
55 + const goBack = () => {
56 + if (window.history.length > 1) {
57 + router.back()
58 + } else {
59 + router.push('/')
60 + }
61 + }
62 +
63 + return {
64 + goBack
65 + }
66 + }
67 +}
68 +</script>
69 +
70 +<style scoped>
71 +.app-layout {
72 + display: flex;
73 + flex-direction: column;
74 + min-height: 100vh;
75 + background-color: var(--background-color);
76 +}
77 +
78 +.app-header {
79 + position: sticky;
80 + top: 0;
81 + z-index: 100;
82 + display: flex;
83 + align-items: center;
84 + justify-content: center;
85 + height: 46px;
86 + padding: 0 16px;
87 + background-color: var(--white);
88 + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
89 +}
90 +
91 +.header-back {
92 + position: absolute;
93 + left: 16px;
94 + display: flex;
95 + align-items: center;
96 + justify-content: center;
97 +}
98 +
99 +.header-title {
100 + font-size: 18px;
101 + font-weight: 600;
102 + margin: 0;
103 +}
104 +
105 +.header-right {
106 + position: absolute;
107 + right: 16px;
108 + display: flex;
109 + align-items: center;
110 +}
111 +
112 +.app-content {
113 + flex: 1;
114 + overflow-y: auto;
115 + padding-bottom: 20px;
116 + -webkit-overflow-scrolling: touch;
117 +}
118 +
119 +.app-content.has-bottom-nav {
120 + padding-bottom: 50px;
121 +}
122 +</style>
...\ No newline at end of file ...\ No newline at end of file
1 +import { createApp } from 'vue'
2 +import './style.css'
3 +import App from './App.vue'
4 +import router from './router'
5 +
6 +// 导入Vant组件和样式
7 +import { Button, NavBar, Tabbar, TabbarItem } from 'vant'
8 +import 'vant/lib/index.css'
9 +
10 +const app = createApp(App)
11 +app.use(router)
12 +
13 +// 注册Vant组件
14 +app.use(Button)
15 +app.use(NavBar)
16 +app.use(Tabbar)
17 +app.use(TabbarItem)
18 +
19 +app.mount('#app')
1 +import { createRouter, createWebHistory } from 'vue-router'
2 +
3 +const routes = [
4 + {
5 + path: '/',
6 + name: 'home',
7 + component: () => import('../views/Home.vue')
8 + },
9 + {
10 + path: '/about',
11 + name: 'about',
12 + component: () => import('../views/About.vue')
13 + }
14 +]
15 +
16 +const router = createRouter({
17 + history: createWebHistory(),
18 + routes
19 +})
20 +
21 +export default router
1 +@tailwind base;
2 +@tailwind components;
3 +@tailwind utilities;
4 +
5 +/* Custom fonts */
6 +/* @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700&display=swap'); */
7 +
8 +/* Base styles */
9 +:root {
10 + --primary-color: #4caf50;
11 + --secondary-color: #2196f3;
12 + --background-start: #f0f9ef;
13 + --background-end: #e6f4ff;
14 + --text-color: #333333;
15 + --text-light: #6e6e6e;
16 +}
17 +
18 +body {
19 + font-family: 'Noto Sans SC', sans-serif;
20 + @apply text-gray-800;
21 +}
22 +
23 +/* Scrollbar styling */
24 +::-webkit-scrollbar {
25 + width: 5px;
26 + height: 5px;
27 +}
28 +
29 +::-webkit-scrollbar-track {
30 + background-color: transparent;
31 +}
32 +
33 +::-webkit-scrollbar-thumb {
34 + border-radius: 25px;
35 + transition: all 0.3s;
36 + background-color: rgba(106, 115, 125, 0.2);
37 + &:hover {
38 + background-color: rgba(106, 115, 125, 0.27);
39 + }
40 +}
41 +
42 +::-webkit-scrollbar-corner {
43 + display: none;
44 +}
45 +
46 +/* Custom frosted glass components */
47 +.frosted-glass {
48 + @apply bg-white/20 backdrop-blur-md border border-white/30 shadow-lg rounded-xl;
49 +}
50 +
51 +.frosted-card {
52 + @apply frosted-glass p-4;
53 +}
54 +
55 +.gradient-bg {
56 + @apply bg-gradient-to-br from-green-50 via-teal-50 to-blue-50;
57 +}
58 +
59 +/* Custom animations */
60 +.fade-in {
61 + animation: fadeIn 0.5s ease-in;
62 +}
63 +
64 +@keyframes fadeIn {
65 + from { opacity: 0; }
66 + to { opacity: 1; }
67 +}
68 +
69 +/* Button styles */
70 +.btn-primary {
71 + @apply bg-gradient-to-r from-green-500 to-emerald-500 text-white py-2 px-4 rounded-full
72 + shadow-md hover:shadow-lg transition-all duration-300 font-medium;
73 +}
74 +
75 +.btn-secondary {
76 + @apply bg-white/70 backdrop-blur-sm text-gray-700 py-2 px-4 rounded-full
77 + shadow-sm hover:shadow transition-all duration-300 border border-gray-100;
78 +}
79 +
80 +.btn-outline {
81 + @apply border border-green-500 text-green-500 bg-transparent py-2 px-4 rounded-full
82 + hover:bg-green-50 transition-all duration-300;
83 +}
84 +
85 +/* Badge styles */
86 +.badge {
87 + @apply px-2 py-1 text-xs rounded-full font-medium;
88 +}
89 +
90 +.badge-success {
91 + @apply bg-green-100 text-green-700;
92 +}
93 +
94 +.badge-info {
95 + @apply bg-blue-100 text-blue-700;
96 +}
97 +
98 +.badge-warning {
99 + @apply bg-orange-100 text-orange-700;
100 +}
101 +
102 +/* Custom utilities */
103 +.text-gradient {
104 + @apply bg-clip-text text-transparent bg-gradient-to-r from-green-600 to-blue-600;
105 +}
106 +
107 +.card-shadow {
108 + box-shadow: 0 8px 20px -8px rgba(0, 0, 0, 0.1);
109 +}
110 +
111 +/* Price tag */
112 +.price-tag {
113 + @apply text-orange-500 font-semibold;
114 +}
115 +
116 +
117 +*, ::before, ::after {
118 + --tw-border-spacing-x: 0;
119 + --tw-border-spacing-y: 0;
120 + --tw-translate-x: 0;
121 + --tw-translate-y: 0;
122 + --tw-rotate: 0;
123 + --tw-skew-x: 0;
124 + --tw-skew-y: 0;
125 + --tw-scale-x: 1;
126 + --tw-scale-y: 1;
127 + --tw-pan-x: ;
128 + --tw-pan-y: ;
129 + --tw-pinch-zoom: ;
130 + --tw-scroll-snap-strictness: proximity;
131 + --tw-gradient-from-position: ;
132 + --tw-gradient-via-position: ;
133 + --tw-gradient-to-position: ;
134 + --tw-ordinal: ;
135 + --tw-slashed-zero: ;
136 + --tw-numeric-figure: ;
137 + --tw-numeric-spacing: ;
138 + --tw-numeric-fraction: ;
139 + --tw-ring-inset: ;
140 + --tw-ring-offset-width: 0px;
141 + --tw-ring-offset-color: #fff;
142 + --tw-ring-color: rgb(59 130 246 / 0.5);
143 + --tw-ring-offset-shadow: 0 0 #0000;
144 + --tw-ring-shadow: 0 0 #0000;
145 + --tw-shadow: 0 0 #0000;
146 + --tw-shadow-colored: 0 0 #0000;
147 + --tw-blur: ;
148 + --tw-brightness: ;
149 + --tw-contrast: ;
150 + --tw-grayscale: ;
151 + --tw-hue-rotate: ;
152 + --tw-invert: ;
153 + --tw-saturate: ;
154 + --tw-sepia: ;
155 + --tw-drop-shadow: ;
156 + --tw-backdrop-blur: ;
157 + --tw-backdrop-brightness: ;
158 + --tw-backdrop-contrast: ;
159 + --tw-backdrop-grayscale: ;
160 + --tw-backdrop-hue-rotate: ;
161 + --tw-backdrop-invert: ;
162 + --tw-backdrop-opacity: ;
163 + --tw-backdrop-saturate: ;
164 + --tw-backdrop-sepia: ;
165 + --tw-contain-size: ;
166 + --tw-contain-layout: ;
167 + --tw-contain-paint: ;
168 + --tw-contain-style: ;
169 +}
170 +
171 +::backdrop {
172 + --tw-border-spacing-x: 0;
173 + --tw-border-spacing-y: 0;
174 + --tw-translate-x: 0;
175 + --tw-translate-y: 0;
176 + --tw-rotate: 0;
177 + --tw-skew-x: 0;
178 + --tw-skew-y: 0;
179 + --tw-scale-x: 1;
180 + --tw-scale-y: 1;
181 + --tw-pan-x: ;
182 + --tw-pan-y: ;
183 + --tw-pinch-zoom: ;
184 + --tw-scroll-snap-strictness: proximity;
185 + --tw-gradient-from-position: ;
186 + --tw-gradient-via-position: ;
187 + --tw-gradient-to-position: ;
188 + --tw-ordinal: ;
189 + --tw-slashed-zero: ;
190 + --tw-numeric-figure: ;
191 + --tw-numeric-spacing: ;
192 + --tw-numeric-fraction: ;
193 + --tw-ring-inset: ;
194 + --tw-ring-offset-width: 0px;
195 + --tw-ring-offset-color: #fff;
196 + --tw-ring-color: rgb(59 130 246 / 0.5);
197 + --tw-ring-offset-shadow: 0 0 #0000;
198 + --tw-ring-shadow: 0 0 #0000;
199 + --tw-shadow: 0 0 #0000;
200 + --tw-shadow-colored: 0 0 #0000;
201 + --tw-blur: ;
202 + --tw-brightness: ;
203 + --tw-contrast: ;
204 + --tw-grayscale: ;
205 + --tw-hue-rotate: ;
206 + --tw-invert: ;
207 + --tw-saturate: ;
208 + --tw-sepia: ;
209 + --tw-drop-shadow: ;
210 + --tw-backdrop-blur: ;
211 + --tw-backdrop-brightness: ;
212 + --tw-backdrop-contrast: ;
213 + --tw-backdrop-grayscale: ;
214 + --tw-backdrop-hue-rotate: ;
215 + --tw-backdrop-invert: ;
216 + --tw-backdrop-opacity: ;
217 + --tw-backdrop-saturate: ;
218 + --tw-backdrop-sepia: ;
219 + --tw-contain-size: ;
220 + --tw-contain-layout: ;
221 + --tw-contain-paint: ;
222 + --tw-contain-style: ;
223 +}/*
224 +! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com
225 +*//*
226 +1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
227 +2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
228 +*/
229 +
230 +*,
231 +::before,
232 +::after {
233 + box-sizing: border-box; /* 1 */
234 + border-width: 0; /* 2 */
235 + border-style: solid; /* 2 */
236 + border-color: #e5e7eb; /* 2 */
237 +}
238 +
239 +::before,
240 +::after {
241 + --tw-content: '';
242 +}
243 +
244 +/*
245 +1. Use a consistent sensible line-height in all browsers.
246 +2. Prevent adjustments of font size after orientation changes in iOS.
247 +3. Use a more readable tab size.
248 +4. Use the user's configured `sans` font-family by default.
249 +5. Use the user's configured `sans` font-feature-settings by default.
250 +6. Use the user's configured `sans` font-variation-settings by default.
251 +7. Disable tap highlights on iOS
252 +*/
253 +
254 +html,
255 +:host {
256 + line-height: 1.5; /* 1 */
257 + -webkit-text-size-adjust: 100%; /* 2 */
258 + -moz-tab-size: 4; /* 3 */
259 + -o-tab-size: 4;
260 + tab-size: 4; /* 3 */
261 + font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; /* 4 */
262 + font-feature-settings: normal; /* 5 */
263 + font-variation-settings: normal; /* 6 */
264 + -webkit-tap-highlight-color: transparent; /* 7 */
265 +}
266 +
267 +/*
268 +1. Remove the margin in all browsers.
269 +2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
270 +*/
271 +
272 +body {
273 + margin: 0; /* 1 */
274 + line-height: inherit; /* 2 */
275 +}
276 +
277 +/*
278 +1. Add the correct height in Firefox.
279 +2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
280 +3. Ensure horizontal rules are visible by default.
281 +*/
282 +
283 +hr {
284 + height: 0; /* 1 */
285 + color: inherit; /* 2 */
286 + border-top-width: 1px; /* 3 */
287 +}
288 +
289 +/*
290 +Add the correct text decoration in Chrome, Edge, and Safari.
291 +*/
292 +
293 +abbr:where([title]) {
294 + -webkit-text-decoration: underline dotted;
295 + text-decoration: underline dotted;
296 +}
297 +
298 +/*
299 +Remove the default font size and weight for headings.
300 +*/
301 +
302 +h1,
303 +h2,
304 +h3,
305 +h4,
306 +h5,
307 +h6 {
308 + font-size: inherit;
309 + font-weight: inherit;
310 +}
311 +
312 +/*
313 +Reset links to optimize for opt-in styling instead of opt-out.
314 +*/
315 +
316 +a {
317 + color: inherit;
318 + text-decoration: inherit;
319 +}
320 +
321 +/*
322 +Add the correct font weight in Edge and Safari.
323 +*/
324 +
325 +b,
326 +strong {
327 + font-weight: bolder;
328 +}
329 +
330 +/*
331 +1. Use the user's configured `mono` font-family by default.
332 +2. Use the user's configured `mono` font-feature-settings by default.
333 +3. Use the user's configured `mono` font-variation-settings by default.
334 +4. Correct the odd `em` font sizing in all browsers.
335 +*/
336 +
337 +code,
338 +kbd,
339 +samp,
340 +pre {
341 + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; /* 1 */
342 + font-feature-settings: normal; /* 2 */
343 + font-variation-settings: normal; /* 3 */
344 + font-size: 1em; /* 4 */
345 +}
346 +
347 +/*
348 +Add the correct font size in all browsers.
349 +*/
350 +
351 +small {
352 + font-size: 80%;
353 +}
354 +
355 +/*
356 +Prevent `sub` and `sup` elements from affecting the line height in all browsers.
357 +*/
358 +
359 +sub,
360 +sup {
361 + font-size: 75%;
362 + line-height: 0;
363 + position: relative;
364 + vertical-align: baseline;
365 +}
366 +
367 +sub {
368 + bottom: -0.25em;
369 +}
370 +
371 +sup {
372 + top: -0.5em;
373 +}
374 +
375 +/*
376 +1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
377 +2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
378 +3. Remove gaps between table borders by default.
379 +*/
380 +
381 +table {
382 + text-indent: 0; /* 1 */
383 + border-color: inherit; /* 2 */
384 + border-collapse: collapse; /* 3 */
385 +}
386 +
387 +/*
388 +1. Change the font styles in all browsers.
389 +2. Remove the margin in Firefox and Safari.
390 +3. Remove default padding in all browsers.
391 +*/
392 +
393 +button,
394 +input,
395 +optgroup,
396 +select,
397 +textarea {
398 + font-family: inherit; /* 1 */
399 + font-feature-settings: inherit; /* 1 */
400 + font-variation-settings: inherit; /* 1 */
401 + font-size: 100%; /* 1 */
402 + font-weight: inherit; /* 1 */
403 + line-height: inherit; /* 1 */
404 + letter-spacing: inherit; /* 1 */
405 + color: inherit; /* 1 */
406 + margin: 0; /* 2 */
407 + padding: 0; /* 3 */
408 +}
409 +
410 +/*
411 +Remove the inheritance of text transform in Edge and Firefox.
412 +*/
413 +
414 +button,
415 +select {
416 + text-transform: none;
417 +}
418 +
419 +/*
420 +1. Correct the inability to style clickable types in iOS and Safari.
421 +2. Remove default button styles.
422 +*/
423 +
424 +button,
425 +input:where([type='button']),
426 +input:where([type='reset']),
427 +input:where([type='submit']) {
428 + -webkit-appearance: button; /* 1 */
429 + background-color: transparent; /* 2 */
430 + background-image: none; /* 2 */
431 +}
432 +
433 +/*
434 +Use the modern Firefox focus style for all focusable elements.
435 +*/
436 +
437 +:-moz-focusring {
438 + outline: auto;
439 +}
440 +
441 +/*
442 +Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
443 +*/
444 +
445 +:-moz-ui-invalid {
446 + box-shadow: none;
447 +}
448 +
449 +/*
450 +Add the correct vertical alignment in Chrome and Firefox.
451 +*/
452 +
453 +progress {
454 + vertical-align: baseline;
455 +}
456 +
457 +/*
458 +Correct the cursor style of increment and decrement buttons in Safari.
459 +*/
460 +
461 +::-webkit-inner-spin-button,
462 +::-webkit-outer-spin-button {
463 + height: auto;
464 +}
465 +
466 +/*
467 +1. Correct the odd appearance in Chrome and Safari.
468 +2. Correct the outline style in Safari.
469 +*/
470 +
471 +[type='search'] {
472 + -webkit-appearance: textfield; /* 1 */
473 + outline-offset: -2px; /* 2 */
474 +}
475 +
476 +/*
477 +Remove the inner padding in Chrome and Safari on macOS.
478 +*/
479 +
480 +::-webkit-search-decoration {
481 + -webkit-appearance: none;
482 +}
483 +
484 +/*
485 +1. Correct the inability to style clickable types in iOS and Safari.
486 +2. Change font properties to `inherit` in Safari.
487 +*/
488 +
489 +::-webkit-file-upload-button {
490 + -webkit-appearance: button; /* 1 */
491 + font: inherit; /* 2 */
492 +}
493 +
494 +/*
495 +Add the correct display in Chrome and Safari.
496 +*/
497 +
498 +summary {
499 + display: list-item;
500 +}
501 +
502 +/*
503 +Removes the default spacing and border for appropriate elements.
504 +*/
505 +
506 +blockquote,
507 +dl,
508 +dd,
509 +h1,
510 +h2,
511 +h3,
512 +h4,
513 +h5,
514 +h6,
515 +hr,
516 +figure,
517 +p,
518 +pre {
519 + margin: 0;
520 +}
521 +
522 +fieldset {
523 + margin: 0;
524 + padding: 0;
525 +}
526 +
527 +legend {
528 + padding: 0;
529 +}
530 +
531 +ol,
532 +ul,
533 +menu {
534 + list-style: none;
535 + margin: 0;
536 + padding: 0;
537 +}
538 +
539 +/*
540 +Reset default styling for dialogs.
541 +*/
542 +dialog {
543 + padding: 0;
544 +}
545 +
546 +/*
547 +Prevent resizing textareas horizontally by default.
548 +*/
549 +
550 +textarea {
551 + resize: vertical;
552 +}
553 +
554 +/*
555 +1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
556 +2. Set the default placeholder color to the user's configured gray 400 color.
557 +*/
558 +
559 +input::-moz-placeholder, textarea::-moz-placeholder {
560 + opacity: 1; /* 1 */
561 + color: #9ca3af; /* 2 */
562 +}
563 +
564 +input::placeholder,
565 +textarea::placeholder {
566 + opacity: 1; /* 1 */
567 + color: #9ca3af; /* 2 */
568 +}
569 +
570 +/*
571 +Set the default cursor for buttons.
572 +*/
573 +
574 +button,
575 +[role="button"] {
576 + cursor: pointer;
577 +}
578 +
579 +/*
580 +Make sure disabled buttons don't get the pointer cursor.
581 +*/
582 +:disabled {
583 + cursor: default;
584 +}
585 +
586 +/*
587 +1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
588 +2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
589 + This can trigger a poorly considered lint error in some tools but is included by design.
590 +*/
591 +
592 +img,
593 +svg,
594 +video,
595 +canvas,
596 +audio,
597 +iframe,
598 +embed,
599 +object {
600 + display: block; /* 1 */
601 + vertical-align: middle; /* 2 */
602 +}
603 +
604 +/*
605 +Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
606 +*/
607 +
608 +img,
609 +video {
610 + max-width: 100%;
611 + height: auto;
612 +}
613 +
614 +/* Make elements with the HTML hidden attribute stay hidden by default */
615 +[hidden]:where(:not([hidden="until-found"])) {
616 + display: none;
617 +}
618 +.container {
619 + width: 100%;
620 +}
621 +@media (min-width: 640px) {
622 +
623 + .container {
624 + max-width: 640px;
625 + }
626 +}
627 +@media (min-width: 768px) {
628 +
629 + .container {
630 + max-width: 768px;
631 + }
632 +}
633 +@media (min-width: 1024px) {
634 +
635 + .container {
636 + max-width: 1024px;
637 + }
638 +}
639 +@media (min-width: 1280px) {
640 +
641 + .container {
642 + max-width: 1280px;
643 + }
644 +}
645 +@media (min-width: 1536px) {
646 +
647 + .container {
648 + max-width: 1536px;
649 + }
650 +}
651 +.fixed {
652 + position: fixed;
653 +}
654 +.absolute {
655 + position: absolute;
656 +}
657 +.relative {
658 + position: relative;
659 +}
660 +.sticky {
661 + position: sticky;
662 +}
663 +.inset-0 {
664 + inset: 0px;
665 +}
666 +.bottom-0 {
667 + bottom: 0px;
668 +}
669 +.bottom-16 {
670 + bottom: 4rem;
671 +}
672 +.bottom-2 {
673 + bottom: 0.5rem;
674 +}
675 +.left-0 {
676 + left: 0px;
677 +}
678 +.left-2 {
679 + left: 0.5rem;
680 +}
681 +.right-0 {
682 + right: 0px;
683 +}
684 +.right-2 {
685 + right: 0.5rem;
686 +}
687 +.top-0 {
688 + top: 0px;
689 +}
690 +.top-2 {
691 + top: 0.5rem;
692 +}
693 +.z-0 {
694 + z-index: 0;
695 +}
696 +.z-10 {
697 + z-index: 10;
698 +}
699 +.z-50 {
700 + z-index: 50;
701 +}
702 +.mx-1 {
703 + margin-left: 0.25rem;
704 + margin-right: 0.25rem;
705 +}
706 +.mx-2 {
707 + margin-left: 0.5rem;
708 + margin-right: 0.5rem;
709 +}
710 +.mx-4 {
711 + margin-left: 1rem;
712 + margin-right: 1rem;
713 +}
714 +.mx-auto {
715 + margin-left: auto;
716 + margin-right: auto;
717 +}
718 +.my-3 {
719 + margin-top: 0.75rem;
720 + margin-bottom: 0.75rem;
721 +}
722 +.mb-1 {
723 + margin-bottom: 0.25rem;
724 +}
725 +.mb-2 {
726 + margin-bottom: 0.5rem;
727 +}
728 +.mb-3 {
729 + margin-bottom: 0.75rem;
730 +}
731 +.mb-4 {
732 + margin-bottom: 1rem;
733 +}
734 +.mb-5 {
735 + margin-bottom: 1.25rem;
736 +}
737 +.mb-6 {
738 + margin-bottom: 1.5rem;
739 +}
740 +.mb-7 {
741 + margin-bottom: 1.75rem;
742 +}
743 +.ml-1 {
744 + margin-left: 0.25rem;
745 +}
746 +.ml-2 {
747 + margin-left: 0.5rem;
748 +}
749 +.ml-auto {
750 + margin-left: auto;
751 +}
752 +.mr-1 {
753 + margin-right: 0.25rem;
754 +}
755 +.mr-2 {
756 + margin-right: 0.5rem;
757 +}
758 +.mr-3 {
759 + margin-right: 0.75rem;
760 +}
761 +.mr-4 {
762 + margin-right: 1rem;
763 +}
764 +.mt-1 {
765 + margin-top: 0.25rem;
766 +}
767 +.mt-2 {
768 + margin-top: 0.5rem;
769 +}
770 +.mt-3 {
771 + margin-top: 0.75rem;
772 +}
773 +.mt-4 {
774 + margin-top: 1rem;
775 +}
776 +.mt-5 {
777 + margin-top: 1.25rem;
778 +}
779 +.mt-6 {
780 + margin-top: 1.5rem;
781 +}
782 +.mt-8 {
783 + margin-top: 2rem;
784 +}
785 +.mt-auto {
786 + margin-top: auto;
787 +}
788 +.line-clamp-1 {
789 + overflow: hidden;
790 + display: -webkit-box;
791 + -webkit-box-orient: vertical;
792 + -webkit-line-clamp: 1;
793 +}
794 +.line-clamp-2 {
795 + overflow: hidden;
796 + display: -webkit-box;
797 + -webkit-box-orient: vertical;
798 + -webkit-line-clamp: 2;
799 +}
800 +.block {
801 + display: block;
802 +}
803 +.inline-block {
804 + display: inline-block;
805 +}
806 +.flex {
807 + display: flex;
808 +}
809 +.grid {
810 + display: grid;
811 +}
812 +.h-1\.5 {
813 + height: 0.375rem;
814 +}
815 +.h-10 {
816 + height: 2.5rem;
817 +}
818 +.h-12 {
819 + height: 3rem;
820 +}
821 +.h-14 {
822 + height: 3.5rem;
823 +}
824 +.h-16 {
825 + height: 4rem;
826 +}
827 +.h-2 {
828 + height: 0.5rem;
829 +}
830 +.h-20 {
831 + height: 5rem;
832 +}
833 +.h-24 {
834 + height: 6rem;
835 +}
836 +.h-28 {
837 + height: 7rem;
838 +}
839 +.h-3 {
840 + height: 0.75rem;
841 +}
842 +.h-3\.5 {
843 + height: 0.875rem;
844 +}
845 +.h-32 {
846 + height: 8rem;
847 +}
848 +.h-4 {
849 + height: 1rem;
850 +}
851 +.h-40 {
852 + height: 10rem;
853 +}
854 +.h-48 {
855 + height: 12rem;
856 +}
857 +.h-5 {
858 + height: 1.25rem;
859 +}
860 +.h-56 {
861 + height: 14rem;
862 +}
863 +.h-6 {
864 + height: 1.5rem;
865 +}
866 +.h-64 {
867 + height: 16rem;
868 +}
869 +.h-auto {
870 + height: auto;
871 +}
872 +.h-full {
873 + height: 100%;
874 +}
875 +.h-screen {
876 + height: 100vh;
877 +}
878 +.max-h-60 {
879 + max-height: 15rem;
880 +}
881 +.min-h-screen {
882 + min-height: 100vh;
883 +}
884 +.w-1\/3 {
885 + width: 33.333333%;
886 +}
887 +.w-1\/4 {
888 + width: 25%;
889 +}
890 +.w-10 {
891 + width: 2.5rem;
892 +}
893 +.w-12 {
894 + width: 3rem;
895 +}
896 +.w-14 {
897 + width: 3.5rem;
898 +}
899 +.w-16 {
900 + width: 4rem;
901 +}
902 +.w-2 {
903 + width: 0.5rem;
904 +}
905 +.w-20 {
906 + width: 5rem;
907 +}
908 +.w-24 {
909 + width: 6rem;
910 +}
911 +.w-3 {
912 + width: 0.75rem;
913 +}
914 +.w-3\.5 {
915 + width: 0.875rem;
916 +}
917 +.w-4 {
918 + width: 1rem;
919 +}
920 +.w-5 {
921 + width: 1.25rem;
922 +}
923 +.w-6 {
924 + width: 1.5rem;
925 +}
926 +.w-fit {
927 + width: -moz-fit-content;
928 + width: fit-content;
929 +}
930 +.w-full {
931 + width: 100%;
932 +}
933 +.min-w-\[18px\] {
934 + min-width: 18px;
935 +}
936 +.flex-1 {
937 + flex: 1 1 0%;
938 +}
939 +.flex-shrink-0 {
940 + flex-shrink: 0;
941 +}
942 +@keyframes pulse {
943 +
944 + 50% {
945 + opacity: .5;
946 + }
947 +}
948 +.animate-pulse {
949 + animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
950 +}
951 +@keyframes spin {
952 +
953 + to {
954 + transform: rotate(360deg);
955 + }
956 +}
957 +.animate-spin {
958 + animation: spin 1s linear infinite;
959 +}
960 +.cursor-not-allowed {
961 + cursor: not-allowed;
962 +}
963 +.cursor-pointer {
964 + cursor: pointer;
965 +}
966 +.resize-none {
967 + resize: none;
968 +}
969 +.snap-x {
970 + scroll-snap-type: x var(--tw-scroll-snap-strictness);
971 +}
972 +.snap-mandatory {
973 + --tw-scroll-snap-strictness: mandatory;
974 +}
975 +.snap-center {
976 + scroll-snap-align: center;
977 +}
978 +.list-decimal {
979 + list-style-type: decimal;
980 +}
981 +.list-disc {
982 + list-style-type: disc;
983 +}
984 +.grid-cols-1 {
985 + grid-template-columns: repeat(1, minmax(0, 1fr));
986 +}
987 +.grid-cols-2 {
988 + grid-template-columns: repeat(2, minmax(0, 1fr));
989 +}
990 +.grid-cols-4 {
991 + grid-template-columns: repeat(4, minmax(0, 1fr));
992 +}
993 +.flex-col {
994 + flex-direction: column;
995 +}
996 +.flex-wrap {
997 + flex-wrap: wrap;
998 +}
999 +.items-start {
1000 + align-items: flex-start;
1001 +}
1002 +.items-end {
1003 + align-items: flex-end;
1004 +}
1005 +.items-center {
1006 + align-items: center;
1007 +}
1008 +.items-baseline {
1009 + align-items: baseline;
1010 +}
1011 +.justify-end {
1012 + justify-content: flex-end;
1013 +}
1014 +.justify-center {
1015 + justify-content: center;
1016 +}
1017 +.justify-between {
1018 + justify-content: space-between;
1019 +}
1020 +.justify-around {
1021 + justify-content: space-around;
1022 +}
1023 +.gap-2 {
1024 + gap: 0.5rem;
1025 +}
1026 +.gap-3 {
1027 + gap: 0.75rem;
1028 +}
1029 +.gap-4 {
1030 + gap: 1rem;
1031 +}
1032 +.-space-x-2 > :not([hidden]) ~ :not([hidden]) {
1033 + --tw-space-x-reverse: 0;
1034 + margin-right: calc(-0.5rem * var(--tw-space-x-reverse));
1035 + margin-left: calc(-0.5rem * calc(1 - var(--tw-space-x-reverse)));
1036 +}
1037 +.space-x-2 > :not([hidden]) ~ :not([hidden]) {
1038 + --tw-space-x-reverse: 0;
1039 + margin-right: calc(0.5rem * var(--tw-space-x-reverse));
1040 + margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse)));
1041 +}
1042 +.space-x-3 > :not([hidden]) ~ :not([hidden]) {
1043 + --tw-space-x-reverse: 0;
1044 + margin-right: calc(0.75rem * var(--tw-space-x-reverse));
1045 + margin-left: calc(0.75rem * calc(1 - var(--tw-space-x-reverse)));
1046 +}
1047 +.space-x-4 > :not([hidden]) ~ :not([hidden]) {
1048 + --tw-space-x-reverse: 0;
1049 + margin-right: calc(1rem * var(--tw-space-x-reverse));
1050 + margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse)));
1051 +}
1052 +.space-x-6 > :not([hidden]) ~ :not([hidden]) {
1053 + --tw-space-x-reverse: 0;
1054 + margin-right: calc(1.5rem * var(--tw-space-x-reverse));
1055 + margin-left: calc(1.5rem * calc(1 - var(--tw-space-x-reverse)));
1056 +}
1057 +.space-x-8 > :not([hidden]) ~ :not([hidden]) {
1058 + --tw-space-x-reverse: 0;
1059 + margin-right: calc(2rem * var(--tw-space-x-reverse));
1060 + margin-left: calc(2rem * calc(1 - var(--tw-space-x-reverse)));
1061 +}
1062 +.space-y-1 > :not([hidden]) ~ :not([hidden]) {
1063 + --tw-space-y-reverse: 0;
1064 + margin-top: calc(0.25rem * calc(1 - var(--tw-space-y-reverse)));
1065 + margin-bottom: calc(0.25rem * var(--tw-space-y-reverse));
1066 +}
1067 +.space-y-2 > :not([hidden]) ~ :not([hidden]) {
1068 + --tw-space-y-reverse: 0;
1069 + margin-top: calc(0.5rem * calc(1 - var(--tw-space-y-reverse)));
1070 + margin-bottom: calc(0.5rem * var(--tw-space-y-reverse));
1071 +}
1072 +.space-y-3 > :not([hidden]) ~ :not([hidden]) {
1073 + --tw-space-y-reverse: 0;
1074 + margin-top: calc(0.75rem * calc(1 - var(--tw-space-y-reverse)));
1075 + margin-bottom: calc(0.75rem * var(--tw-space-y-reverse));
1076 +}
1077 +.space-y-4 > :not([hidden]) ~ :not([hidden]) {
1078 + --tw-space-y-reverse: 0;
1079 + margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse)));
1080 + margin-bottom: calc(1rem * var(--tw-space-y-reverse));
1081 +}
1082 +.space-y-6 > :not([hidden]) ~ :not([hidden]) {
1083 + --tw-space-y-reverse: 0;
1084 + margin-top: calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));
1085 + margin-bottom: calc(1.5rem * var(--tw-space-y-reverse));
1086 +}
1087 +.overflow-hidden {
1088 + overflow: hidden;
1089 +}
1090 +.overflow-x-auto {
1091 + overflow-x: auto;
1092 +}
1093 +.overflow-x-scroll {
1094 + overflow-x: scroll;
1095 +}
1096 +.whitespace-nowrap {
1097 + white-space: nowrap;
1098 +}
1099 +.whitespace-pre-line {
1100 + white-space: pre-line;
1101 +}
1102 +.rounded {
1103 + border-radius: 0.25rem;
1104 +}
1105 +.rounded-full {
1106 + border-radius: 9999px;
1107 +}
1108 +.rounded-lg {
1109 + border-radius: 0.5rem;
1110 +}
1111 +.rounded-md {
1112 + border-radius: 0.375rem;
1113 +}
1114 +.rounded-xl {
1115 + border-radius: 0.75rem;
1116 +}
1117 +.rounded-b-3xl {
1118 + border-bottom-right-radius: 1.5rem;
1119 + border-bottom-left-radius: 1.5rem;
1120 +}
1121 +.border {
1122 + border-width: 1px;
1123 +}
1124 +.border-2 {
1125 + border-width: 2px;
1126 +}
1127 +.border-4 {
1128 + border-width: 4px;
1129 +}
1130 +.border-b {
1131 + border-bottom-width: 1px;
1132 +}
1133 +.border-b-2 {
1134 + border-bottom-width: 2px;
1135 +}
1136 +.border-l-2 {
1137 + border-left-width: 2px;
1138 +}
1139 +.border-r {
1140 + border-right-width: 1px;
1141 +}
1142 +.border-t {
1143 + border-top-width: 1px;
1144 +}
1145 +.border-none {
1146 + border-style: none;
1147 +}
1148 +.border-gray-100 {
1149 + --tw-border-opacity: 1;
1150 + border-color: rgb(243 244 246 / var(--tw-border-opacity, 1));
1151 +}
1152 +.border-gray-200 {
1153 + --tw-border-opacity: 1;
1154 + border-color: rgb(229 231 235 / var(--tw-border-opacity, 1));
1155 +}
1156 +.border-gray-300 {
1157 + --tw-border-opacity: 1;
1158 + border-color: rgb(209 213 219 / var(--tw-border-opacity, 1));
1159 +}
1160 +.border-green-200 {
1161 + --tw-border-opacity: 1;
1162 + border-color: rgb(187 247 208 / var(--tw-border-opacity, 1));
1163 +}
1164 +.border-green-500 {
1165 + --tw-border-opacity: 1;
1166 + border-color: rgb(34 197 94 / var(--tw-border-opacity, 1));
1167 +}
1168 +.border-green-600 {
1169 + --tw-border-opacity: 1;
1170 + border-color: rgb(22 163 74 / var(--tw-border-opacity, 1));
1171 +}
1172 +.border-red-400 {
1173 + --tw-border-opacity: 1;
1174 + border-color: rgb(248 113 113 / var(--tw-border-opacity, 1));
1175 +}
1176 +.border-transparent {
1177 + border-color: transparent;
1178 +}
1179 +.border-white {
1180 + --tw-border-opacity: 1;
1181 + border-color: rgb(255 255 255 / var(--tw-border-opacity, 1));
1182 +}
1183 +.border-white\/30 {
1184 + border-color: rgb(255 255 255 / 0.3);
1185 +}
1186 +.border-t-transparent {
1187 + border-top-color: transparent;
1188 +}
1189 +.bg-amber-500\/90 {
1190 + background-color: rgb(245 158 11 / 0.9);
1191 +}
1192 +.bg-blue-100 {
1193 + --tw-bg-opacity: 1;
1194 + background-color: rgb(219 234 254 / var(--tw-bg-opacity, 1));
1195 +}
1196 +.bg-gray-100 {
1197 + --tw-bg-opacity: 1;
1198 + background-color: rgb(243 244 246 / var(--tw-bg-opacity, 1));
1199 +}
1200 +.bg-gray-200 {
1201 + --tw-bg-opacity: 1;
1202 + background-color: rgb(229 231 235 / var(--tw-bg-opacity, 1));
1203 +}
1204 +.bg-gray-300 {
1205 + --tw-bg-opacity: 1;
1206 + background-color: rgb(209 213 219 / var(--tw-bg-opacity, 1));
1207 +}
1208 +.bg-gray-400 {
1209 + --tw-bg-opacity: 1;
1210 + background-color: rgb(156 163 175 / var(--tw-bg-opacity, 1));
1211 +}
1212 +.bg-gray-500 {
1213 + --tw-bg-opacity: 1;
1214 + background-color: rgb(107 114 128 / var(--tw-bg-opacity, 1));
1215 +}
1216 +.bg-green-100 {
1217 + --tw-bg-opacity: 1;
1218 + background-color: rgb(220 252 231 / var(--tw-bg-opacity, 1));
1219 +}
1220 +.bg-green-50 {
1221 + --tw-bg-opacity: 1;
1222 + background-color: rgb(240 253 244 / var(--tw-bg-opacity, 1));
1223 +}
1224 +.bg-green-50\/50 {
1225 + background-color: rgb(240 253 244 / 0.5);
1226 +}
1227 +.bg-green-500 {
1228 + --tw-bg-opacity: 1;
1229 + background-color: rgb(34 197 94 / var(--tw-bg-opacity, 1));
1230 +}
1231 +.bg-green-600 {
1232 + --tw-bg-opacity: 1;
1233 + background-color: rgb(22 163 74 / var(--tw-bg-opacity, 1));
1234 +}
1235 +.bg-orange-100 {
1236 + --tw-bg-opacity: 1;
1237 + background-color: rgb(255 237 213 / var(--tw-bg-opacity, 1));
1238 +}
1239 +.bg-orange-500\/30 {
1240 + background-color: rgb(249 115 22 / 0.3);
1241 +}
1242 +.bg-purple-100 {
1243 + --tw-bg-opacity: 1;
1244 + background-color: rgb(243 232 255 / var(--tw-bg-opacity, 1));
1245 +}
1246 +.bg-red-100 {
1247 + --tw-bg-opacity: 1;
1248 + background-color: rgb(254 226 226 / var(--tw-bg-opacity, 1));
1249 +}
1250 +.bg-red-500 {
1251 + --tw-bg-opacity: 1;
1252 + background-color: rgb(239 68 68 / var(--tw-bg-opacity, 1));
1253 +}
1254 +.bg-red-500\/90 {
1255 + background-color: rgb(239 68 68 / 0.9);
1256 +}
1257 +.bg-transparent {
1258 + background-color: transparent;
1259 +}
1260 +.bg-white {
1261 + --tw-bg-opacity: 1;
1262 + background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1));
1263 +}
1264 +.bg-white\/10 {
1265 + background-color: rgb(255 255 255 / 0.1);
1266 +}
1267 +.bg-white\/20 {
1268 + background-color: rgb(255 255 255 / 0.2);
1269 +}
1270 +.bg-white\/30 {
1271 + background-color: rgb(255 255 255 / 0.3);
1272 +}
1273 +.bg-white\/50 {
1274 + background-color: rgb(255 255 255 / 0.5);
1275 +}
1276 +.bg-white\/70 {
1277 + background-color: rgb(255 255 255 / 0.7);
1278 +}
1279 +.bg-white\/80 {
1280 + background-color: rgb(255 255 255 / 0.8);
1281 +}
1282 +.bg-white\/90 {
1283 + background-color: rgb(255 255 255 / 0.9);
1284 +}
1285 +.bg-gradient-to-b {
1286 + background-image: linear-gradient(to bottom, var(--tw-gradient-stops));
1287 +}
1288 +.bg-gradient-to-br {
1289 + background-image: linear-gradient(to bottom right, var(--tw-gradient-stops));
1290 +}
1291 +.bg-gradient-to-r {
1292 + background-image: linear-gradient(to right, var(--tw-gradient-stops));
1293 +}
1294 +.bg-gradient-to-t {
1295 + background-image: linear-gradient(to top, var(--tw-gradient-stops));
1296 +}
1297 +.from-black\/70 {
1298 + --tw-gradient-from: rgb(0 0 0 / 0.7) var(--tw-gradient-from-position);
1299 + --tw-gradient-to: rgb(0 0 0 / 0) var(--tw-gradient-to-position);
1300 + --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
1301 +}
1302 +.from-green-50 {
1303 + --tw-gradient-from: #f0fdf4 var(--tw-gradient-from-position);
1304 + --tw-gradient-to: rgb(240 253 244 / 0) var(--tw-gradient-to-position);
1305 + --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
1306 +}
1307 +.from-green-50\/70 {
1308 + --tw-gradient-from: rgb(240 253 244 / 0.7) var(--tw-gradient-from-position);
1309 + --tw-gradient-to: rgb(240 253 244 / 0) var(--tw-gradient-to-position);
1310 + --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
1311 +}
1312 +.from-green-500 {
1313 + --tw-gradient-from: #22c55e var(--tw-gradient-from-position);
1314 + --tw-gradient-to: rgb(34 197 94 / 0) var(--tw-gradient-to-position);
1315 + --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
1316 +}
1317 +.from-green-500\/10 {
1318 + --tw-gradient-from: rgb(34 197 94 / 0.1) var(--tw-gradient-from-position);
1319 + --tw-gradient-to: rgb(34 197 94 / 0) var(--tw-gradient-to-position);
1320 + --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
1321 +}
1322 +.from-red-500 {
1323 + --tw-gradient-from: #ef4444 var(--tw-gradient-from-position);
1324 + --tw-gradient-to: rgb(239 68 68 / 0) var(--tw-gradient-to-position);
1325 + --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
1326 +}
1327 +.from-red-500\/70 {
1328 + --tw-gradient-from: rgb(239 68 68 / 0.7) var(--tw-gradient-from-position);
1329 + --tw-gradient-to: rgb(239 68 68 / 0) var(--tw-gradient-to-position);
1330 + --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
1331 +}
1332 +.from-transparent {
1333 + --tw-gradient-from: transparent var(--tw-gradient-from-position);
1334 + --tw-gradient-to: rgb(0 0 0 / 0) var(--tw-gradient-to-position);
1335 + --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
1336 +}
1337 +.from-white {
1338 + --tw-gradient-from: #fff var(--tw-gradient-from-position);
1339 + --tw-gradient-to: rgb(255 255 255 / 0) var(--tw-gradient-to-position);
1340 + --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
1341 +}
1342 +.via-black\/20 {
1343 + --tw-gradient-to: rgb(0 0 0 / 0) var(--tw-gradient-to-position);
1344 + --tw-gradient-stops: var(--tw-gradient-from), rgb(0 0 0 / 0.2) var(--tw-gradient-via-position), var(--tw-gradient-to);
1345 +}
1346 +.via-green-100\/30 {
1347 + --tw-gradient-to: rgb(220 252 231 / 0) var(--tw-gradient-to-position);
1348 + --tw-gradient-stops: var(--tw-gradient-from), rgb(220 252 231 / 0.3) var(--tw-gradient-via-position), var(--tw-gradient-to);
1349 +}
1350 +.via-green-50\/10 {
1351 + --tw-gradient-to: rgb(240 253 244 / 0) var(--tw-gradient-to-position);
1352 + --tw-gradient-stops: var(--tw-gradient-from), rgb(240 253 244 / 0.1) var(--tw-gradient-via-position), var(--tw-gradient-to);
1353 +}
1354 +.via-teal-50 {
1355 + --tw-gradient-to: rgb(240 253 250 / 0) var(--tw-gradient-to-position);
1356 + --tw-gradient-stops: var(--tw-gradient-from), #f0fdfa var(--tw-gradient-via-position), var(--tw-gradient-to);
1357 +}
1358 +.to-black\/50 {
1359 + --tw-gradient-to: rgb(0 0 0 / 0.5) var(--tw-gradient-to-position);
1360 +}
1361 +.to-black\/60 {
1362 + --tw-gradient-to: rgb(0 0 0 / 0.6) var(--tw-gradient-to-position);
1363 +}
1364 +.to-black\/70 {
1365 + --tw-gradient-to: rgb(0 0 0 / 0.7) var(--tw-gradient-to-position);
1366 +}
1367 +.to-blue-50 {
1368 + --tw-gradient-to: #eff6ff var(--tw-gradient-to-position);
1369 +}
1370 +.to-blue-50\/10 {
1371 + --tw-gradient-to: rgb(239 246 255 / 0.1) var(--tw-gradient-to-position);
1372 +}
1373 +.to-blue-50\/30 {
1374 + --tw-gradient-to: rgb(239 246 255 / 0.3) var(--tw-gradient-to-position);
1375 +}
1376 +.to-blue-500 {
1377 + --tw-gradient-to: #3b82f6 var(--tw-gradient-to-position);
1378 +}
1379 +.to-blue-500\/10 {
1380 + --tw-gradient-to: rgb(59 130 246 / 0.1) var(--tw-gradient-to-position);
1381 +}
1382 +.to-green-600 {
1383 + --tw-gradient-to: #16a34a var(--tw-gradient-to-position);
1384 +}
1385 +.to-red-600 {
1386 + --tw-gradient-to: #dc2626 var(--tw-gradient-to-position);
1387 +}
1388 +.to-red-600\/90 {
1389 + --tw-gradient-to: rgb(220 38 38 / 0.9) var(--tw-gradient-to-position);
1390 +}
1391 +.to-transparent {
1392 + --tw-gradient-to: transparent var(--tw-gradient-to-position);
1393 +}
1394 +.to-white\/90 {
1395 + --tw-gradient-to: rgb(255 255 255 / 0.9) var(--tw-gradient-to-position);
1396 +}
1397 +.bg-cover {
1398 + background-size: cover;
1399 +}
1400 +.bg-center {
1401 + background-position: center;
1402 +}
1403 +.object-cover {
1404 + -o-object-fit: cover;
1405 + object-fit: cover;
1406 +}
1407 +.p-1 {
1408 + padding: 0.25rem;
1409 +}
1410 +.p-2 {
1411 + padding: 0.5rem;
1412 +}
1413 +.p-3 {
1414 + padding: 0.75rem;
1415 +}
1416 +.p-4 {
1417 + padding: 1rem;
1418 +}
1419 +.p-6 {
1420 + padding: 1.5rem;
1421 +}
1422 +.px-1 {
1423 + padding-left: 0.25rem;
1424 + padding-right: 0.25rem;
1425 +}
1426 +.px-1\.5 {
1427 + padding-left: 0.375rem;
1428 + padding-right: 0.375rem;
1429 +}
1430 +.px-2 {
1431 + padding-left: 0.5rem;
1432 + padding-right: 0.5rem;
1433 +}
1434 +.px-3 {
1435 + padding-left: 0.75rem;
1436 + padding-right: 0.75rem;
1437 +}
1438 +.px-4 {
1439 + padding-left: 1rem;
1440 + padding-right: 1rem;
1441 +}
1442 +.px-6 {
1443 + padding-left: 1.5rem;
1444 + padding-right: 1.5rem;
1445 +}
1446 +.py-0\.5 {
1447 + padding-top: 0.125rem;
1448 + padding-bottom: 0.125rem;
1449 +}
1450 +.py-1 {
1451 + padding-top: 0.25rem;
1452 + padding-bottom: 0.25rem;
1453 +}
1454 +.py-1\.5 {
1455 + padding-top: 0.375rem;
1456 + padding-bottom: 0.375rem;
1457 +}
1458 +.py-10 {
1459 + padding-top: 2.5rem;
1460 + padding-bottom: 2.5rem;
1461 +}
1462 +.py-12 {
1463 + padding-top: 3rem;
1464 + padding-bottom: 3rem;
1465 +}
1466 +.py-2 {
1467 + padding-top: 0.5rem;
1468 + padding-bottom: 0.5rem;
1469 +}
1470 +.py-3 {
1471 + padding-top: 0.75rem;
1472 + padding-bottom: 0.75rem;
1473 +}
1474 +.py-4 {
1475 + padding-top: 1rem;
1476 + padding-bottom: 1rem;
1477 +}
1478 +.py-8 {
1479 + padding-top: 2rem;
1480 + padding-bottom: 2rem;
1481 +}
1482 +.pb-1 {
1483 + padding-bottom: 0.25rem;
1484 +}
1485 +.pb-16 {
1486 + padding-bottom: 4rem;
1487 +}
1488 +.pb-2 {
1489 + padding-bottom: 0.5rem;
1490 +}
1491 +.pb-20 {
1492 + padding-bottom: 5rem;
1493 +}
1494 +.pb-24 {
1495 + padding-bottom: 6rem;
1496 +}
1497 +.pb-3 {
1498 + padding-bottom: 0.75rem;
1499 +}
1500 +.pb-4 {
1501 + padding-bottom: 1rem;
1502 +}
1503 +.pb-8 {
1504 + padding-bottom: 2rem;
1505 +}
1506 +.pl-3 {
1507 + padding-left: 0.75rem;
1508 +}
1509 +.pl-5 {
1510 + padding-left: 1.25rem;
1511 +}
1512 +.pt-3 {
1513 + padding-top: 0.75rem;
1514 +}
1515 +.pt-4 {
1516 + padding-top: 1rem;
1517 +}
1518 +.pt-6 {
1519 + padding-top: 1.5rem;
1520 +}
1521 +.text-center {
1522 + text-align: center;
1523 +}
1524 +.text-right {
1525 + text-align: right;
1526 +}
1527 +.text-2xl {
1528 + font-size: 1.5rem;
1529 + line-height: 2rem;
1530 +}
1531 +.text-3xl {
1532 + font-size: 1.875rem;
1533 + line-height: 2.25rem;
1534 +}
1535 +.text-base {
1536 + font-size: 1rem;
1537 + line-height: 1.5rem;
1538 +}
1539 +.text-lg {
1540 + font-size: 1.125rem;
1541 + line-height: 1.75rem;
1542 +}
1543 +.text-sm {
1544 + font-size: 0.875rem;
1545 + line-height: 1.25rem;
1546 +}
1547 +.text-xl {
1548 + font-size: 1.25rem;
1549 + line-height: 1.75rem;
1550 +}
1551 +.text-xs {
1552 + font-size: 0.75rem;
1553 + line-height: 1rem;
1554 +}
1555 +.font-bold {
1556 + font-weight: 700;
1557 +}
1558 +.font-medium {
1559 + font-weight: 500;
1560 +}
1561 +.font-semibold {
1562 + font-weight: 600;
1563 +}
1564 +.text-amber-400 {
1565 + --tw-text-opacity: 1;
1566 + color: rgb(251 191 36 / var(--tw-text-opacity, 1));
1567 +}
1568 +.text-blue-500 {
1569 + --tw-text-opacity: 1;
1570 + color: rgb(59 130 246 / var(--tw-text-opacity, 1));
1571 +}
1572 +.text-blue-600 {
1573 + --tw-text-opacity: 1;
1574 + color: rgb(37 99 235 / var(--tw-text-opacity, 1));
1575 +}
1576 +.text-blue-700 {
1577 + --tw-text-opacity: 1;
1578 + color: rgb(29 78 216 / var(--tw-text-opacity, 1));
1579 +}
1580 +.text-gray-300 {
1581 + --tw-text-opacity: 1;
1582 + color: rgb(209 213 219 / var(--tw-text-opacity, 1));
1583 +}
1584 +.text-gray-400 {
1585 + --tw-text-opacity: 1;
1586 + color: rgb(156 163 175 / var(--tw-text-opacity, 1));
1587 +}
1588 +.text-gray-500 {
1589 + --tw-text-opacity: 1;
1590 + color: rgb(107 114 128 / var(--tw-text-opacity, 1));
1591 +}
1592 +.text-gray-600 {
1593 + --tw-text-opacity: 1;
1594 + color: rgb(75 85 99 / var(--tw-text-opacity, 1));
1595 +}
1596 +.text-gray-700 {
1597 + --tw-text-opacity: 1;
1598 + color: rgb(55 65 81 / var(--tw-text-opacity, 1));
1599 +}
1600 +.text-gray-800 {
1601 + --tw-text-opacity: 1;
1602 + color: rgb(31 41 55 / var(--tw-text-opacity, 1));
1603 +}
1604 +.text-gray-900 {
1605 + --tw-text-opacity: 1;
1606 + color: rgb(17 24 39 / var(--tw-text-opacity, 1));
1607 +}
1608 +.text-green-500 {
1609 + --tw-text-opacity: 1;
1610 + color: rgb(34 197 94 / var(--tw-text-opacity, 1));
1611 +}
1612 +.text-green-600 {
1613 + --tw-text-opacity: 1;
1614 + color: rgb(22 163 74 / var(--tw-text-opacity, 1));
1615 +}
1616 +.text-green-700 {
1617 + --tw-text-opacity: 1;
1618 + color: rgb(21 128 61 / var(--tw-text-opacity, 1));
1619 +}
1620 +.text-orange-100 {
1621 + --tw-text-opacity: 1;
1622 + color: rgb(255 237 213 / var(--tw-text-opacity, 1));
1623 +}
1624 +.text-orange-300 {
1625 + --tw-text-opacity: 1;
1626 + color: rgb(253 186 116 / var(--tw-text-opacity, 1));
1627 +}
1628 +.text-orange-500 {
1629 + --tw-text-opacity: 1;
1630 + color: rgb(249 115 22 / var(--tw-text-opacity, 1));
1631 +}
1632 +.text-orange-600 {
1633 + --tw-text-opacity: 1;
1634 + color: rgb(234 88 12 / var(--tw-text-opacity, 1));
1635 +}
1636 +.text-orange-700 {
1637 + --tw-text-opacity: 1;
1638 + color: rgb(194 65 12 / var(--tw-text-opacity, 1));
1639 +}
1640 +.text-pink-500 {
1641 + --tw-text-opacity: 1;
1642 + color: rgb(236 72 153 / var(--tw-text-opacity, 1));
1643 +}
1644 +.text-purple-600 {
1645 + --tw-text-opacity: 1;
1646 + color: rgb(147 51 234 / var(--tw-text-opacity, 1));
1647 +}
1648 +.text-red-500 {
1649 + --tw-text-opacity: 1;
1650 + color: rgb(239 68 68 / var(--tw-text-opacity, 1));
1651 +}
1652 +.text-red-700 {
1653 + --tw-text-opacity: 1;
1654 + color: rgb(185 28 28 / var(--tw-text-opacity, 1));
1655 +}
1656 +.text-white {
1657 + --tw-text-opacity: 1;
1658 + color: rgb(255 255 255 / var(--tw-text-opacity, 1));
1659 +}
1660 +.text-white\/80 {
1661 + color: rgb(255 255 255 / 0.8);
1662 +}
1663 +.text-white\/90 {
1664 + color: rgb(255 255 255 / 0.9);
1665 +}
1666 +.text-yellow-400 {
1667 + --tw-text-opacity: 1;
1668 + color: rgb(250 204 21 / var(--tw-text-opacity, 1));
1669 +}
1670 +.line-through {
1671 + text-decoration-line: line-through;
1672 +}
1673 +.placeholder-gray-400::-moz-placeholder {
1674 + --tw-placeholder-opacity: 1;
1675 + color: rgb(156 163 175 / var(--tw-placeholder-opacity, 1));
1676 +}
1677 +.placeholder-gray-400::placeholder {
1678 + --tw-placeholder-opacity: 1;
1679 + color: rgb(156 163 175 / var(--tw-placeholder-opacity, 1));
1680 +}
1681 +.opacity-15 {
1682 + opacity: 0.15;
1683 +}
1684 +.opacity-70 {
1685 + opacity: 0.7;
1686 +}
1687 +.shadow-lg {
1688 + --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
1689 + --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);
1690 + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
1691 +}
1692 +.shadow-md {
1693 + --tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
1694 + --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);
1695 + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
1696 +}
1697 +.shadow-sm {
1698 + --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
1699 + --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);
1700 + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
1701 +}
1702 +.outline-none {
1703 + outline: 2px solid transparent;
1704 + outline-offset: 2px;
1705 +}
1706 +.blur {
1707 + --tw-blur: blur(8px);
1708 + filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
1709 +}
1710 +.drop-shadow-md {
1711 + --tw-drop-shadow: drop-shadow(0 4px 3px rgb(0 0 0 / 0.07)) drop-shadow(0 2px 2px rgb(0 0 0 / 0.06));
1712 + filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
1713 +}
1714 +.drop-shadow-sm {
1715 + --tw-drop-shadow: drop-shadow(0 1px 1px rgb(0 0 0 / 0.05));
1716 + filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
1717 +}
1718 +.filter {
1719 + filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
1720 +}
1721 +.backdrop-blur-lg {
1722 + --tw-backdrop-blur: blur(16px);
1723 + -webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);
1724 + backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);
1725 +}
1726 +.backdrop-blur-md {
1727 + --tw-backdrop-blur: blur(12px);
1728 + -webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);
1729 + backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);
1730 +}
1731 +.backdrop-blur-sm {
1732 + --tw-backdrop-blur: blur(4px);
1733 + -webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);
1734 + backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);
1735 +}
1736 +.backdrop-filter {
1737 + -webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);
1738 + backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);
1739 +}
1740 +.transition-colors {
1741 + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
1742 + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
1743 + transition-duration: 150ms;
1744 +}
1745 +/* Custom fonts */
1746 +/* Base styles */
1747 +:root {
1748 + --primary-color: #4caf50;
1749 + --secondary-color: #2196f3;
1750 + --background-start: #f0f9ef;
1751 + --background-end: #e6f4ff;
1752 + --text-color: #333333;
1753 + --text-light: #6e6e6e;
1754 +}
1755 +body {
1756 + font-family: 'Noto Sans SC', sans-serif;
1757 + --tw-text-opacity: 1;
1758 + color: rgb(31 41 55 / var(--tw-text-opacity, 1));
1759 +}
1760 +/* Scrollbar styling */
1761 +::-webkit-scrollbar {
1762 + width: 5px;
1763 + height: 5px;
1764 +}
1765 +::-webkit-scrollbar-track {
1766 + background-color: transparent;
1767 +}
1768 +::-webkit-scrollbar-thumb {
1769 + border-radius: 25px;
1770 + -webkit-transition: all 0.3s;
1771 + transition: all 0.3s;
1772 + background-color: rgba(106, 115, 125, 0.2);
1773 + &:hover {
1774 + background-color: rgba(106, 115, 125, 0.27);
1775 + }
1776 +}
1777 +::-webkit-scrollbar-corner {
1778 + display: none;
1779 +}
1780 +/* Custom frosted glass components */
1781 +.frosted-glass {
1782 + border-radius: 0.75rem;
1783 + border-width: 1px;
1784 + border-color: rgb(255 255 255 / 0.3);
1785 + background-color: rgb(255 255 255 / 0.2);
1786 + --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
1787 + --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);
1788 + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
1789 + --tw-backdrop-blur: blur(12px);
1790 + -webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);
1791 + backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);
1792 +}
1793 +.frosted-card {
1794 + padding: 1rem;
1795 + border-radius: 0.75rem;
1796 + border-width: 1px;
1797 + border-color: rgb(255 255 255 / 0.3);
1798 + background-color: rgb(255 255 255 / 0.2);
1799 + --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
1800 + --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);
1801 + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
1802 + --tw-backdrop-blur: blur(12px);
1803 + -webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);
1804 + backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);
1805 +}
1806 +.gradient-bg {
1807 + background-image: linear-gradient(to bottom right, var(--tw-gradient-stops));
1808 + --tw-gradient-from: #f0fdf4 var(--tw-gradient-from-position);
1809 + --tw-gradient-to: rgb(240 253 244 / 0) var(--tw-gradient-to-position);
1810 + --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
1811 + --tw-gradient-to: rgb(240 253 250 / 0) var(--tw-gradient-to-position);
1812 + --tw-gradient-stops: var(--tw-gradient-from), #f0fdfa var(--tw-gradient-via-position), var(--tw-gradient-to);
1813 + --tw-gradient-to: #eff6ff var(--tw-gradient-to-position);
1814 +}
1815 +/* Custom animations */
1816 +.fade-in {
1817 + animation: fadeIn 0.5s ease-in;
1818 +}
1819 +@keyframes fadeIn {
1820 + from { opacity: 0; }
1821 + to { opacity: 1; }
1822 +}
1823 +/* Button styles */
1824 +.btn-primary {
1825 + border-radius: 9999px;
1826 + background-image: linear-gradient(to right, var(--tw-gradient-stops));
1827 + --tw-gradient-from: #22c55e var(--tw-gradient-from-position);
1828 + --tw-gradient-to: rgb(34 197 94 / 0) var(--tw-gradient-to-position);
1829 + --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
1830 + --tw-gradient-to: #10b981 var(--tw-gradient-to-position);
1831 + padding-top: 0.5rem;
1832 + padding-bottom: 0.5rem;
1833 + padding-left: 1rem;
1834 + padding-right: 1rem;
1835 + font-weight: 500;
1836 + --tw-text-opacity: 1;
1837 + color: rgb(255 255 255 / var(--tw-text-opacity, 1));
1838 + --tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
1839 + --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);
1840 + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
1841 + transition-property: all;
1842 + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
1843 + transition-duration: 300ms;
1844 +}
1845 +.btn-primary:hover {
1846 + --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
1847 + --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);
1848 + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
1849 +}
1850 +.btn-secondary {
1851 + border-radius: 9999px;
1852 + border-width: 1px;
1853 + --tw-border-opacity: 1;
1854 + border-color: rgb(243 244 246 / var(--tw-border-opacity, 1));
1855 + background-color: rgb(255 255 255 / 0.7);
1856 + padding-top: 0.5rem;
1857 + padding-bottom: 0.5rem;
1858 + padding-left: 1rem;
1859 + padding-right: 1rem;
1860 + --tw-text-opacity: 1;
1861 + color: rgb(55 65 81 / var(--tw-text-opacity, 1));
1862 + --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
1863 + --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);
1864 + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
1865 + --tw-backdrop-blur: blur(4px);
1866 + -webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);
1867 + backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);
1868 + transition-property: all;
1869 + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
1870 + transition-duration: 300ms;
1871 +}
1872 +.btn-secondary:hover {
1873 + --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
1874 + --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);
1875 + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
1876 +}
1877 +.btn-outline {
1878 + border-radius: 9999px;
1879 + border-width: 1px;
1880 + --tw-border-opacity: 1;
1881 + border-color: rgb(34 197 94 / var(--tw-border-opacity, 1));
1882 + background-color: transparent;
1883 + padding-top: 0.5rem;
1884 + padding-bottom: 0.5rem;
1885 + padding-left: 1rem;
1886 + padding-right: 1rem;
1887 + --tw-text-opacity: 1;
1888 + color: rgb(34 197 94 / var(--tw-text-opacity, 1));
1889 + transition-property: all;
1890 + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
1891 + transition-duration: 300ms;
1892 +}
1893 +.btn-outline:hover {
1894 + --tw-bg-opacity: 1;
1895 + background-color: rgb(240 253 244 / var(--tw-bg-opacity, 1));
1896 +}
1897 +/* Badge styles */
1898 +.badge {
1899 + border-radius: 9999px;
1900 + padding-left: 0.5rem;
1901 + padding-right: 0.5rem;
1902 + padding-top: 0.25rem;
1903 + padding-bottom: 0.25rem;
1904 + font-size: 0.75rem;
1905 + line-height: 1rem;
1906 + font-weight: 500;
1907 +}
1908 +.badge-success {
1909 + --tw-bg-opacity: 1;
1910 + background-color: rgb(220 252 231 / var(--tw-bg-opacity, 1));
1911 + --tw-text-opacity: 1;
1912 + color: rgb(21 128 61 / var(--tw-text-opacity, 1));
1913 +}
1914 +.badge-info {
1915 + --tw-bg-opacity: 1;
1916 + background-color: rgb(219 234 254 / var(--tw-bg-opacity, 1));
1917 + --tw-text-opacity: 1;
1918 + color: rgb(29 78 216 / var(--tw-text-opacity, 1));
1919 +}
1920 +.badge-warning {
1921 + --tw-bg-opacity: 1;
1922 + background-color: rgb(255 237 213 / var(--tw-bg-opacity, 1));
1923 + --tw-text-opacity: 1;
1924 + color: rgb(194 65 12 / var(--tw-text-opacity, 1));
1925 +}
1926 +/* Custom utilities */
1927 +.text-gradient {
1928 + background-image: linear-gradient(to right, var(--tw-gradient-stops));
1929 + --tw-gradient-from: #16a34a var(--tw-gradient-from-position);
1930 + --tw-gradient-to: rgb(22 163 74 / 0) var(--tw-gradient-to-position);
1931 + --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
1932 + --tw-gradient-to: #2563eb var(--tw-gradient-to-position);
1933 + -webkit-background-clip: text;
1934 + background-clip: text;
1935 + color: transparent;
1936 +}
1937 +.card-shadow {
1938 + box-shadow: 0 8px 20px -8px rgba(0, 0, 0, 0.1);
1939 +}
1940 +/* Price tag */
1941 +.price-tag {
1942 + font-weight: 600;
1943 + --tw-text-opacity: 1;
1944 + color: rgb(249 115 22 / var(--tw-text-opacity, 1));
1945 +}
1946 +.last\:border-b-0:last-child {
1947 + border-bottom-width: 0px;
1948 +}
1949 +.hover\:bg-gray-50:hover {
1950 + --tw-bg-opacity: 1;
1951 + background-color: rgb(249 250 251 / var(--tw-bg-opacity, 1));
1952 +}
1953 +.hover\:bg-white:hover {
1954 + --tw-bg-opacity: 1;
1955 + background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1));
1956 +}
1957 +.hover\:bg-white\/30:hover {
1958 + background-color: rgb(255 255 255 / 0.3);
1959 +}
1960 +.hover\:from-green-600:hover {
1961 + --tw-gradient-from: #16a34a var(--tw-gradient-from-position);
1962 + --tw-gradient-to: rgb(22 163 74 / 0) var(--tw-gradient-to-position);
1963 + --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
1964 +}
1965 +.hover\:to-green-700:hover {
1966 + --tw-gradient-to: #15803d var(--tw-gradient-to-position);
1967 +}
1968 +.hover\:text-green-500:hover {
1969 + --tw-text-opacity: 1;
1970 + color: rgb(34 197 94 / var(--tw-text-opacity, 1));
1971 +}
1972 +.focus\:border-green-500:focus {
1973 + --tw-border-opacity: 1;
1974 + border-color: rgb(34 197 94 / var(--tw-border-opacity, 1));
1975 +}
1976 +.focus\:outline-none:focus {
1977 + outline: 2px solid transparent;
1978 + outline-offset: 2px;
1979 +}
1980 +.focus\:ring-1:focus {
1981 + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
1982 + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
1983 + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
1984 +}
1985 +.focus\:ring-2:focus {
1986 + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
1987 + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
1988 + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
1989 +}
1990 +.focus\:ring-green-500:focus {
1991 + --tw-ring-opacity: 1;
1992 + --tw-ring-color: rgb(34 197 94 / var(--tw-ring-opacity, 1));
1993 +}
1994 +.focus\:ring-offset-2:focus {
1995 + --tw-ring-offset-width: 2px;
1996 +}
1997 +.active\:bg-white\/50:active {
1998 + background-color: rgb(255 255 255 / 0.5);
1999 +}
2000 +.disabled\:opacity-70:disabled {
2001 + opacity: 0.7;
2002 +}
2003 +@media (min-width: 640px) {
2004 +
2005 + .sm\:mx-auto {
2006 + margin-left: auto;
2007 + margin-right: auto;
2008 + }
2009 +
2010 + .sm\:w-full {
2011 + width: 100%;
2012 + }
2013 +
2014 + .sm\:max-w-md {
2015 + max-width: 28rem;
2016 + }
2017 +
2018 + .sm\:px-6 {
2019 + padding-left: 1.5rem;
2020 + padding-right: 1.5rem;
2021 + }
2022 +}
2023 +@media (min-width: 1024px) {
2024 +
2025 + .lg\:px-8 {
2026 + padding-left: 2rem;
2027 + padding-right: 2rem;
2028 + }
2029 +}
1 +/**
2 + * Mock data for the parent-child education app
3 + * This file contains sample data for courses, activities, and users
4 + */
5 +
6 +// Featured course for the top banner
7 +export const featuredCourse = {
8 + id: 'featured-1',
9 + title: '传承之道',
10 + subtitle: '大理鸡足山游学',
11 + imageUrl: 'https://cdn.ipadbiz.cn/mlaj/images/zMRLZh40kms.jpg',
12 + liveTime: '14:00:00'
13 +};
14 +
15 +// User recommendations
16 +export const userRecommendations = [
17 + { title: "亲子阅读技巧入门", duration: "15分钟", image: "https://cdn.ipadbiz.cn/mlaj/images/zMRLZh40kms.jpg" },
18 + { title: "3-6岁孩子的情绪管理", duration: "20分钟", image: "https://cdn.ipadbiz.cn/mlaj/images/27kCu7bXGEI.jpg" },
19 + { title: "趣味数学启蒙课", duration: "12分钟", image: "https://cdn.ipadbiz.cn/mlaj/images/jbwr0qZvpD4.jpg" },
20 + { title: "儿童英语绘本故事", duration: "18分钟", image: "https://cdn.ipadbiz.cn/mlaj/images/GGCP6vshpPY.jpg" }
21 +];
22 +
23 +// Live streaming sessions
24 +export const liveStreams = [
25 + {
26 + id: 'live-1',
27 + title: '无界之世',
28 + subtitle: '敦煌行',
29 + imageUrl: 'https://cdn.ipadbiz.cn/mlaj/images/2JIvboGLeho.jpg',
30 + isLive: true,
31 + viewers: 272
32 + },
33 + {
34 + id: 'live-2',
35 + title: '慧眼读书会',
36 + subtitle: '',
37 + imageUrl: 'https://cdn.ipadbiz.cn/mlaj/images/_6HzPU9Hyfg (2).jpg',
38 + isLive: true,
39 + viewers: 272
40 + }
41 +];
42 +
43 +// Course list data
44 +export const courses = [
45 + {
46 + id: 'course-1',
47 + title: '好分凭借力,陪你跃龙门!',
48 + subtitle: '4.17-6.18 美乐考前赋能营',
49 + imageUrl: 'https://cdn.ipadbiz.cn/mlaj/images/jbwr0qZvpD4.jpg', // Updated with new image
50 + price: 365,
51 + updatedLessons: 16,
52 + subscribers: 1140,
53 + expireDate: '2025-04-17 00:00:00',
54 + description: `【美乐考前赋能营】
55 + 结合平台多年亲子教育的实践经验,
56 + 针对近在眼前的高考,
57 + 和为学业考试而焦虑的群体
58 + 精心打造、最为定制的专项课程。
59 +
60 + 旨在帮助家长更好地理解,支持孩子,
61 + 有效管理自己的情绪和压力;
62 + 进而引导孩子的情绪状态,
63 + 以最佳的心理状态迎接挑战。
64 +
65 + 在考试的最后冲刺阶段,
66 + 给世界一个爱赏识他们的理由,
67 + 共同助力每个学子梦想成真!`,
68 + sections: [
69 + { title: '课程介绍', content: '课程详细描述内容...' },
70 + { title: '课程目录', content: '第1章:心态准备\n第2章:考前减压\n第3章:家庭支持' }
71 + ]
72 + },
73 + {
74 + id: 'course-2',
75 + title: '大国少年-梦想嘉年华',
76 + subtitle: '亲子冬令营',
77 + imageUrl: 'https://cdn.ipadbiz.cn/mlaj/images/GGCP6vshpPY.jpg', // Updated with new image
78 + price: 3665,
79 + updatedLessons: 16,
80 + subscribers: 1140
81 + },
82 + {
83 + id: 'course-3',
84 + title: '大国少年-世界正东方',
85 + subtitle: '亲子夏令营',
86 + imageUrl: 'https://cdn.ipadbiz.cn/mlaj/images/2Juj2cXWB7U.jpg', // Updated with new image
87 + price: 1280,
88 + updatedLessons: 16,
89 + subscribers: 1140
90 + }
91 +];
92 +
93 +// Activity data
94 +export const activities = [
95 + {
96 + id: 'activity-1',
97 + title: '慧眼读书 | 《论语》',
98 + subtitle: '亲子共读',
99 + imageUrl: 'https://cdn.ipadbiz.cn/mlaj/images/2JIvboGLeho.jpg', // Updated with new image
100 + location: '北京',
101 + status: '活动中',
102 + period: '2024.12.09-2025.01.17',
103 + price: 99,
104 + originalPrice: 120,
105 + isHot: true,
106 + isFree: false,
107 + participantsCount: 18,
108 + maxParticipants: 30
109 + },
110 + {
111 + id: 'activity-2',
112 + title: '好分凭借力,陪你跃龙门!',
113 + subtitle: '4.17-6.18 美乐考前赋能营',
114 + imageUrl: 'https://cdn.ipadbiz.cn/mlaj/images/_6HzPU9Hyfg (2).jpg', // Updated with new image
115 + location: '线上',
116 + status: '活动中',
117 + period: '2024.12.17-2025.01.26',
118 + price: 366,
119 + originalPrice: 468,
120 + isHot: false,
121 + isFree: false,
122 + participantsCount: 25,
123 + maxParticipants: 50
124 + },
125 + {
126 + id: 'activity-3',
127 + title: '7.29-8.4 敦煌: 【青云之路】',
128 + subtitle: '亲子游学营',
129 + imageUrl: 'https://cdn.ipadbiz.cn/mlaj/images/Y17FE9Fuw4Y.jpg', // Updated with new image
130 + location: '敦煌',
131 + status: '即将开始',
132 + period: '2025.01.07-2025.01.26',
133 + price: 3980,
134 + originalPrice: 4200,
135 + isHot: true,
136 + isFree: false,
137 + participantsCount: 12,
138 + maxParticipants: 20
139 + },
140 + {
141 + id: 'activity-4',
142 + title: '【大国少年·梦想嘉年华】',
143 + subtitle: '亲子冬令营',
144 + imageUrl: 'https://cdn.ipadbiz.cn/mlaj/images/-G3rw6Y02D0.jpg',
145 + location: '上海',
146 + status: '进行中',
147 + period: '2025.01.09-2025.01.22',
148 + price: 1280,
149 + originalPrice: 1380,
150 + isHot: false,
151 + isFree: false,
152 + participantsCount: 24,
153 + maxParticipants: 40
154 + },
155 + {
156 + id: 'activity-5',
157 + title: '慧眼读书会 |《零极限》',
158 + subtitle: '共读',
159 + imageUrl: 'https://cdn.ipadbiz.cn/mlaj/images/Oalh2MojUuk.jpg',
160 + location: '线上',
161 + status: '进行中',
162 + period: '2025.01.09-2025.01.22',
163 + price: 0,
164 + originalPrice: 0,
165 + isHot: false,
166 + isFree: true,
167 + participantsCount: 45,
168 + maxParticipants: 100
169 + }
170 +];
171 +
172 +// User profile data
173 +export const userProfile = {
174 + name: '李玉红',
175 + avatar: 'https://cdn.ipadbiz.cn/mlaj/images/user-avatar-2.jpg',
176 + activities: [],
177 + courses: [],
178 + children: [],
179 + checkIns: {
180 + totalDays: 45,
181 + currentStreak: 7,
182 + longestStreak: 15
183 + }
184 +};
185 +
186 +// Daily check-in data
187 +export const checkInTypes = [
188 + { id: 'reading', name: '阅读打卡', icon: 'book' },
189 + { id: 'exercise', name: '运动打卡', icon: 'running' },
190 + { id: 'study', name: '学习打卡', icon: 'graduation-cap' },
191 + { id: 'reflection', name: '反思打卡', icon: 'pencil-alt' }
192 +];
193 +
194 +// Community posts data
195 +export const communityPosts = [
196 + {
197 + id: 'post-1',
198 + author: {
199 + id: 'user-1',
200 + name: '王小明',
201 + avatar: 'https://cdn.ipadbiz.cn/mlaj/images/user-avatar-3.jpg'
202 + },
203 + content: '今天和孩子一起完成了《论语》的共读,收获颇丰!孩子对"己所不欲勿施于人"这句话有了更深的理解。',
204 + images: ['https://cdn.ipadbiz.cn/mlaj/images/post-1-1.jpg', 'https://cdn.ipadbiz.cn/mlaj/images/post-1-2.jpg'],
205 + likes: 24,
206 + comments: 5,
207 + createdAt: '2023-03-15T08:30:00Z'
208 + },
209 + {
210 + id: 'post-2',
211 + author: {
212 + id: 'user-2',
213 + name: '李晓华',
214 + avatar: 'https://cdn.ipadbiz.cn/mlaj/images/user-avatar-1.jpg'
215 + },
216 + content: '冬令营第三天,孩子们参观了科技馆,学习了很多有趣的知识,晚上的篝火晚会也很精彩!',
217 + images: ['https://cdn.ipadbiz.cn/mlaj/images/post-2-1.jpg'],
218 + likes: 36,
219 + comments: 8,
220 + createdAt: '2023-03-14T15:45:00Z'
221 + }
222 +];
1 +<script setup>
2 +import { Button, NavBar, Tabbar, TabbarItem } from 'vant';
3 +</script>
4 +
5 +<template>
6 + <div class="min-h-screen flex flex-col bg-gray-100">
7 + <van-nav-bar
8 + title="关于"
9 + left-text="返回"
10 + right-text="菜单"
11 + left-arrow
12 + @click-left="onClickLeft"
13 + @click-right="onClickRight"
14 + />
15 +
16 + <div class="flex-1 p-4">
17 + <div class="bg-white rounded-lg shadow p-4 mb-4">
18 + <h2 class="text-xl font-bold mb-2">关于我们</h2>
19 + <p class="text-gray-600 mb-4">这是一个示例项目的关于页面,展示了如何使用Vue3、Vite、Vant4和TailwindCSS构建现代化的Web应用。</p>
20 + <van-button type="primary" block>了解更多</van-button>
21 + </div>
22 +
23 + <div class="grid grid-cols-2 gap-4">
24 + <div class="bg-white rounded-lg shadow p-4">
25 + <h3 class="text-lg font-semibold mb-2">联系方式</h3>
26 + <ul class="text-gray-600">
27 + <li>邮箱:example@example.com</li>
28 + <li>电话:123-456-7890</li>
29 + <li>地址:示例地址</li>
30 + </ul>
31 + </div>
32 + <div class="bg-white rounded-lg shadow p-4">
33 + <h3 class="text-lg font-semibold mb-2">版本信息</h3>
34 + <ul class="text-gray-600">
35 + <li>当前版本:1.0.0</li>
36 + <li>更新日期:2024-03-20</li>
37 + <li>开源协议:MIT</li>
38 + </ul>
39 + </div>
40 + </div>
41 + </div>
42 +
43 + <van-tabbar v-model="active">
44 + <van-tabbar-item icon="home-o">首页</van-tabbar-item>
45 + <van-tabbar-item icon="search">搜索</van-tabbar-item>
46 + <van-tabbar-item icon="friends-o">好友</van-tabbar-item>
47 + <van-tabbar-item icon="setting-o">设置</van-tabbar-item>
48 + </van-tabbar>
49 + </div>
50 +</template>
51 +
52 +<script>
53 +export default {
54 + data() {
55 + return {
56 + active: 0
57 + }
58 + },
59 + methods: {
60 + onClickLeft() {
61 + this.$router.back()
62 + },
63 + onClickRight() {
64 + // 处理右侧按钮点击
65 + }
66 + }
67 +}
68 +</script>
1 +<!--
2 + * @Date: 2025-03-20 19:55:21
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2025-03-20 20:26:17
5 + * @FilePath: /vue-vite/src/views/Home.vue
6 + * @Description: 文件描述
7 +-->
8 +<template>
9 + <AppLayout title="亲子学院" :rightContent="rightContent">
10 + <div class="pb-16 bg-gradient-to-b from-white via-green-50/10 to-blue-50/10">
11 + <!-- Header Section with Welcome & Weather -->
12 + <div class="px-4 pt-3 pb-4">
13 + <FrostedGlass class="p-4 rounded-xl mb-4">
14 + <div class="flex justify-between items-center mb-3">
15 + <div class="flex items-center">
16 + <div class="w-10 h-10 rounded-full overflow-hidden mr-3">
17 + <img
18 + src="https://cdn.ipadbiz.cn/mlaj/images/user-avatar-2.jpg"
19 + alt="王小明"
20 + class="w-full h-full object-cover"
21 + @error="handleImageError" />
22 + </div>
23 + <div>
24 + <h2 class="text-xl font-bold">欢迎回来,小明!</h2>
25 + <p class="text-sm text-gray-500">{{ formatToday() }}</p>
26 + </div>
27 + </div>
28 + <div class="flex items-center text-sm">
29 + <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-amber-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
30 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
31 + </svg>
32 + <span class="ml-1 font-medium">23°C</span>
33 + <span class="ml-1 text-gray-500">晴朗</span>
34 + </div>
35 + </div>
36 +
37 + <!-- User Stats -->
38 + <div class="flex justify-between text-center py-2">
39 + <div class="border-r border-gray-200 flex-1">
40 + <div class="text-lg font-bold">3</div>
41 + <div class="text-xs text-gray-500">连续打卡</div>
42 + </div>
43 + <div class="border-r border-gray-200 flex-1">
44 + <div class="text-lg font-bold">12</div>
45 + <div class="text-xs text-gray-500">已学课程</div>
46 + </div>
47 + <div class="flex-1">
48 + <div class="text-lg font-bold">25</div>
49 + <div class="text-xs text-gray-500">积分</div>
50 + </div>
51 + </div>
52 + </FrostedGlass>
53 +
54 + <!-- Daily Check-in -->
55 + <FrostedGlass class="p-4 rounded-xl">
56 + <div class="flex justify-between items-center mb-3">
57 + <h3 class="font-medium">今日打卡</h3>
58 + <router-link to="/profile" class="text-green-600 text-sm">打卡记录</router-link>
59 + </div>
60 +
61 + <div v-if="checkInSuccess" class="bg-green-50 border border-green-200 rounded-lg p-4 text-center">
62 + <svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 text-green-500 mx-auto mb-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
63 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
64 + </svg>
65 + <h4 class="text-green-700 font-medium mb-1">打卡成功!</h4>
66 + <p class="text-green-600 text-sm">+5 积分已添加到您的账户</p>
67 + </div>
68 + <template v-else>
69 + <div class="flex space-x-3 overflow-x-auto py-2">
70 + <button
71 + v-for="checkInType in checkInTypes"
72 + :key="checkInType.id"
73 + :class="[
74 + 'flex-shrink-0 flex flex-col items-center p-3 rounded-lg transition-colors',
75 + selectedCheckIn?.id === checkInType.id
76 + ? 'bg-green-100 border border-green-200'
77 + : 'bg-white/70 border border-gray-100 hover:bg-white'
78 + ]"
79 + @click="handleCheckInSelect(checkInType)"
80 + >
81 + <div :class="[
82 + 'w-12 h-12 rounded-full flex items-center justify-center mb-1 transition-colors',
83 + selectedCheckIn?.id === checkInType.id
84 + ? 'bg-green-500 text-white'
85 + : 'bg-gray-100 text-gray-500'
86 + ]">
87 + <svg v-if="checkInType.id === 'reading'" xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
88 + <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" />
89 + </svg>
90 + <svg v-if="checkInType.id === 'exercise'" xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
91 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
92 + </svg>
93 + <svg v-if="checkInType.id === 'study'" xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
94 + <path d="M12 14l9-5-9-5-9 5 9 5z" />
95 + <path d="M12 14l6.16-3.422a12.083 12.083 0 01.665 6.479A11.952 11.952 0 0012 20.055a11.952 11.952 0 00-6.824-2.998a12.078 12.078 0 01.665-6.479L12 14z" />
96 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 14l9-5-9-5-9 5 9 5zm0 0l6.16-3.422a12.083 12.083 0 01.665 6.479A11.952 11.952 0 0012 20.055a11.952 11.952 0 00-6.824-2.998a12.078 12.078 0 01.665-6.479L12 14zm-4 6v-7.5l4-2.222" />
97 + </svg>
98 + <svg v-if="checkInType.id === 'reflection'" xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
99 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
100 + </svg>
101 + </div>
102 + <span class="text-xs">{{ checkInType.name }}</span>
103 + </button>
104 + </div>
105 +
106 + <div v-if="selectedCheckIn" class="mt-3">
107 + <textarea
108 + :placeholder="`请输入${selectedCheckIn.name}内容...`"
109 + v-model="checkInContent"
110 + class="w-full p-3 border border-gray-200 rounded-lg text-sm resize-none h-24"
111 + />
112 + <button
113 + class="mt-2 w-full bg-gradient-to-r from-green-500 to-green-600 text-white py-2 rounded-lg flex items-center justify-center"
114 + @click="handleCheckInSubmit"
115 + :disabled="isCheckingIn"
116 + >
117 + <template v-if="isCheckingIn">
118 + <div class="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin mr-2"></div>
119 + 提交中...
120 + </template>
121 + <template v-else>提交打卡</template>
122 + </button>
123 + </div>
124 + </template>
125 + </FrostedGlass>
126 + </div>
127 +
128 + <!-- Summer Camp Promotion -->
129 + <div class="px-4 mb-6">
130 + <SummerCampCard />
131 + </div>
132 +
133 + <!-- Featured Courses Carousel -->
134 + <div class="mb-6">
135 + <div class="px-4 mb-2">
136 + <h3 class="font-medium">精选课程</h3>
137 + </div>
138 + <div class="relative">
139 + <div
140 + ref="carouselRef"
141 + class="flex overflow-x-scroll snap-x snap-mandatory"
142 + style="scrollbar-width: none; -ms-overflow-style: none;"
143 + >
144 + <div
145 + v-for="(course, index) in courses.slice(0, 4)"
146 + :key="course.id"
147 + class="flex-shrink-0 w-full snap-center px-4"
148 + >
149 + <div class="relative rounded-xl overflow-hidden shadow-lg h-48">
150 + <img
151 + :src="course.imageUrl || 'https://cdn.ipadbiz.cn/mlaj/images/featured-course.jpg'"
152 + :alt="course.title"
153 + class="w-full h-full object-cover"
154 + />
155 + <div class="absolute inset-0 bg-gradient-to-b from-transparent via-black/20 to-black/60 flex flex-col justify-end p-4">
156 + <div class="bg-amber-500/90 text-white px-2 py-1 rounded-full text-xs font-medium inline-block w-fit mb-1">
157 + {{ course.category }}
158 + </div>
159 + <h2 class="text-2xl font-bold text-white drop-shadow-md">{{ course.title }}</h2>
160 + <p class="text-white/90 text-sm drop-shadow-sm mb-1">{{ course.subtitle }}</p>
161 + <div class="flex justify-between items-center">
162 + <div class="flex items-center">
163 + <div class="flex">
164 + <svg
165 + v-for="i in 5"
166 + :key="i"
167 + xmlns="http://www.w3.org/2000/svg"
168 + :class="[`h-4 w-4`, i <= course.rating ? 'text-amber-400' : 'text-gray-300']"
169 + viewBox="0 0 20 20"
170 + fill="currentColor"
171 + >
172 + <path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
173 + </svg>
174 + </div>
175 + <span class="text-white text-xs ml-1">{{ course.ratingCount }}人评</span>
176 + </div>
177 + <router-link
178 + :to="`/courses/${course.id}`"
179 + class="bg-white/90 text-green-600 px-3 py-1 rounded-full text-xs font-medium"
180 + >
181 + 立即学习
182 + </router-link>
183 + </div>
184 + </div>
185 + </div>
186 + </div>
187 + </div>
188 +
189 + <!-- Carousel Indicators -->
190 + <div class="flex justify-center mt-4">
191 + <button
192 + v-for="(_, index) in courses.slice(0, 4)"
193 + :key="index"
194 + @click="scrollToSlide(index)"
195 + :class="[
196 + 'w-2 h-2 mx-1 rounded-full',
197 + currentSlide === index ? 'bg-green-600' : 'bg-gray-300'
198 + ]"
199 + />
200 + </div>
201 + </div>
202 + </div>
203 +
204 + <!-- Tab Navigation -->
205 + <div class="px-4 border-b border-gray-200">
206 + <div class="flex space-x-6">
207 + <button
208 + v-for="tab in ['推荐', '直播', '精选']"
209 + :key="tab"
210 + @click="activeTab = tab"
211 + :class="[
212 + 'pb-3 px-1 font-medium',
213 + activeTab === tab
214 + ? 'text-green-600 border-b-2 border-green-600'
215 + : 'text-gray-500'
216 + ]"
217 + >
218 + {{ tab }}
219 + <span
220 + v-if="tab === '直播'"
221 + class="ml-1 px-1.5 py-0.5 bg-red-500 text-white text-xs rounded-full"
222 + >
223 + 2
224 + </span>
225 + </button>
226 + </div>
227 + </div>
228 +
229 + <!-- Content Based on Active Tab -->
230 + <div class="px-4 mt-5">
231 + <!-- Recommended Content -->
232 + <div v-if="activeTab === '推荐'">
233 + <!-- Personalized Recommendations -->
234 + <section class="mb-7">
235 + <div class="flex justify-between items-center mb-3">
236 + <h3 class="font-medium">为您推荐</h3>
237 + <button class="text-xs text-gray-500 flex items-center">
238 + 换一批
239 + <svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 ml-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
240 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
241 + </svg>
242 + </button>
243 + </div>
244 + <div class="grid grid-cols-2 gap-4">
245 + <FrostedGlass
246 + v-for="(item, index) in userRecommendations"
247 + :key="index"
248 + class="p-3 rounded-xl"
249 + >
250 + <div class="flex flex-col h-full">
251 + <div class="h-28 mb-2 rounded-lg overflow-hidden">
252 + <img
253 + :src="item.image"
254 + :alt="item.title"
255 + class="w-full h-full object-cover"
256 + @error="handleImageError"
257 + />
258 + </div>
259 + <h4 class="font-medium text-sm mb-1 line-clamp-1">{{ item.title }}</h4>
260 + <p class="text-xs text-gray-500 flex items-center mt-auto">
261 + <svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
262 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
263 + </svg>
264 + {{ item.duration }}
265 + </p>
266 + </div>
267 + </FrostedGlass>
268 + </div>
269 + </section>
270 +
271 + <!-- Recent Activities -->
272 + <section class="mb-7">
273 + <div class="flex justify-between items-center mb-3">
274 + <h3 class="font-medium">最新活动</h3>
275 + <router-link to="/activities" class="text-xs text-gray-500 flex items-center">
276 + 更多
277 + <svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 ml-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
278 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
279 + </svg>
280 + </router-link>
281 + </div>
282 + <div class="space-y-4">
283 + <ActivityCard
284 + v-for="activity in activities.slice(0, 3)"
285 + :key="activity.id"
286 + :activity="activity"
287 + />
288 + </div>
289 + </section>
290 +
291 + <!-- Popular Courses -->
292 + <section>
293 + <div class="flex justify-between items-center mb-3">
294 + <h3 class="font-medium">热门课程</h3>
295 + <router-link to="/courses" class="text-xs text-gray-500 flex items-center">
296 + 更多
297 + <svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 ml-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
298 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
299 + </svg>
300 + </router-link>
301 + </div>
302 + <div class="space-y-4">
303 + <CourseCard
304 + v-for="course in courses.slice(0, 3)"
305 + :key="course.id"
306 + :course="course"
307 + />
308 + </div>
309 + </section>
310 + </div>
311 +
312 + <!-- Live Content -->
313 + <div v-if="activeTab === '直播'">
314 + <section>
315 + <div class="flex justify-between items-center mb-3">
316 + <h3 class="font-medium">正在直播</h3>
317 + <div class="text-xs text-red-500 flex items-center">
318 + <div class="w-2 h-2 bg-red-500 rounded-full mr-1 animate-pulse"></div>
319 + 2个直播中
320 + </div>
321 + </div>
322 + <div class="grid grid-cols-2 gap-4 mb-7">
323 + <LiveStreamCard
324 + v-for="stream in liveStreams"
325 + :key="stream.id"
326 + :stream="stream"
327 + />
328 + </div>
329 +
330 + <div class="mb-5">
331 + <div class="flex justify-between items-center mb-3">
332 + <h3 class="font-medium">直播日历</h3>
333 + <router-link to="/live-calendar" class="text-xs text-blue-500">
334 + 查看日历
335 + </router-link>
336 + </div>
337 + <FrostedGlass class="p-3 rounded-xl">
338 + <div class="flex space-x-2 overflow-x-auto py-1">
339 + <div
340 + v-for="(day, i) in ['今天', '明天', '周三', '周四', '周五', '周六', '周日']"
341 + :key="day"
342 + :class="[
343 + 'flex-shrink-0 w-10 h-14 flex flex-col items-center justify-center rounded-lg',
344 + i === 0 ? 'bg-green-500 text-white' : 'bg-white/50'
345 + ]"
346 + >
347 + <div class="text-xs">{{ day }}</div>
348 + <div class="font-bold mt-1">{{ new Date().getDate() + i }}</div>
349 + </div>
350 + </div>
351 + </FrostedGlass>
352 + </div>
353 +
354 + <div>
355 + <h3 class="font-medium mb-3">直播预告</h3>
356 + <div class="space-y-3">
357 + <FrostedGlass
358 + v-for="(item, index) in [
359 + { title: '亲子阅读会第1期', time: '今天 19:30-20:30', image: 'https://cdn.ipadbiz.cn/mlaj/images/live-1.jpg' },
360 + { title: '儿童心理健康讲座', time: '明天 20:00-21:00', image: 'https://cdn.ipadbiz.cn/mlaj/images/live-2.jpg' },
361 + { title: '家庭教育经验分享', time: '周三 19:00-20:00', image: 'https://cdn.ipadbiz.cn/mlaj/images/live-3.jpg' }
362 + ]"
363 + :key="index"
364 + class="p-3 rounded-xl"
365 + >
366 + <div class="flex justify-between items-center">
367 + <div class="flex items-center">
368 + <div class="w-12 h-12 bg-green-100 rounded-lg overflow-hidden mr-3 flex-shrink-0">
369 + <img
370 + :src="item.image"
371 + :alt="item.title"
372 + class="w-full h-full object-cover"
373 + @error="handleImageError"
374 + />
375 + </div>
376 + <div>
377 + <h4 class="font-medium text-sm">{{ item.title }}</h4>
378 + <p class="text-xs text-gray-500 mt-1">{{ item.time }}</p>
379 + </div>
380 + </div>
381 + <button class="bg-white text-green-600 border border-green-600 px-3 py-1 rounded-full text-xs flex-shrink-0">
382 + 预约
383 + </button>
384 + </div>
385 + </FrostedGlass>
386 + </div>
387 + </div>
388 + </section>
389 + </div>
390 +
391 + <!-- Featured Content -->
392 + <div v-if="activeTab === '精选'">
393 + <section>
394 + <div class="mb-5">
395 + <h3 class="font-medium mb-3">精选内容</h3>
396 + <FrostedGlass class="p-4 rounded-xl">
397 + <div class="flex flex-col">
398 + <div class="inline-block px-2 py-1 bg-blue-100 text-blue-600 text-xs rounded-full mb-2 w-fit">
399 + 独家专栏
400 + </div>
401 + <h4 class="font-medium text-lg mb-2">《如何培养孩子的阅读习惯》</h4>
402 + <p class="text-gray-600 text-sm mb-4 line-clamp-2">
403 + 阅读习惯的培养是一个长期过程,本文将分享如何从日常生活点滴培养孩子的阅读兴趣和习惯...
404 + </p>
405 + <router-link to="/articles/1" class="text-green-600 text-sm font-medium">
406 + 查看完整文章
407 + </router-link>
408 + </div>
409 + </FrostedGlass>
410 + </div>
411 +
412 + <div>
413 + <h3 class="font-medium mb-3">推荐视频</h3>
414 + <div class="space-y-4">
415 + <div
416 + v-for="(item, index) in [
417 + { title: '亲子沟通的艺术', views: '1.2万', duration: '08:25', image: 'https://cdn.ipadbiz.cn/mlaj/images/video-1.jpg' },
418 + { title: '如何做好家庭教育', views: '8千', duration: '12:40', image: 'https://cdn.ipadbiz.cn/mlaj/images/video-2.jpg' },
419 + { title: '孩子营养餐制作指南', views: '5千', duration: '15:18', image: 'https://cdn.ipadbiz.cn/mlaj/images/video-3.jpg' }
420 + ]"
421 + :key="index"
422 + class="relative rounded-xl overflow-hidden shadow-md h-48"
423 + >
424 + <img
425 + :src="item.image"
426 + :alt="item.title"
427 + class="w-full h-full object-cover"
428 + @error="handleImageError"
429 + />
430 + <div class="absolute inset-0 bg-gradient-to-b from-transparent to-black/70 flex flex-col justify-end p-4">
431 + <h4 class="text-white font-medium mb-1">{{ item.title }}</h4>
432 + <div class="flex justify-between items-center">
433 + <p class="text-white/80 text-xs">{{ item.views }}次播放 · {{ item.duration }}</p>
434 + <button class="bg-white/20 backdrop-blur-sm p-2 rounded-full">
435 + <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-white" viewBox="0 0 20 20" fill="currentColor">
436 + <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z" clip-rule="evenodd" />
437 + </svg>
438 + </button>
439 + </div>
440 + </div>
441 + </div>
442 + </div>
443 + </div>
444 + </section>
445 + </div>
446 + </div>
447 + </div>
448 + </AppLayout>
449 +</template>
450 +
451 +<script setup lang="jsx">
452 +import { ref, onMounted, onUnmounted, defineComponent, h } from 'vue'
453 +import AppLayout from '@/components/layout/AppLayout.vue'
454 +import FrostedGlass from '@/components/ui/FrostedGlass.vue'
455 +import CourseCard from '@/components/ui/CourseCard.vue'
456 +import LiveStreamCard from '@/components/ui/LiveStreamCard.vue'
457 +import ActivityCard from '@/components/ui/ActivityCard.vue'
458 +import SummerCampCard from '@/components/ui/SummerCampCard.vue'
459 +import { courses, liveStreams, activities, checkInTypes, userRecommendations } from '@/utils/mockData'
460 +
461 +// 响应式状态
462 +const activeTab = ref('推荐')
463 +const selectedCheckIn = ref(null)
464 +const checkInContent = ref('')
465 +const currentSlide = ref(0)
466 +const isCheckingIn = ref(false)
467 +const checkInSuccess = ref(false)
468 +const carouselRef = ref(null)
469 +
470 +// 右侧内容组件
471 +const RightContent = defineComponent({
472 + setup() {
473 + return () => (
474 + <div class="flex items-center">
475 + <button class="p-2 mr-1">
476 + <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-gray-700" fill="none" viewBox="0 0 24 24" stroke="currentColor">
477 + <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" />
478 + </svg>
479 + </button>
480 + <button class="p-2">
481 + <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-gray-700" fill="none" viewBox="0 0 24 24" stroke="currentColor">
482 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
483 + </svg>
484 + </button>
485 + </div>
486 + )
487 + }
488 +})
489 +
490 +const rightContent = h(RightContent)
491 +
492 +// 图片加载错误处理
493 +const handleImageError = (e) => {
494 + e.target.onerror = null
495 + e.target.src = 'https://cdn.ipadbiz.cn/mlaj/images/user-avatar-2.jpg'
496 +}
497 +
498 +// 格式化今天的日期
499 +const formatToday = () => {
500 + const today = new Date()
501 + const options = { month: 'long', day: 'numeric', weekday: 'long' }
502 + return today.toLocaleDateString('zh-CN', options)
503 +}
504 +
505 +// 轮播图滚动到指定位置
506 +const scrollToSlide = (index) => {
507 + if (carouselRef.value) {
508 + const slideWidth = carouselRef.value.offsetWidth
509 + carouselRef.value.scrollTo({
510 + left: index * slideWidth,
511 + behavior: 'smooth'
512 + })
513 + currentSlide.value = index
514 + }
515 +}
516 +
517 +// 处理打卡类型选择
518 +const handleCheckInSelect = (checkInType) => {
519 + selectedCheckIn.value = checkInType
520 + checkInContent.value = ''
521 +}
522 +
523 +// 处理打卡提交
524 +const handleCheckInSubmit = () => {
525 + if (!checkInContent.value.trim()) return
526 +
527 + isCheckingIn.value = true
528 +
529 + // 模拟API调用
530 + setTimeout(() => {
531 + isCheckingIn.value = false
532 + checkInSuccess.value = true
533 + selectedCheckIn.value = null
534 + checkInContent.value = ''
535 +
536 + // 3秒后重置成功提示
537 + setTimeout(() => {
538 + checkInSuccess.value = false
539 + }, 3000)
540 + }, 1500)
541 +}
542 +
543 +// 自动轮播
544 +let carouselInterval
545 +onMounted(() => {
546 + carouselInterval = setInterval(() => {
547 + if (carouselRef.value) {
548 + const nextSlide = (currentSlide.value + 1) % courses.slice(0, 4).length
549 + scrollToSlide(nextSlide)
550 + }
551 + }, 5000)
552 +})
553 +
554 +onUnmounted(() => {
555 + if (carouselInterval) {
556 + clearInterval(carouselInterval)
557 + }
558 +})
559 +</script>
1 +/** @type {import('tailwindcss').Config} */
2 +export default {
3 + content: [
4 + "./index.html",
5 + "./src/**/*.{vue,js,ts,jsx,tsx}",
6 + ],
7 + theme: {
8 + extend: {},
9 + },
10 + plugins: [],
11 +}
1 +/*
2 + * @Date: 2025-03-20 19:53:12
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2025-03-20 20:27:29
5 + * @FilePath: /vue-vite/vite.config.js
6 + * @Description: 文件描述
7 + */
8 +import { defineConfig } from 'vite'
9 +import vue from '@vitejs/plugin-vue'
10 +import vueJsx from '@vitejs/plugin-vue-jsx'
11 +import path from "path";
12 +
13 +// https://vite.dev/config/
14 +export default defineConfig({
15 + plugins: [vue(), vueJsx()],
16 + resolve: {
17 + alias: { // 将会被传递到 @rollup/plugin-alias 作为 entries 的选项。也可以是一个对象,或一个 { find, replacement } 的数组. 当使用文件系统路径的别名时,请始终使用绝对路径。相对路径的别名值会被原封不动地使用,因此无法被正常解析。 更高级的自定义解析方法可以通过 插件 实现。
18 + "@": path.resolve(__dirname, "src"),
19 + "@components": path.resolve(__dirname, "src/components"),
20 + "@composables": path.resolve(__dirname, "src/composables"),
21 + "@utils": path.resolve(__dirname, "src/utils"),
22 + "@images": path.resolve(__dirname, "src/assets/images"),
23 + "@css": path.resolve(__dirname, "src/assets/css"),
24 + "@mock": path.resolve(__dirname, "src/assets/mock"),
25 + "common": path.resolve(__dirname, "src/common"),
26 + "@api": path.resolve(__dirname, "src/api"),
27 + },
28 + dedupe: ['vue'], // 如果你在你的应用程序中有相同依赖的副本(比如 monorepos),使用这个选项来强制 Vite 总是将列出的依赖关系解析到相同的副本(从项目根目录)。
29 + // conditions: [''], // 在解析包的 情景导出 时允许的附加条件。
30 + // mainFields: [''], // package.json 中,在解析包的入口点时尝试的字段列表。注意,这比从 exports 字段解析的情景导出优先级低:如果一个入口点从 exports 成功解析,主字段将被忽略。
31 + extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue'], // 导入时想要省略的扩展名列表。注意,不 建议忽略自定义导入类型的扩展名(例如:.vue),因为它会干扰 IDE 和类型支持。
32 + },
33 +})