hookehuyr

feat(背景效果): 添加星空背景组件并集成到测试页面

新增可配置的星空背景组件,支持自定义星星数量、颜色、速度等参数
在测试页面中集成该组件作为背景效果
...@@ -37,6 +37,7 @@ declare module 'vue' { ...@@ -37,6 +37,7 @@ declare module 'vue' {
37 RouterView: typeof import('vue-router')['RouterView'] 37 RouterView: typeof import('vue-router')['RouterView']
38 SearchBar: typeof import('./components/ui/SearchBar.vue')['default'] 38 SearchBar: typeof import('./components/ui/SearchBar.vue')['default']
39 SharePoster: typeof import('./components/ui/SharePoster.vue')['default'] 39 SharePoster: typeof import('./components/ui/SharePoster.vue')['default']
40 + StarryBackground: typeof import('./components/effects/StarryBackground.vue')['default']
40 SummerCampCard: typeof import('./components/ui/SummerCampCard.vue')['default'] 41 SummerCampCard: typeof import('./components/ui/SummerCampCard.vue')['default']
41 TaskCalendar: typeof import('./components/ui/TaskCalendar.vue')['default'] 42 TaskCalendar: typeof import('./components/ui/TaskCalendar.vue')['default']
42 TaskCascaderFilter: typeof import('./components/teacher/TaskCascaderFilter.vue')['default'] 43 TaskCascaderFilter: typeof import('./components/teacher/TaskCascaderFilter.vue')['default']
......
1 +<template>
2 + <div class="canvas-box" :style="containerStyle">
3 + <canvas ref="canvasRef">你的浏览器不支持canvas</canvas>
4 + </div>
5 +</template>
6 +
7 +<script setup>
8 +import { ref, onMounted, onUnmounted, computed } from 'vue';
9 +
10 +const props = defineProps({
11 + // 背景颜色
12 + bgColor: {
13 + type: String,
14 + default: 'white'
15 + },
16 + // 背景图片 URL
17 + bgImage: {
18 + type: String,
19 + default: ''
20 + },
21 + // 星星数量
22 + starCount: {
23 + type: Number,
24 + default: 500
25 + },
26 + // 星星颜色 (RGBA 前缀,例如 '255,255,255')
27 + starColor: {
28 + type: String,
29 + default: '255,255,255'
30 + },
31 + // 星星移动速度系数 (值越大移动越快)
32 + starSpeed: {
33 + type: Number,
34 + default: 0.15
35 + },
36 + // 流星速度 - 水平分量 (负值向左,正值向右)
37 + meteorVx: {
38 + type: Number,
39 + default: -2
40 + },
41 + // 流星速度 - 垂直分量 (正值向下)
42 + meteorVy: {
43 + type: Number,
44 + default: 4
45 + },
46 + // 流星拖尾长度系数 (值越大尾巴越长)
47 + meteorTrail: {
48 + type: Number,
49 + default: 20
50 + }
51 +});
52 +
53 +const containerStyle = computed(() => {
54 + const style = {
55 + backgroundColor: props.bgColor
56 + };
57 + if (props.bgImage) {
58 + style.backgroundImage = `url(${props.bgImage})`;
59 + style.backgroundSize = 'cover';
60 + style.backgroundPosition = 'center';
61 + style.backgroundRepeat = 'no-repeat';
62 + }
63 + return style;
64 +});
65 +
66 +const canvasRef = ref(null);
67 +let context = null;
68 +let width = 0;
69 +let height = 0;
70 +let stars = [];
71 +let animationFrameId = null;
72 +let meteorTimeoutId = null;
73 +let meteorIndex = -1; // 当前流星的索引
74 +
75 +// 初始化星星
76 +const initStars = () => {
77 + stars = [];
78 + for (let i = 0; i < props.starCount; i++) {
79 + stars.push({
80 + x: Math.round(Math.random() * width),
81 + y: Math.round(Math.random() * height),
82 + r: Math.random() * 3,
83 + ra: Math.random() * 0.01,
84 + alpha: Math.random(),
85 + vx: Math.random() * props.starSpeed - props.starSpeed / 2,
86 + vy: Math.random() * props.starSpeed - props.starSpeed / 2
87 + });
88 + }
89 +};
90 +
91 +// 渲染函数
92 +const render = () => {
93 + if (!context) return;
94 +
95 + // 使用 clearRect 代替 fillRect 覆盖,如果需要透明背景或自定义背景色由 CSS 控制
96 + // 但为了实现星星的拖尾效果或者覆盖上一帧,这里使用 clearRect 清除整个画布
97 + context.clearRect(0, 0, width, height);
98 +
99 + for (let i = 0; i < props.starCount; i++) {
100 + const star = stars[i];
101 +
102 + // 流星逻辑
103 + if (i === meteorIndex) {
104 + star.vx = props.meteorVx;
105 + star.vy = props.meteorVy;
106 + context.beginPath();
107 + context.strokeStyle = `rgba(${props.starColor}, ${star.alpha})`;
108 + context.lineWidth = star.r;
109 + context.moveTo(star.x, star.y);
110 + context.lineTo(star.x + star.vx * props.meteorTrail, star.y + star.vy * props.meteorTrail);
111 + context.stroke();
112 + context.closePath();
113 + }
114 +
115 + // 星星闪烁与移动逻辑
116 + star.alpha += star.ra;
117 + if (star.alpha <= 0) {
118 + star.alpha = 0;
119 + star.ra = -star.ra;
120 + star.vx = Math.random() * props.starSpeed - props.starSpeed / 2;
121 + star.vy = Math.random() * props.starSpeed - props.starSpeed / 2;
122 + } else if (star.alpha > 1) {
123 + star.alpha = 1;
124 + star.ra = -star.ra;
125 + }
126 +
127 + star.x += star.vx;
128 + // 边界检查
129 + if (star.x >= width) {
130 + star.x = 0;
131 + } else if (star.x < 0) {
132 + star.x = width;
133 + star.vx = Math.random() * props.starSpeed - props.starSpeed / 2;
134 + star.vy = Math.random() * props.starSpeed - props.starSpeed / 2;
135 + }
136 +
137 + star.y += star.vy;
138 + if (star.y >= height) {
139 + star.y = 0;
140 + star.vy = Math.random() * props.starSpeed - props.starSpeed / 2;
141 + star.vx = Math.random() * props.starSpeed - props.starSpeed / 2;
142 + } else if (star.y < 0) {
143 + star.y = height;
144 + }
145 +
146 + // 绘制星星
147 + context.beginPath();
148 + const bg = context.createRadialGradient(star.x, star.y, 0, star.x, star.y, star.r);
149 + bg.addColorStop(0, `rgba(${props.starColor}, ${star.alpha})`);
150 + bg.addColorStop(1, `rgba(${props.starColor}, 0)`);
151 + context.fillStyle = bg;
152 + context.arc(star.x, star.y, star.r, 0, Math.PI * 2, true);
153 + context.fill();
154 + context.closePath();
155 + }
156 +
157 + animationFrameId = requestAnimationFrame(render);
158 +};
159 +
160 +// 流星触发逻辑
161 +const triggerMeteor = () => {
162 + const time = Math.round(Math.random() * 3000 + 33);
163 + meteorTimeoutId = setTimeout(() => {
164 + meteorIndex = Math.ceil(Math.random() * stars.length);
165 + triggerMeteor();
166 + }, time);
167 +};
168 +
169 +// 窗口大小调整处理
170 +const handleResize = () => {
171 + if (canvasRef.value) {
172 + width = window.innerWidth;
173 + height = window.innerHeight;
174 + canvasRef.value.width = width;
175 + canvasRef.value.height = height;
176 + // 大小改变时可能需要重置星星位置,或者保留。这里选择不重置,只更新边界。
177 + }
178 +};
179 +
180 +onMounted(() => {
181 + if (canvasRef.value) {
182 + width = window.innerWidth;
183 + height = window.innerHeight;
184 + canvasRef.value.width = width;
185 + canvasRef.value.height = height;
186 + context = canvasRef.value.getContext('2d');
187 +
188 + initStars();
189 + render();
190 + triggerMeteor();
191 +
192 + window.addEventListener('resize', handleResize);
193 + }
194 +});
195 +
196 +onUnmounted(() => {
197 + if (animationFrameId) {
198 + cancelAnimationFrame(animationFrameId);
199 + }
200 + if (meteorTimeoutId) {
201 + clearTimeout(meteorTimeoutId);
202 + }
203 + window.removeEventListener('resize', handleResize);
204 +});
205 +</script>
206 +
207 +<style scoped>
208 +.canvas-box {
209 + position: fixed;
210 + left: 0;
211 + top: 0;
212 + width: 100vw;
213 + height: 100vh;
214 + z-index: -1;
215 + overflow: hidden;
216 +}
217 +</style>
1 +<!--
2 + * @Date: 2025-12-18 00:22:07
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2025-12-22 14:01:23
5 + * @FilePath: /mlaj/src/views/test.vue
6 + * @Description: 文件描述
7 +-->
1 <template> 8 <template>
2 <button @click="setVolume">设置音量</button> 9 <button @click="setVolume">设置音量</button>
3 <audio ref="audioRef" src="https://img.tukuppt.com/newpreview_music/09/03/95/5c8af46b01eb138909.mp3"></audio> 10 <audio ref="audioRef" src="https://img.tukuppt.com/newpreview_music/09/03/95/5c8af46b01eb138909.mp3"></audio>
11 +
12 + <!-- 使用封装的星空背景组件 -->
13 + <StarryBackground bgImage="https://cdn.ipadbiz.cn/mlaj/images/test-bgg03.jpg" />
4 </template> 14 </template>
5 15
6 <script setup> 16 <script setup>
7 import { ref } from 'vue'; 17 import { ref } from 'vue';
18 +import StarryBackground from '@/components/effects/StarryBackground.vue';
8 19
9 const audioRef = ref(null); 20 const audioRef = ref(null);
10 21
...@@ -36,3 +47,13 @@ const setVolume = async () => { ...@@ -36,3 +47,13 @@ const setVolume = async () => {
36 } 47 }
37 }; 48 };
38 </script> 49 </script>
50 +
51 +<style scoped>
52 +button {
53 + position: relative;
54 + z-index: 10;
55 + padding: 10px 20px;
56 + font-size: 16px;
57 + cursor: pointer;
58 +}
59 +</style>
......