fix: 全面修复 22 项问题

严重 Bug:
- NaN 金额校验绕过(add 页面 parseFloat 空字符串)
- 4 个页面补充 waitForReady(add/budget/category-manage/group-manage)
- 导出 Loading 遮罩不消失(profile exportData 缺 hideLoading)
- Store 吞掉请求错误不 re-throw(transaction/stats/budget/user)
- 分类排序 SQL 拼接改参数化查询(category PUT /sort)
- 备份接口添加管理员权限检查(backup requireAdmin)

中等问题:
- 群组加入竞态条件改为 INSERT IGNORE
- 统计页切月/切Tab添加 loadSeq 防竞态
- stats store 补全 loading 状态 + Overview 初始值补全 7 字段
- 保存成功后返回不再弹「放弃修改」确认框
- 群组管理创建/加入添加 saving 双重提交保护
- 预算页添加 onShow 刷新
- 服务端输入校验(name/color/invite_code/微信code)

UI/样式:
- 触摸目标: bell/nav-back/nav-save→88rpx, avatar-badge→64rpx, action-btn→80rpx
- :active 反馈: bell/profile-card/identity-card/id-manage/arrow/action-btn/group-btn/cat-info
- 内联样式改动态绑定(4处 spacer 使用 capsuleRight)
- FAB 文字+改用 Icon 组件
- prefers-reduced-motion 动画降级(Skeleton/profile/budget/add)
- 胶囊适配(add/category-manage/group-manage)
- CategoryIcon !important 改为 iconStyle 计算属性

文档:
- CLAUDE.md 补充 SaveSuccess/ChartWrapper 组件 + category/budget/index API
This commit is contained in:
2026-06-04 17:06:02 +08:00
parent 21f4d81959
commit e110cd5c5d
25 changed files with 314 additions and 88 deletions

View File

@@ -8,7 +8,13 @@
@touchmove="!disableSwipe && onTouchMove($event)"
@touchend="!disableSwipe && onTouchEnd()"
>
<!-- 头像群组模式下显示他人创建者 -->
<view v-if="isOtherCreator" class="creator-avatar">
<image v-if="creatorAvatarUrl" :src="creatorAvatarUrl" class="creator-img" mode="aspectFill" />
<text v-else class="creator-initial">{{ item.creator_nickname?.[0] }}</text>
</view>
<CategoryIcon
v-else
:label="item.category_name?.[0] || '?'"
:color="item.category_color || '#BFB3B3'"
:bg-color="(item.category_color || '#BFB3B3') + '20'"
@@ -16,7 +22,10 @@
/>
<view class="info">
<text class="name">{{ item.note || item.category_name || '未分类' }}</text>
<text class="meta">{{ item.category_name || '未分类' }}{{ hideDate ? '' : ' · ' + formatDate(item.date) }}</text>
<text class="meta">
<text v-if="isOtherCreator" class="creator-name">{{ item.creator_nickname }} · </text>
{{ item.category_name || '未分类' }}{{ hideDate ? '' : ' · ' + formatDate(item.date) }}
</text>
</view>
<text class="amount" :class="item.type">{{ item.type === 'expense' ? '-' : '+' }}{{ formatAmount(item.amount) }}</text>
</view>
@@ -27,14 +36,16 @@
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { ref, computed } from 'vue'
import { formatAmount, formatDate } from '@/utils/format'
import { API_BASE } from '@/config'
import Icon from '@/components/Icon/Icon.vue'
import CategoryIcon from '@/components/CategoryIcon/CategoryIcon.vue'
withDefaults(defineProps<{
const props = withDefaults(defineProps<{
item: {
id: number
user_id: number
amount: number
type: 'expense' | 'income'
note?: string
@@ -42,14 +53,33 @@ withDefaults(defineProps<{
category_name?: string
category_icon?: string
category_color?: string
creator_nickname?: string
creator_avatar?: string
}
hideDate?: boolean
/** 禁用滑动删除(首页预览等场景) */
disableSwipe?: boolean
}>(), { hideDate: false, disableSwipe: false })
/** 是否显示创建者信息(群组模式) */
showCreator?: boolean
/** 当前用户 ID用于判断是否为本人记录 */
currentUserId?: number
}>(), { hideDate: false, disableSwipe: false, showCreator: false, currentUserId: 0 })
/** 是否为他人的记录(群组模式下,非本人的才显示创建者信息) */
const isOtherCreator = computed(() => {
if (!props.showCreator || !props.item.creator_nickname) return false
return !props.currentUserId || props.item.user_id !== props.currentUserId
})
defineEmits(['item-tap', 'delete'])
const creatorAvatarUrl = computed(() => {
if (!props.item.creator_avatar) return ''
if (props.item.creator_avatar.startsWith('http')) return props.item.creator_avatar
// 截取服务端根地址(去掉 /api
const base = API_BASE.replace(/\/api$/, '')
return base + '/api/user/avatar/' + props.item.creator_avatar
})
const startX = ref(0)
const offsetX = ref(0)
const showDelete = ref(false)
@@ -121,6 +151,29 @@ function onTouchEnd() {
}
}
.creator-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
background: #FFF0E6;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
flex-shrink: 0;
}
.creator-img {
width: 100%;
height: 100%;
}
.creator-initial {
font-size: 28rpx;
font-weight: 600;
color: #FF8C69;
}
.info {
flex: 1;
min-width: 0;
@@ -143,6 +196,11 @@ function onTouchEnd() {
margin-top: 4rpx;
}
.creator-name {
color: #FF8C69;
font-weight: 500;
}
.amount {
font-family: 'Fredoka', sans-serif;
font-size: 32rpx;