Compare commits

..

1 Commits

Author SHA1 Message Date
324a3ec5c9 fix: 键盘收起后添加底部保存按钮
- 记账页面添加底部保存按钮,键盘收起时显示
- 预算页面添加底部保存按钮,键盘收起时显示
- 按钮根据编辑/新增模式显示不同文案
- 正确处理底部安全区适配
2026-06-15 16:45:18 +08:00
13 changed files with 156 additions and 366 deletions

View File

@@ -1,7 +1,7 @@
<template>
<view class="budget-bar-wrap">
<view class="bar-track">
<view class="bar-fill" :class="{ over: rawPercentage > 100 }" :style="{ width: percentage + '%' }"></view>
<view class="bar-fill" :style="{ width: percentage + '%', background: fillColor }"></view>
</view>
<view class="bar-info" v-if="showLabel">
<text class="bar-text" v-if="rawPercentage <= 100">预算 {{ formatAmount(total) }} · 剩余 {{ formatAmount(Math.max(0, total - used)) }}</text>
@@ -27,25 +27,25 @@ const rawPercentage = computed(() => {
})
const percentage = computed(() => Math.min(100, rawPercentage.value))
const fillColor = computed(() => {
if (rawPercentage.value > 100) return '#FF6B6B'
return 'linear-gradient(90deg, #FF8C69, #FFB89A)'
})
</script>
<style lang="scss" scoped>
.bar-track {
height: 16rpx;
border-radius: 8rpx;
background: $border;
background: #F0E0D6;
overflow: hidden;
}
.bar-fill {
height: 100%;
border-radius: 8rpx;
background: linear-gradient(90deg, $primary, lighten($primary, 15%));
transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1);
&.over {
background: $danger;
}
transition: width 0.6s ease-out;
}
.bar-info {
@@ -56,20 +56,20 @@ const percentage = computed(() => Math.min(100, rawPercentage.value))
.bar-text {
font-size: 24rpx;
color: $text-sec;
color: #8B7E7E;
}
.bar-pct {
font-family: 'Fredoka', sans-serif;
font-size: 28rpx;
font-weight: 500;
color: $primary;
color: #FF8C69;
&.over { color: $danger; }
&.over { color: #FF6B6B; }
}
.bar-text.over {
color: $danger;
color: #FF6B6B;
font-weight: 500;
}
</style>

View File

@@ -22,7 +22,6 @@ const iconStyle = computed(() => ({
background: props.selected ? '#FFE8E0' : (props.bgColor || (props.color + '15')),
width: (sizeMap[props.size || 'md']) + 'rpx',
height: (sizeMap[props.size || 'md']) + 'rpx',
boxShadow: props.selected ? '0 0 0 4rpx rgba(255, 140, 105, 0.15)' : 'none',
}))
const fontSize = computed(() => (fontMap[props.size || 'md']) + 'rpx')
@@ -43,7 +42,6 @@ const fontSize = computed(() => (fontMap[props.size || 'md']) + 'rpx')
}
.selected {
border-color: $primary;
transform: scale(1.05);
border-color: #FF8C69;
}
</style>

View File

@@ -207,10 +207,10 @@ function onConfirm() {
.key.fn {
background: $primary-light;
border-color: darken($primary-light, 8%);
border-color: #FFD0C0;
&:active {
background: darken($primary-light, 8%);
background: #FFD0C0;
box-shadow: $shadow-clay-pressed;
transform: scale(0.96);
}

View File

@@ -1,22 +1,8 @@
<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 class="success-icon">
<Icon name="check" :size="80" color="#FFFFFF" />
</view>
<text class="success-text">{{ text }}</text>
<view class="success-actions" v-if="showContinue">
@@ -45,33 +31,12 @@ 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)
setTimeout(() => { onBack() }, 2000)
}
} else {
animating.value = false
@@ -109,44 +74,13 @@ function onBack() {
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;
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
&.show {
transform: scale(1);
@@ -154,42 +88,12 @@ function onBack() {
}
}
// 图标外环脉冲
.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%));
background: linear-gradient(135deg, $success, #5BA85E);
@include flex-center;
box-shadow: 0 8rpx 32rpx rgba(123, 198, 126, 0.4);
}
.success-text {
@@ -210,16 +114,15 @@ function onBack() {
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); }
&:active { opacity: 0.8; }
}
&.back {
background: rgba(255, 255, 255, 0.2);
&:active { background: rgba(255, 255, 255, 0.3); transform: scale(0.96); }
&:active { background: rgba(255, 255, 255, 0.3); }
}
}
@@ -228,10 +131,4 @@ function onBack() {
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>

View File

@@ -19,7 +19,7 @@ const style = computed(() => ({
<style lang="scss" scoped>
.skeleton {
background: linear-gradient(90deg, $border 25%, $surface-warm 50%, $border 75%);
background: linear-gradient(90deg, #F0E0D6 25%, #F8EDE4 50%, #F0E0D6 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-radius: 8rpx;

View File

@@ -108,8 +108,6 @@ function onTouchEnd() {
</script>
<style lang="scss" scoped>
@import '@/styles/mixins.scss';
.tx-item-wrap {
position: relative;
overflow: hidden;
@@ -123,33 +121,22 @@ function onTouchEnd() {
gap: 24rpx;
border-radius: 20rpx;
transition: transform 0.2s ease;
background: $surface;
background: #FFFFFF;
position: relative;
z-index: 1;
&:active {
background: $surface-warm;
background: #FFF0E6;
}
}
// 左滑提示:首次加载时微微露出删除按钮
.tx-item-wrap:first-child .tx-item:not(.no-hint) {
animation: swipe-hint 0.6s ease 1.5s both;
}
@keyframes swipe-hint {
0% { transform: translateX(0); }
40% { transform: translateX(-40rpx); }
100% { transform: translateX(0); }
}
.delete-btn {
position: absolute;
right: 0;
top: 0;
bottom: 0;
width: 160rpx;
background: $danger;
background: #FF6B6B;
display: flex;
align-items: center;
justify-content: center;
@@ -182,9 +169,9 @@ function onTouchEnd() {
width: 28rpx;
height: 28rpx;
border-radius: 50%;
background: $primary-light;
background: #FFF0E6;
font-size: 16rpx;
color: $primary;
color: #FF8C69;
align-items: center;
justify-content: center;
flex-shrink: 0;
@@ -198,20 +185,22 @@ function onTouchEnd() {
.name {
font-size: 28rpx;
font-weight: 500;
color: $text;
color: #2D1B1B;
display: block;
@include text-ellipsis;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.meta {
font-size: 24rpx;
color: $text-sec;
color: #8B7E7E;
display: block;
margin-top: 4rpx;
}
.creator-name {
color: $primary;
color: #FF8C69;
font-weight: 500;
}
@@ -222,13 +211,7 @@ function onTouchEnd() {
font-variant-numeric: tabular-nums;
white-space: nowrap;
&.expense { color: $text; }
&.income { color: $success; }
}
@media (prefers-reduced-motion: reduce) {
.tx-item-wrap:first-child .tx-item {
animation: none;
}
&.expense { color: #2D1B1B; }
&.income { color: #7BC67E; }
}
</style>

View File

@@ -25,6 +25,8 @@
:fixed="true"
size="large"
:auto-focus="true"
@focus="keyboardVisible = true"
@blur="keyboardVisible = false"
@confirm="save"
/>
@@ -55,7 +57,12 @@
</view>
</view>
<!-- 底部保存按钮键盘收起时显示 -->
<view class="save-bar" v-if="!showSuccess && !keyboardVisible">
<view class="save-btn" @tap="save">
<text class="save-btn-text">{{ editId ? '保存修改' : '保存' }}</text>
</view>
</view>
<SaveSuccess
v-model:visible="showSuccess"
@@ -99,6 +106,8 @@ const showSuccess = ref(false)
const scrollToCat = ref('')
const todayStr = localToday
const amountEditorRef = ref<InstanceType<typeof AmountEditor> | null>(null)
const keyboardVisible = ref(false)
// Remember last selected category per type
const lastCatByType: Record<string, number | null> = { expense: null, income: null }
@@ -301,9 +310,46 @@ function goBack() {
display: flex;
flex-direction: column;
// AmountEditor fixed Numpad 占位: 4行×96rpx + 3间隙×16rpx + padding×2×16rpx = 464rpx
// 同时留出保存按钮的位置,确保内容不被遮挡
$numpad-h: 464rpx;
padding-bottom: calc(#{$numpad-h} + constant(safe-area-inset-bottom));
padding-bottom: calc(#{$numpad-h} + env(safe-area-inset-bottom));
$save-bar-h: 144rpx;
padding-bottom: calc(#{$numpad-h} + #{$save-bar-h} + constant(safe-area-inset-bottom));
padding-bottom: calc(#{$numpad-h} + #{$save-bar-h} + env(safe-area-inset-bottom));
}
// 底部保存按钮栏
.save-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: $space-md $space-xl;
padding-bottom: calc(#{$space-md} + constant(safe-area-inset-bottom));
padding-bottom: calc(#{$space-md} + env(safe-area-inset-bottom));
background: $surface;
border-top: 2rpx solid $border;
z-index: 50;
}
.save-btn {
height: 96rpx;
background: linear-gradient(135deg, $primary, #E67355);
border-radius: $radius-xl;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 12rpx rgba(255, 140, 105, 0.3);
&:active {
opacity: 0.8;
transform: scale(0.98);
}
}
.save-btn-text {
font-size: $font-xl;
font-weight: 600;
color: $surface;
}
.header-fixed {
@@ -328,11 +374,7 @@ function goBack() {
.title { font-size: $font-xl; font-weight: 500; color: $text; }
.type-toggle-wrap {
display: flex; justify-content: center;
margin: $space-sm 0;
@include fade-in-up;
}
.type-toggle-wrap { display: flex; justify-content: center; margin: $space-sm 0; }
.type-toggle {
display: inline-flex;
@@ -367,7 +409,7 @@ function goBack() {
background: $surface;
border-radius: $radius-lg;
box-shadow: $shadow-sm;
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
transition: transform $transition-normal;
}
.income .slider { transform: translateX(160rpx); }
@@ -391,9 +433,8 @@ function goBack() {
gap: 6rpx;
padding: 12rpx 0;
position: relative;
-webkit-tap-highlight-color: transparent;
&:active { transform: scale(0.92); }
&:active { transform: scale(0.95); }
&.suggested {
.cat-icon-wrap {
@@ -401,16 +442,10 @@ function goBack() {
border-radius: $radius-lg;
padding: 6rpx;
margin: -6rpx;
animation: suggest-pulse 2s ease-in-out infinite;
}
}
}
@keyframes suggest-pulse {
0%, 100% { border-color: $primary; }
50% { border-color: lighten($primary, 30%); }
}
.cat-icon-wrap {
position: relative;
transition: all $transition-normal;

View File

@@ -67,9 +67,18 @@
size="large"
unit="元"
:auto-focus="false"
@focus="keyboardVisible = true"
@blur="keyboardVisible = false"
@confirm="onConfirm"
/>
</view>
<!-- 底部保存按钮键盘收起时显示 -->
<view class="save-bar" v-if="!keyboardVisible">
<view class="save-btn" @tap="onConfirm">
<text class="save-btn-text">保存预算</text>
</view>
</view>
</view>
</template>
@@ -98,6 +107,7 @@ const loading = ref(true)
const initialLoaded = ref(false)
const amountStr = ref('')
const amountEditorRef = ref<InstanceType<typeof AmountEditor> | null>(null)
const keyboardVisible = ref(false)
const presets = [1000, 2000, 3000, 5000, 10000]
@@ -197,10 +207,11 @@ function goBack() {
.page {
min-height: 100vh;
background: $bg;
// AmountEditor fixed Numpad 占位: 4行×96rpx + 3间隙×16rpx + padding×2×16rpx = 464rpx
// AmountEditor fixed Numpad 占位 + 保存按钮占位
$numpad-h: 464rpx;
padding-bottom: calc(#{$numpad-h} + constant(safe-area-inset-bottom));
padding-bottom: calc(#{$numpad-h} + env(safe-area-inset-bottom));
$save-bar-h: 144rpx;
padding-bottom: calc(#{$numpad-h} + #{$save-bar-h} + constant(safe-area-inset-bottom));
padding-bottom: calc(#{$numpad-h} + #{$save-bar-h} + env(safe-area-inset-bottom));
}
.header-fixed {
@@ -372,6 +383,40 @@ function goBack() {
color: $text;
}
// 底部保存按钮栏
.save-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: $space-md $space-xl;
padding-bottom: calc(#{$space-md} + constant(safe-area-inset-bottom));
padding-bottom: calc(#{$space-md} + env(safe-area-inset-bottom));
background: $surface;
border-top: 2rpx solid $border;
z-index: 50;
}
.save-btn {
height: 96rpx;
background: linear-gradient(135deg, $primary, #E67355);
border-radius: $radius-xl;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 12rpx rgba(255, 140, 105, 0.3);
&:active {
opacity: 0.8;
transform: scale(0.98);
}
}
.save-btn-text {
font-size: $font-xl;
font-weight: 600;
color: $surface;
}
@media (prefers-reduced-motion: reduce) {

View File

@@ -45,22 +45,13 @@
<text class="section-title">我的群组</text>
</view>
<view v-if="groupStore.loading" class="skeleton-list">
<view v-for="i in 2" :key="i" class="skeleton-card">
<Skeleton width="80rpx" height="80rpx" variant="circle" />
<view class="skeleton-info">
<Skeleton width="200rpx" height="28rpx" variant="text" />
<Skeleton width="280rpx" height="24rpx" variant="text" />
</view>
</view>
<view v-if="groupStore.loading" class="loading-box">
<text class="loading-text">加载中...</text>
</view>
<view v-else-if="groupStore.groups.length === 0" class="empty-box">
<view class="empty-icon-wrap">
<Icon name="user" :size="48" color="#BFB3B3" />
</view>
<Icon name="user" :size="48" color="#BFB3B3" />
<text class="empty-text">还没有加入任何群组</text>
<text class="empty-hint">创建或加入一个群组和家人朋友一起记账</text>
</view>
<view v-else class="group-list">
@@ -156,7 +147,6 @@ import { waitForReady } from '@/utils/app-ready'
import { statusBarHeight, capsuleRight } from '@/utils/system'
import { API_BASE } from '@/config'
import Icon from '@/components/Icon/Icon.vue'
import Skeleton from '@/components/Skeleton/Skeleton.vue'
import type { GroupMember, Group } from '@/api/group'
const groupStore = useGroupStore()
@@ -483,20 +473,7 @@ function handleDissolve(group: Group) {
color: $text-sec;
}
// 骨架屏
.skeleton-list { padding: 0 $space-xl; }
.skeleton-card {
display: flex; align-items: center; gap: $space-md;
padding: $space-lg; margin-bottom: $space-sm;
background: $surface; border-radius: $radius-xl;
border: 2rpx solid $border;
}
.skeleton-info {
flex: 1; display: flex; flex-direction: column; gap: $space-xs;
}
// 空状态
.empty-box {
.loading-box, .empty-box {
display: flex;
flex-direction: column;
align-items: center;
@@ -505,20 +482,8 @@ function handleDissolve(group: Group) {
padding: 80rpx 0;
}
.empty-icon-wrap {
width: 120rpx; height: 120rpx; border-radius: 50%;
background: $surface-warm; @include flex-center;
margin-bottom: $space-sm;
}
.empty-text {
font-size: $font-xl;
font-weight: 600;
color: $text;
}
.empty-hint {
font-size: $font-md;
.loading-text, .empty-text {
font-size: $font-lg;
color: $text-muted;
}

View File

@@ -364,7 +364,6 @@ function goBills() {
padding: $space-2xl;
@include card($radius-2xl);
box-shadow: $shadow-clay;
@include fade-in-up;
}
.card-label {
@@ -394,7 +393,6 @@ function goBills() {
font-weight: 600;
color: $text;
font-variant-numeric: tabular-nums;
@include count-up;
}
.mini-stats {
@@ -436,7 +434,6 @@ function goBills() {
display: flex;
align-items: center;
gap: $space-md;
@include fade-in-up(0.1s);
}
.today-item {
@@ -478,7 +475,6 @@ function goBills() {
gap: $space-md;
padding: 0 $space-xl;
margin-top: $space-xl;
@include fade-in-up(0.15s);
}
.quick-btn {
@@ -489,10 +485,9 @@ function goBills() {
flex-direction: column;
@include flex-center;
gap: $space-xs;
transition: transform $transition-fast, box-shadow $transition-fast;
&:active {
transform: scale(0.94);
transform: scale(0.97);
box-shadow: $shadow-clay-pressed;
}
}

View File

@@ -23,23 +23,13 @@
</view>
<!-- 通知列表 -->
<view v-if="loading && list.length === 0" class="skeleton-list">
<view v-for="i in 4" :key="i" class="skeleton-notif">
<Skeleton width="64rpx" height="64rpx" variant="circle" />
<view class="skeleton-info">
<Skeleton width="240rpx" height="28rpx" variant="text" />
<Skeleton width="400rpx" height="24rpx" variant="text" />
<Skeleton width="120rpx" height="20rpx" variant="text" />
</view>
</view>
<view v-if="loading && list.length === 0" class="loading-box">
<text class="loading-text">加载中...</text>
</view>
<view v-else-if="list.length === 0" class="empty-box">
<view class="empty-icon-wrap">
<Icon name="bell" :size="48" color="#BFB3B3" />
</view>
<Icon name="bell" :size="48" color="#BFB3B3" />
<text class="empty-text">暂无通知</text>
<text class="empty-hint">新的通知会在这里显示</text>
</view>
<view v-else class="notification-list">
@@ -97,7 +87,6 @@ import { getNotifications, markRead, markAllRead, getNotificationImageUrl } from
import { waitForReady } from '@/utils/app-ready'
import { statusBarHeight, capsuleRight } from '@/utils/system'
import Icon from '@/components/Icon/Icon.vue'
import Skeleton from '@/components/Skeleton/Skeleton.vue'
import type { Notification } from '@/api/notification'
const notifStore = useNotificationStore()
@@ -316,20 +305,7 @@ onReachBottom(() => {
}
}
// 骨架屏
.skeleton-list { padding: 0 $space-xl; }
.skeleton-notif {
display: flex; align-items: flex-start; gap: $space-lg;
padding: $space-lg; margin-bottom: $space-sm;
background: $surface; border-radius: $radius-xl;
border: 2rpx solid $border;
}
.skeleton-info {
flex: 1; display: flex; flex-direction: column; gap: $space-xs;
}
// 空状态
.empty-box {
.loading-box, .empty-box {
display: flex;
flex-direction: column;
align-items: center;
@@ -337,20 +313,8 @@ onReachBottom(() => {
padding: 120rpx 0;
}
.empty-icon-wrap {
width: 120rpx; height: 120rpx; border-radius: 50%;
background: $surface-warm; @include flex-center;
margin-bottom: $space-sm;
}
.empty-text {
font-size: $font-xl;
font-weight: 600;
color: $text;
}
.empty-hint {
font-size: $font-md;
.loading-text, .empty-text {
font-size: $font-lg;
color: $text-muted;
}

View File

@@ -20,25 +20,15 @@
</view>
<!-- 加载//列表 -->
<view v-if="loading" class="skeleton-list">
<view v-for="i in 3" :key="i" class="skeleton-card">
<Skeleton width="88rpx" height="88rpx" variant="circle" />
<view class="skeleton-info">
<Skeleton width="200rpx" height="28rpx" variant="text" />
<Skeleton width="300rpx" height="24rpx" variant="text" />
</view>
<Skeleton width="120rpx" height="32rpx" variant="rect" />
</view>
<view v-if="loading" class="loading-box">
<text class="loading-text">加载中...</text>
</view>
<view v-else-if="list.length === 0" class="empty-box">
<view class="empty-icon-wrap">
<Icon name="edit" :size="48" color="#BFB3B3" />
</view>
<Icon name="edit" :size="48" color="#BFB3B3" />
<text class="empty-text">暂无周期账单</text>
<text class="empty-hint">设置房租订阅工资等定期收支</text>
<view class="empty-btn" @tap="openAddModal">
<Icon name="plus" :size="24" color="#FFFFFF" />
<text class="empty-btn-text">添加周期账单</text>
</view>
</view>
@@ -172,7 +162,6 @@ import { getRecurringTemplates, createRecurringTemplate, updateRecurringTemplate
import Icon from '@/components/Icon/Icon.vue'
import CategoryIcon from '@/components/CategoryIcon/CategoryIcon.vue'
import AmountEditor from '@/components/AmountEditor/AmountEditor.vue'
import Skeleton from '@/components/Skeleton/Skeleton.vue'
import type { RecurringTemplate } from '@/api/recurring'
const catStore = useCategoryStore()
@@ -372,35 +361,16 @@ function goBack() { uni.navigateBack() }
}
.sync-text { font-size: $font-md; color: #43A047; font-weight: 500; }
// 骨架屏
.skeleton-list { padding: 0 $space-xl; }
.skeleton-card {
display: flex; align-items: center; gap: $space-md;
padding: $space-lg; margin-bottom: $space-sm;
background: $surface; border-radius: $radius-xl;
border: 2rpx solid $border;
}
.skeleton-info {
flex: 1; display: flex; flex-direction: column; gap: $space-xs;
}
// 空状态
.empty-box {
.loading-box, .empty-box {
display: flex; flex-direction: column; align-items: center;
padding: 120rpx 0; gap: $space-sm;
}
.empty-icon-wrap {
width: 120rpx; height: 120rpx; border-radius: 50%;
background: $surface-warm; @include flex-center;
margin-bottom: $space-sm;
}
.empty-text { font-size: $font-xl; font-weight: 600; color: $text; }
.loading-text, .empty-text { font-size: $font-lg; color: $text-muted; }
.empty-hint { font-size: $font-md; color: $text-muted; margin-top: -$space-xs; }
.empty-btn {
margin-top: $space-lg; padding: $space-sm $space-xl;
background: linear-gradient(135deg, $primary, $primary-dark);
border-radius: $radius-lg; display: flex; align-items: center; gap: $space-xs;
&:active { opacity: 0.8; transform: scale(0.96); }
background: $primary; border-radius: $radius-lg;
&:active { opacity: 0.8; }
}
.empty-btn-text { font-size: $font-base; color: $surface; font-weight: 600; }

View File

@@ -247,65 +247,3 @@
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
}
// ===== 入场动画 =====
// 卡片淡入上浮
@mixin fade-in-up($delay: 0s) {
animation: fadeInUp 0.4s cubic-bezier(0.23, 1, 0.32, 1) $delay both;
}
// 列表项交错淡入
@mixin stagger-item($index: 0, $base-delay: 0.05s) {
opacity: 0;
animation: fadeInUp 0.35s cubic-bezier(0.23, 1, 0.32, 1) ($index * $base-delay) both;
}
// 数字跳动
@mixin count-up {
animation: countPulse 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
// 弹性缩放
@mixin bounce-in($delay: 0s) {
animation: bounceIn 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) $delay both;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(24rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes bounceIn {
from {
opacity: 0;
transform: scale(0.85);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes countPulse {
0% { transform: scale(1); }
40% { transform: scale(1.08); }
100% { transform: scale(1); }
}
// 尊重减弱动画偏好
@media (prefers-reduced-motion: reduce) {
.fade-in-up,
.stagger-item,
.bounce-in {
animation: none !important;
opacity: 1 !important;
transform: none !important;
}
}