update:优化特效

This commit is contained in:
2026-09-22 09:37:37 +08:00
parent 01e8f6c6da
commit e98e5129d2
7 changed files with 477 additions and 10 deletions
+399
View File
@@ -0,0 +1,399 @@
<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>
+17 -3
View File
@@ -2,6 +2,7 @@
import { computed, onMounted, onUnmounted, ref, watch } from "vue";
import { onLoad, onShow } from "@dcloudio/uni-app";
import Container from "@/components/Container.vue";
import DeviceOnlineEffects from "@/components/DeviceOnlineEffects.vue";
import Header from "@/components/Header.vue";
import ScreenHint from "@/components/ScreenHint.vue";
import ModalDialog from "@/components/ModalDialog.vue";
@@ -510,10 +511,17 @@ onShow(async () => {
src="../../static/home-device/device-platform.png"
mode="widthFix"
/>
<DeviceOnlineEffects
v-if="isDeviceOnline"
variant="detail"
/>
<image
class="device-bow"
src="../../static/home-device/device-bow.png"
mode="widthFix"
:class="{ 'device-bow--online': isDeviceOnline }"
:src="isDeviceOnline
? '../../static/home-device/device-bow--online.png'
: '../../static/home-device/device-bow.png'"
:mode="isDeviceOnline ? 'aspectFit' : 'widthFix'"
/>
</view>
@@ -948,7 +956,7 @@ onShow(async () => {
transform: translateY(0);
}
50% {
transform: translateY(-10rpx);
transform: translateY(-20rpx);
}
}
@@ -956,6 +964,7 @@ onShow(async () => {
position: absolute;
top: 214rpx;
left: 50%;
z-index: 1;
width: 750rpx;
transform: translateX(-50%);
}
@@ -964,10 +973,15 @@ onShow(async () => {
position: absolute;
top: 0;
left: 50%;
z-index: 4;
width: 562rpx;
transform: translateX(-50%);
}
.device-bow--online {
height: 466rpx;
}
.device-heading {
position: relative;
z-index: 2;
+61 -7
View File
@@ -6,6 +6,7 @@ import AppFooter from "@/components/AppFooter.vue";
import UserHeader from "@/components/UserHeader.vue";
import Signin from "@/components/Signin.vue";
import BubbleTip from "@/components/BubbleTip.vue";
import DeviceOnlineEffects from "@/components/DeviceOnlineEffects.vue";
import ModalDialog from "@/components/ModalDialog.vue";
import OtaModal from "@/components/OtaModal.vue";
@@ -60,6 +61,7 @@ const deviceCardAssets = {
},
online: {
bow: "../static/home-device/device-bow--online.png",
platform: "../static/home-device/device-platform-glow--online.png",
background: "../static/home-device/device-card-bg--bound.png",
actionBackground: "../static/home-device/device-action-bg--device.png",
},
@@ -483,8 +485,28 @@ onShareTimeline(() => {
mode="scaleToFill"
/>
<image
v-if="deviceCardState === 'online'"
class="device-visual-platform-glow"
:src="deviceCardAsset.platform"
mode="scaleToFill"
/>
<DeviceOnlineEffects
v-if="deviceCardState === 'online'"
variant="home"
/>
<view
v-if="deviceCardState === 'online'"
class="device-visual-float-group"
>
<image
class="device-visual-bow device-visual-bow--online"
:src="deviceCardAsset.bow"
mode="scaleToFill"
/>
</view>
<image
v-else
class="device-visual-bow"
:class="{ 'device-visual-bow--floating': deviceCardState === 'online' }"
:src="deviceCardAsset.bow"
mode="scaleToFill"
/>
@@ -686,6 +708,8 @@ onShareTimeline(() => {
}
.device-visual-background,
.device-visual-platform-glow,
.device-visual-float-group,
.device-visual-bow {
display: flex;
position: absolute;
@@ -697,6 +721,26 @@ onShareTimeline(() => {
left: 0;
width: 100%;
height: 100%;
z-index: 0;
}
.device-visual-platform-glow {
left: 0;
bottom: 0;
z-index: 2;
width: 100%;
height: 100%;
transform-origin: center bottom;
animation: device-platform-glow 3.6s ease-in-out -1.1s infinite;
}
.device-visual-float-group {
top: 0;
left: 0;
z-index: 5;
width: 100%;
height: 388rpx;
animation: device-bow-float 3s ease-in-out infinite;
}
.device-visual-bow {
@@ -706,17 +750,27 @@ onShareTimeline(() => {
height: 388rpx;
}
.device-visual-bow--floating {
animation: device-bow-float 3s ease-in-out infinite;
.device-visual-bow--online {
z-index: 1;
}
@keyframes device-bow-float {
0%,
100% {
transform: translateY(0) rotate(-0.5deg);
transform: translateY(0);
}
50% {
transform: translateY(-10rpx) rotate(0.5deg);
transform: translateY(-20rpx);
}
}
@keyframes device-platform-glow {
0%,
100% {
opacity: 0.92;
}
50% {
opacity: 1;
}
}
@@ -730,7 +784,7 @@ onShareTimeline(() => {
position: absolute;
top: 10rpx;
right: 14rpx;
z-index: 2;
z-index: 6;
height: 34rpx;
color: #ffffff;
font-family: Inter-Regular, sans-serif;
@@ -802,7 +856,7 @@ onShareTimeline(() => {
position: absolute;
bottom: 28rpx;
left: 50%;
z-index: 2;
z-index: 6;
width: 218rpx;
height: 56rpx;
justify-content: center;
Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 376 KiB