feat: 移动端UI精致化优化

- 添加页面过渡动画(淡入+滑动)
- 优化统计卡片视觉,添加渐变色和微动效
- 实现列表项左滑删除手势(编辑+删除按钮)
- 优化底部导航,缩小高度并添加点击波纹效果
- 改进空状态,添加插画和引导按钮

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Developer
2026-03-18 16:48:42 +08:00
co-authored by Claude
parent af3fcbea96
commit 349760b60c
6 changed files with 466 additions and 40 deletions
+65 -17
View File
@@ -4,10 +4,12 @@ import { isLoggedIn, getRecords, getStats, createRecord, updateRecord, deleteRec
import AppShell from '../components/AppShell';
import { Icon } from '../components/icons';
import RecordModal from '../components/RecordModal';
import SwipeableItem from '../components/SwipeableItem';
import EmptyState from '../components/EmptyState';
import { useToast } from '../components/ToastProvider';
import { useConfirm } from '../components/ConfirmProvider';
import { useCategories } from '../hooks/useCategories';
import { getMonthStr, formatMoney, formatMonth, parseDateTime, getDatePart } from '../lib/utils';
import { getMonthStr, formatMoney, formatMonth, parseDateTime } from '../lib/utils';
export default function Home() {
const router = useRouter();
@@ -191,6 +193,23 @@ export default function Home() {
}
};
const handleQuickDelete = async (record) => {
const shouldDelete = await confirm({
title: '删除这条记录?',
description: `${record.category} ${record.type === 'income' ? '+' : '-'}${formatMoney(record.amount)}`,
confirmText: '删除',
});
if (!shouldDelete) return;
try {
await deleteRecord(record.id);
setRecords(prev => prev.filter(r => r.id !== record.id));
showToast('记录已删除', { tone: 'success' });
} catch (err) {
showToast(err.message, { tone: 'error' });
}
};
const groupedRecords = useMemo(() => {
return records.reduce((groups, record) => {
const date = getDatePart(record.date);
@@ -286,10 +305,14 @@ export default function Home() {
{loading ? (
<div className="loading-state">加载中...</div>
) : records.length === 0 ? (
<div className="empty-state empty-state--card">
<div className="empty-state__icon"><Icon name="info" size={24} /></div>
<p>暂无记录点击上方按钮添加</p>
</div>
<EmptyState
icon="info"
title="暂无记录"
description={typeFilter === 'all' ? '本月还没有记账,点击下方按钮添加第一笔记录' : '该类型下暂无记录'}
action
actionLabel="添加记录"
onAction={openAddModal}
/>
) : (
<div className="records-list records-list--flush">
{sortedDates.map((date) => (
@@ -301,19 +324,44 @@ export default function Home() {
{groupedRecords[date].map((record) => {
const icon = categoryIconMap[`${record.type}:${record.category}`] || '📝';
return (
<button key={record.id} type="button" className="record-item" onClick={() => openEditModal(record)}>
<div className="record-left">
<div className="record-icon">{icon}</div>
<div className="record-info">
<div className="record-category">{record.category}</div>
{record.note && <div className="record-note">{record.note}</div>}
<SwipeableItem
key={record.id}
onSwipeLeft={() => openEditModal(record)}
renderRightActions={(handleAction) => (
<>
<button
type="button"
className="swipeable-action swipeable-action--edit"
onClick={() => handleAction(() => openEditModal(record))}
aria-label="编辑"
>
<Icon name="edit" size={20} />
</button>
<button
type="button"
className="swipeable-action swipeable-action--delete"
onClick={() => handleAction(() => handleQuickDelete(record))}
aria-label="删除"
>
<Icon name="trash" size={20} />
</button>
</>
)}
>
<button type="button" className="record-item" onClick={() => openEditModal(record)}>
<div className="record-left">
<div className="record-icon">{icon}</div>
<div className="record-info">
<div className="record-category">{record.category}</div>
{record.note && <div className="record-note">{record.note}</div>}
</div>
</div>
</div>
<div className={`record-amount ${record.type}`}>
{record.type === 'income' ? '+' : '-'}
{formatMoney(record.amount)}
</div>
</button>
<div className={`record-amount ${record.type}`}>
{record.type === 'income' ? '+' : '-'}
{formatMoney(record.amount)}
</div>
</button>
</SwipeableItem>
);
})}
</div>