1. Design Token 统一:CategoryIcon/BudgetBar/Numpad/Skeleton/TransactionItem 硬编码hex颜色替换为 $primary/$border/$danger 等SCSS变量 2. 入场动画:mixins.scss 添加 fade-in-up/bounce-in/count-up 等 mixin+keyframes, 首页卡片/金额/快捷按钮应用交错入场动画 3. 记账成功动画增强:SaveSuccess 增加纸屑粒子爆发+光环脉冲效果, prefers-reduced-motion 下自动隐藏 4. 骨架屏替代文字加载:recurring/group-manage/notifications 三个页面 从"加载中..."替换为骨架屏;空状态增加图标容器+引导文案 5. 交互微反馈:分类按压0.92缩放、推荐虚线呼吸动画、 Tab滑块cubic-bezier优化、快捷按钮弹性按压 6. 左滑删除提示:TransactionItem首项1.5s后自动微微左滑再回弹, 引导用户发现滑动删除功能
238 lines
5.2 KiB
Vue
238 lines
5.2 KiB
Vue
<template>
|
|
<view class="save-success" v-if="visible">
|
|
<!-- 纸屑粒子 -->
|
|
<view class="confetti-container" v-if="animating">
|
|
<view
|
|
v-for="i in 20"
|
|
:key="i"
|
|
class="confetti"
|
|
:class="`confetti-${i}`"
|
|
:style="confettiStyle(i)"
|
|
></view>
|
|
</view>
|
|
|
|
<view class="success-content" :class="{ show: animating }">
|
|
<view class="success-icon-wrap">
|
|
<view class="success-ring"></view>
|
|
<view class="success-icon">
|
|
<Icon name="check" :size="80" color="#FFFFFF" />
|
|
</view>
|
|
</view>
|
|
<text class="success-text">{{ text }}</text>
|
|
<view class="success-actions" v-if="showContinue">
|
|
<view class="action-btn continue" @tap="onContinue">
|
|
<text class="action-text">继续记一笔</text>
|
|
</view>
|
|
<view class="action-btn back" @tap="onBack">
|
|
<text class="action-text">返回</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, watch } from 'vue'
|
|
import Icon from '@/components/Icon/Icon.vue'
|
|
|
|
const props = withDefaults(defineProps<{
|
|
visible: boolean
|
|
text?: string
|
|
showContinue?: boolean
|
|
}>(), { text: '保存成功', showContinue: false })
|
|
|
|
const emit = defineEmits(['update:visible', 'continue', 'back'])
|
|
|
|
const animating = ref(false)
|
|
|
|
// 纸屑样式:随机位置、颜色、动画延迟
|
|
const confettiColors = ['#FF8C69', '#FFD166', '#7BC67E', '#6C9BCF', '#FF69B4', '#FFB89A']
|
|
function confettiStyle(i: number) {
|
|
const angle = (i / 20) * 360
|
|
const distance = 120 + Math.random() * 200
|
|
const color = confettiColors[i % confettiColors.length]
|
|
const delay = Math.random() * 0.3
|
|
const size = 8 + Math.random() * 12
|
|
const isRect = i % 3 === 0
|
|
return {
|
|
'--tx': `${Math.cos(angle * Math.PI / 180) * distance}rpx`,
|
|
'--ty': `${Math.sin(angle * Math.PI / 180) * distance - 100}rpx`,
|
|
'--rot': `${Math.random() * 720 - 360}deg`,
|
|
background: color,
|
|
width: isRect ? `${size}rpx` : `${size * 0.6}rpx`,
|
|
height: `${size}rpx`,
|
|
borderRadius: isRect ? '2rpx' : '50%',
|
|
animationDelay: `${delay}s`,
|
|
}
|
|
}
|
|
|
|
watch(() => props.visible, (val) => {
|
|
if (val) {
|
|
setTimeout(() => { animating.value = true }, 50)
|
|
// 如果不显示继续按钮,自动关闭
|
|
if (!props.showContinue) {
|
|
setTimeout(() => { onBack() }, 2200)
|
|
}
|
|
} else {
|
|
animating.value = false
|
|
}
|
|
})
|
|
|
|
function onContinue() {
|
|
animating.value = false
|
|
setTimeout(() => {
|
|
emit('update:visible', false)
|
|
emit('continue')
|
|
}, 300)
|
|
}
|
|
|
|
function onBack() {
|
|
animating.value = false
|
|
setTimeout(() => {
|
|
emit('update:visible', false)
|
|
emit('back')
|
|
}, 300)
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
@import '@/styles/mixins.scss';
|
|
|
|
.save-success {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
@include flex-center;
|
|
background: rgba(0, 0, 0, 0.5);
|
|
z-index: 9999;
|
|
}
|
|
|
|
// 纸屑容器
|
|
.confetti-container {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
width: 0;
|
|
height: 0;
|
|
z-index: 1;
|
|
}
|
|
|
|
.confetti {
|
|
position: absolute;
|
|
opacity: 0;
|
|
animation: confetti-burst 0.8s cubic-bezier(0.23, 1, 0.32, 1) forwards;
|
|
}
|
|
|
|
@keyframes confetti-burst {
|
|
0% {
|
|
transform: translate(0, 0) rotate(0deg) scale(0);
|
|
opacity: 0;
|
|
}
|
|
20% {
|
|
opacity: 1;
|
|
}
|
|
100% {
|
|
transform: translate(var(--tx), var(--ty)) rotate(var(--rot)) scale(1);
|
|
opacity: 0;
|
|
}
|
|
}
|
|
|
|
.success-content {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
transform: scale(0.8);
|
|
opacity: 0;
|
|
transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
z-index: 2;
|
|
|
|
&.show {
|
|
transform: scale(1);
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
// 图标外环脉冲
|
|
.success-icon-wrap {
|
|
position: relative;
|
|
@include flex-center;
|
|
}
|
|
|
|
.success-ring {
|
|
position: absolute;
|
|
width: 200rpx;
|
|
height: 200rpx;
|
|
border-radius: 50%;
|
|
border: 4rpx solid rgba(123, 198, 126, 0.3);
|
|
animation: ring-pulse 1s ease-out 0.3s both;
|
|
}
|
|
|
|
@keyframes ring-pulse {
|
|
0% {
|
|
transform: scale(0.8);
|
|
opacity: 0;
|
|
}
|
|
50% {
|
|
opacity: 1;
|
|
}
|
|
100% {
|
|
transform: scale(1.5);
|
|
opacity: 0;
|
|
}
|
|
}
|
|
|
|
.success-icon {
|
|
width: 160rpx;
|
|
height: 160rpx;
|
|
border-radius: 50%;
|
|
background: linear-gradient(135deg, $success, darken($success, 10%));
|
|
@include flex-center;
|
|
box-shadow: 0 8rpx 32rpx rgba(123, 198, 126, 0.4);
|
|
}
|
|
|
|
.success-text {
|
|
font-size: $font-xl;
|
|
font-weight: 600;
|
|
color: $surface;
|
|
margin-top: $space-lg;
|
|
}
|
|
|
|
.success-actions {
|
|
display: flex;
|
|
gap: $space-md;
|
|
margin-top: $space-2xl;
|
|
}
|
|
|
|
.action-btn {
|
|
padding: $space-sm $space-xl;
|
|
border-radius: $radius-xl;
|
|
min-width: 200rpx;
|
|
@include flex-center;
|
|
transition: transform $transition-fast, opacity $transition-fast;
|
|
|
|
&.continue {
|
|
background: $primary;
|
|
&:active { opacity: 0.8; transform: scale(0.96); }
|
|
}
|
|
|
|
&.back {
|
|
background: rgba(255, 255, 255, 0.2);
|
|
&:active { background: rgba(255, 255, 255, 0.3); transform: scale(0.96); }
|
|
}
|
|
}
|
|
|
|
.action-text {
|
|
font-size: $font-lg;
|
|
font-weight: 600;
|
|
color: $surface;
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.confetti { animation: none !important; display: none; }
|
|
.success-ring { animation: none !important; display: none; }
|
|
.success-content { transition: opacity 0.2s ease; }
|
|
}
|
|
</style>
|