Files
shoot-miniprograms/src/components/DeviceOnlineEffects.vue
T
2026-09-22 09:37:37 +08:00

400 lines
8.5 KiB
Vue

<script setup>
import { computed } from "vue";
const props = defineProps({
variant: {
type: String,
default: "home",
validator: (value) => ["home", "detail"].includes(value),
},
});
// 固定粒子分布营造随机感,避免每次进入页面时因运行时随机数产生位置跳变。
const sparkSeeds = [
[1, "dot", 18, 98, -28, 5, 5, 2.3, 0.4],
[2, "streak", 23, 90, -38, 4, 18, 2, 1.1, true],
[3, "dot", 28, 112, -17, 6, 6, 2.8, 1.9, true],
[4, "dot", 32, 104, -12, 12, 12, 2.6, 0.7, true, true],
[5, "dot", 36, 88, -24, 4, 4, 1.8, 1.4],
[6, "streak", 40, 116, -8, 4, 20, 2.4, 2.2, true],
[7, "dot", 44, 96, -16, 7, 7, 2.1, 0.2],
[8, "dot", 48, 120, -4, 5, 5, 2.9, 1.3, true],
[9, "dot", 51, 110, 3, 14, 14, 3, 2.7, true, true],
[10, "streak", 54, 92, 8, 4, 22, 1.9, 0.8],
[11, "dot", 58, 105, 14, 6, 6, 2.5, 1.6, true],
[12, "dot", 62, 86, 21, 4, 4, 2, 0.5],
[13, "dot", 66, 114, 10, 7, 7, 2.7, 2.1, true],
[14, "streak", 70, 96, 28, 4, 18, 2.2, 1],
[15, "dot", 74, 108, 20, 11, 11, 2.8, 2.4, true, true],
[16, "dot", 80, 90, 35, 5, 5, 2.3, 0.3, true],
[17, "dot", 21, 118, -8, 4, 4, 2.6, 1.7],
[18, "streak", 27, 84, -31, 3, 16, 1.8, 0.6, true],
[19, "dot", 34, 122, -3, 6, 6, 2.9, 2.3, true],
[20, "dot", 42, 102, 5, 5, 5, 2.2, 0.9],
[21, "dot", 47, 88, -10, 13, 13, 2.7, 1.5, true, true],
[22, "streak", 56, 120, 16, 4, 21, 2.5, 2.6, true],
[23, "dot", 64, 100, 24, 7, 7, 2.4, 0.1],
[24, "dot", 72, 84, 38, 5, 5, 2, 1.2, true],
[25, "dot", 82, 116, 12, 4, 4, 2.8, 2],
[26, "dot", 68, 124, 7, 10, 10, 3.1, 2.9, true, true],
[27, "streak", 77, 98, 33, 4, 19, 2.1, 0.7, true],
[28, "dot", 29, 94, -20, 7, 7, 2.5, 1.4],
[29, "dot", 53, 126, 0, 5, 5, 2.3, 2.2, true],
[30, "dot", 60, 108, 18, 6, 6, 2.6, 0.4],
];
const deviceSparks = computed(() => {
const isDetail = props.variant === "detail";
const sizeScale = isDetail ? 1.25 : 1;
const bottomOffset = isDetail ? 120 : 0;
return sparkSeeds.map(([
id,
type,
left,
bottom,
angle,
width,
height,
duration,
delay,
long = false,
large = false,
]) => ({
id,
type,
long,
large,
trackStyle: {
left: `${left}%`,
bottom: `${bottom + bottomOffset}rpx`,
transform: `rotate(${angle}deg)`,
},
sparkStyle: {
width: `${width * sizeScale}rpx`,
height: `${height * sizeScale}rpx`,
marginLeft: `${-(width * sizeScale) / 2}rpx`,
animationDuration: `${duration}s`,
animationDelay: `-${delay}s`,
},
}));
});
</script>
<template>
<view
class="device-online-effects"
:class="`device-online-effects--${variant}`"
>
<view class="device-online-rays-stage">
<view class="device-online-rays-orbit">
<view class="device-online-ray device-online-ray--1" />
<view class="device-online-ray device-online-ray--2" />
<view class="device-online-ray device-online-ray--3" />
<view class="device-online-ray device-online-ray--4" />
<view class="device-online-ray device-online-ray--5" />
<view class="device-online-ray device-online-ray--6" />
<view class="device-online-ray device-online-ray--7" />
</view>
</view>
<view class="device-online-particles">
<view
v-for="spark in deviceSparks"
:key="spark.id"
class="device-online-spark-track"
:style="spark.trackStyle"
>
<view
class="device-online-spark"
:class="[
`device-online-spark--${spark.type}`,
{
'device-online-spark--long': spark.long,
'device-online-spark--large': spark.large,
}
]"
:style="spark.sparkStyle"
/>
</view>
</view>
</view>
</template>
<style scoped lang="scss">
.device-online-effects {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 3;
overflow: hidden;
pointer-events: none;
}
/* 光束沿底座椭圆轨迹扫动,用位移、明暗和横向缩放表达前后纵深。 */
.device-online-rays-stage {
position: absolute;
left: 0;
bottom: 112rpx;
width: 100%;
height: 140rpx;
overflow: hidden;
transform-origin: 50% 100%;
}
.device-online-rays-orbit {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
transform-origin: 50% 100%;
animation: device-rays-rotate 6s ease-in-out infinite;
}
.device-online-ray {
position: absolute;
bottom: 0;
left: 50%;
width: 48rpx;
height: 132rpx;
margin-left: -24rpx;
transform-origin: 50% 100%;
border-radius: 50% 50% 12% 12%;
background: linear-gradient(
180deg,
rgba(255, 255, 244, 0) 0%,
rgba(255, 254, 239, 0.1) 36%,
rgba(255, 248, 215, 0.22) 72%,
rgba(255, 232, 151, 0.36) 100%
);
}
.device-online-ray--1 {
opacity: 0.55;
transform: rotate(-55deg);
}
.device-online-ray--2 {
width: 68rpx;
margin-left: -34rpx;
opacity: 0.72;
transform: rotate(-36deg);
}
.device-online-ray--3 {
width: 42rpx;
margin-left: -21rpx;
opacity: 0.6;
transform: rotate(-18deg);
}
.device-online-ray--4 {
width: 76rpx;
margin-left: -38rpx;
opacity: 0.82;
}
.device-online-ray--5 {
width: 46rpx;
margin-left: -23rpx;
opacity: 0.64;
transform: rotate(19deg);
}
.device-online-ray--6 {
width: 64rpx;
margin-left: -32rpx;
opacity: 0.7;
transform: rotate(38deg);
}
.device-online-ray--7 {
width: 44rpx;
margin-left: -22rpx;
opacity: 0.56;
transform: rotate(56deg);
}
.device-online-particles {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
.device-online-spark-track {
position: absolute;
width: 0;
height: 0;
transform-origin: center bottom;
}
.device-online-spark {
position: absolute;
bottom: 0;
left: 0;
background: #fff6bd;
box-shadow: 0 0 6rpx rgba(255, 225, 112, 0.65);
animation: device-spark-rise 2.4s linear infinite;
}
.device-online-spark--dot {
border-radius: 50%;
}
.device-online-spark--streak {
border-radius: 50%;
background: linear-gradient(
180deg,
rgba(255, 255, 235, 0) 0%,
rgba(255, 250, 205, 0.75) 45%,
#ffe379 100%
);
}
.device-online-spark--long {
animation-name: device-spark-rise-long;
}
.device-online-spark--large {
background: #fffbdc;
box-shadow:
0 0 10rpx rgba(255, 243, 174, 0.95),
0 0 20rpx rgba(255, 207, 74, 0.72);
}
.device-online-effects--detail .device-online-rays-stage {
bottom: 215rpx;
height: 190rpx;
transform: scaleX(1.35);
}
.device-online-effects--detail .device-online-ray {
height: 180rpx;
}
.device-online-effects--detail .device-online-rays-orbit {
animation-name: device-rays-rotate-detail;
}
.device-online-effects--detail .device-online-spark {
animation-name: device-spark-rise-detail;
}
.device-online-effects--detail .device-online-spark--long {
animation-name: device-spark-rise-detail-long;
}
@keyframes device-spark-rise {
0% {
opacity: 0;
transform: translateY(8rpx) scale(0.45);
}
16% {
opacity: 1;
}
72% {
opacity: 0.62;
}
100% {
opacity: 0;
transform: translateY(-148rpx) scale(1);
}
}
@keyframes device-spark-rise-long {
0% {
opacity: 0;
transform: translateY(8rpx) scale(0.4);
}
16% {
opacity: 1;
}
72% {
opacity: 0.55;
}
100% {
opacity: 0;
transform: translateY(-176rpx) scale(1);
}
}
@keyframes device-spark-rise-detail {
0% {
opacity: 0;
transform: translateY(12rpx) scale(0.45);
}
16% {
opacity: 1;
}
72% {
opacity: 0.62;
}
100% {
opacity: 0;
transform: translateY(-225rpx) scale(1);
}
}
@keyframes device-spark-rise-detail-long {
0% {
opacity: 0;
transform: translateY(12rpx) scale(0.4);
}
16% {
opacity: 1;
}
72% {
opacity: 0.55;
}
100% {
opacity: 0;
transform: translateY(-270rpx) scale(1);
}
}
@keyframes device-rays-rotate {
0%,
100% {
opacity: 0.48;
transform: translate(-42rpx, 4rpx) skewX(-8deg) scaleX(0.82);
}
25% {
opacity: 0.88;
transform: translate(0, 0) skewX(0deg) scaleX(1.05);
}
50% {
opacity: 0.48;
transform: translate(42rpx, 4rpx) skewX(8deg) scaleX(0.82);
}
75% {
opacity: 0.24;
transform: translate(0, 10rpx) skewX(0deg) scaleX(0.68);
}
}
@keyframes device-rays-rotate-detail {
0%,
100% {
opacity: 0.48;
transform: translate(-62rpx, 6rpx) skewX(-8deg) scaleX(0.82);
}
25% {
opacity: 0.88;
transform: translate(0, 0) skewX(0deg) scaleX(1.05);
}
50% {
opacity: 0.48;
transform: translate(62rpx, 6rpx) skewX(8deg) scaleX(0.82);
}
75% {
opacity: 0.24;
transform: translate(0, 14rpx) skewX(0deg) scaleX(0.68);
}
}
</style>