Revert "feat: 移动端UI精致化优化"

This reverts commit 349760b60c.
This commit is contained in:
Developer
2026-03-18 18:42:31 +08:00
parent 349760b60c
commit 1e478c2eaa
6 changed files with 40 additions and 466 deletions
-27
View File
@@ -1,27 +0,0 @@
import { Icon } from './icons';
export default function EmptyState({
icon = 'info',
title = '暂无数据',
description = '这里还没有内容',
action,
actionLabel,
onAction,
}) {
return (
<div className="empty-state-enhanced">
<div className="empty-state-illustration">
<div className="empty-state-bg">
<Icon name={icon} size={32} />
</div>
</div>
<h3 className="empty-state-title">{title}</h3>
{description && <p className="empty-state-description">{description}</p>}
{action && (
<button type="button" className="btn btn-primary empty-state-action" onClick={onAction}>
{actionLabel}
</button>
)}
</div>
);
}
-41
View File
@@ -1,41 +0,0 @@
import { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
export default function PageTransition({ children }) {
const router = useRouter();
const [isAnimating, setIsAnimating] = useState(false);
const [displayChildren, setDisplayChildren] = useState(children);
useEffect(() => {
const handleRouteChangeStart = () => {
setIsAnimating(true);
};
const handleRouteChangeComplete = () => {
setDisplayChildren(children);
setTimeout(() => {
setIsAnimating(false);
}, 50);
};
router.events.on('routeChangeStart', handleRouteChangeStart);
router.events.on('routeChangeComplete', handleRouteChangeComplete);
return () => {
router.events.off('routeChangeStart', handleRouteChangeStart);
router.events.off('routeChangeComplete', handleRouteChangeComplete);
};
}, [router, children]);
useEffect(() => {
if (!isAnimating) {
setDisplayChildren(children);
}
}, [children, isAnimating]);
return (
<div className={`page-transition ${isAnimating ? 'page-transition--entering' : 'page-transition--active'}`}>
{displayChildren}
</div>
);
}
-93
View File
@@ -1,93 +0,0 @@
import { useState, useRef, useCallback } from 'react';
export default function SwipeableItem({
children,
onSwipeLeft,
renderRightActions,
swipeThreshold = 80,
}) {
const [isSwiping, setIsSwiping] = useState(false);
const [translateX, setTranslateX] = useState(0);
const [isRevealed, setIsRevealed] = useState(false);
const startXRef = useRef(0);
const currentXRef = useRef(0);
const itemRef = useRef(null);
const handleTouchStart = useCallback((e) => {
startXRef.current = e.touches[0].clientX;
currentXRef.current = startXRef.current;
setIsSwiping(true);
}, []);
const handleTouchMove = useCallback((e) => {
if (!isSwiping) return;
currentXRef.current = e.touches[0].clientX;
const diff = currentXRef.current - startXRef.current;
// Only allow swiping left (negative diff)
if (diff < 0) {
// Limit the swipe distance
const newTranslateX = Math.max(diff, -swipeThreshold - 20);
setTranslateX(newTranslateX);
} else if (isRevealed) {
// If already revealed, allow swiping right to close
const newTranslateX = Math.min(0, -swipeThreshold + diff);
setTranslateX(newTranslateX);
}
}, [isSwiping, isRevealed, swipeThreshold]);
const handleTouchEnd = useCallback(() => {
setIsSwiping(false);
const diff = currentXRef.current - startXRef.current;
if (diff < -swipeThreshold / 2 || (isRevealed && diff > -swipeThreshold / 2)) {
// Reveal actions
setTranslateX(-swipeThreshold);
setIsRevealed(true);
} else {
// Snap back
setTranslateX(0);
setIsRevealed(false);
}
}, [isRevealed, swipeThreshold]);
const handleClose = useCallback(() => {
setTranslateX(0);
setIsRevealed(false);
}, []);
const handleActionClick = useCallback((callback) => {
callback?.();
handleClose();
}, [handleClose]);
return (
<div className="swipeable-item" ref={itemRef}>
<div
className={`swipeable-content ${isSwiping ? 'is-swiping' : ''}`}
style={{
transform: `translateX(${translateX}px)`,
transition: isSwiping ? 'none' : 'transform 0.2s ease-out',
}}
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
onTouchCancel={handleTouchEnd}
>
{children}
</div>
{renderRightActions && (
<div
className="swipeable-actions"
style={{
opacity: isRevealed || translateX < 0 ? 1 : 0,
transition: 'opacity 0.2s ease-out',
}}
>
{renderRightActions(handleActionClick)}
</div>
)}
</div>
);
}
-3
View File
@@ -2,7 +2,6 @@ import { SWRConfig } from 'swr';
import ErrorBoundary from '../components/ErrorBoundary'; import ErrorBoundary from '../components/ErrorBoundary';
import { ToastProvider } from '../components/ToastProvider'; import { ToastProvider } from '../components/ToastProvider';
import { ConfirmProvider } from '../components/ConfirmProvider'; import { ConfirmProvider } from '../components/ConfirmProvider';
import PageTransition from '../components/PageTransition';
import { swrConfig } from '../lib/swr-config'; import { swrConfig } from '../lib/swr-config';
import '../styles/globals.css'; import '../styles/globals.css';
@@ -12,9 +11,7 @@ export default function App({ Component, pageProps }) {
<SWRConfig value={swrConfig}> <SWRConfig value={swrConfig}>
<ConfirmProvider> <ConfirmProvider>
<ToastProvider> <ToastProvider>
<PageTransition>
<Component {...pageProps} /> <Component {...pageProps} />
</PageTransition>
</ToastProvider> </ToastProvider>
</ConfirmProvider> </ConfirmProvider>
</SWRConfig> </SWRConfig>
+6 -54
View File
@@ -4,12 +4,10 @@ import { isLoggedIn, getRecords, getStats, createRecord, updateRecord, deleteRec
import AppShell from '../components/AppShell'; import AppShell from '../components/AppShell';
import { Icon } from '../components/icons'; import { Icon } from '../components/icons';
import RecordModal from '../components/RecordModal'; import RecordModal from '../components/RecordModal';
import SwipeableItem from '../components/SwipeableItem';
import EmptyState from '../components/EmptyState';
import { useToast } from '../components/ToastProvider'; import { useToast } from '../components/ToastProvider';
import { useConfirm } from '../components/ConfirmProvider'; import { useConfirm } from '../components/ConfirmProvider';
import { useCategories } from '../hooks/useCategories'; import { useCategories } from '../hooks/useCategories';
import { getMonthStr, formatMoney, formatMonth, parseDateTime } from '../lib/utils'; import { getMonthStr, formatMoney, formatMonth, parseDateTime, getDatePart } from '../lib/utils';
export default function Home() { export default function Home() {
const router = useRouter(); const router = useRouter();
@@ -193,23 +191,6 @@ 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(() => { const groupedRecords = useMemo(() => {
return records.reduce((groups, record) => { return records.reduce((groups, record) => {
const date = getDatePart(record.date); const date = getDatePart(record.date);
@@ -305,14 +286,10 @@ export default function Home() {
{loading ? ( {loading ? (
<div className="loading-state">加载中...</div> <div className="loading-state">加载中...</div>
) : records.length === 0 ? ( ) : records.length === 0 ? (
<EmptyState <div className="empty-state empty-state--card">
icon="info" <div className="empty-state__icon"><Icon name="info" size={24} /></div>
title="暂无记录" <p>暂无记录点击上方按钮添加</p>
description={typeFilter === 'all' ? '本月还没有记账,点击下方按钮添加第一笔记录' : '该类型下暂无记录'} </div>
action
actionLabel="添加记录"
onAction={openAddModal}
/>
) : ( ) : (
<div className="records-list records-list--flush"> <div className="records-list records-list--flush">
{sortedDates.map((date) => ( {sortedDates.map((date) => (
@@ -324,31 +301,7 @@ export default function Home() {
{groupedRecords[date].map((record) => { {groupedRecords[date].map((record) => {
const icon = categoryIconMap[`${record.type}:${record.category}`] || '📝'; const icon = categoryIconMap[`${record.type}:${record.category}`] || '📝';
return ( return (
<SwipeableItem <button key={record.id} type="button" className="record-item" onClick={() => openEditModal(record)}>
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-left">
<div className="record-icon">{icon}</div> <div className="record-icon">{icon}</div>
<div className="record-info"> <div className="record-info">
@@ -361,7 +314,6 @@ export default function Home() {
{formatMoney(record.amount)} {formatMoney(record.amount)}
</div> </div>
</button> </button>
</SwipeableItem>
); );
})} })}
</div> </div>
+22 -236
View File
@@ -564,77 +564,24 @@ textarea:focus-visible {
text-align: left; text-align: left;
border: 1px solid rgba(148, 163, 184, 0.12); border: 1px solid rgba(148, 163, 184, 0.12);
background: rgba(255, 255, 255, 0.82); background: rgba(255, 255, 255, 0.82);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.45), 0 2px 8px rgba(15, 23, 42, 0.04); box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.45);
cursor: pointer; cursor: pointer;
transition: transform var(--transition), box-shadow var(--transition), border-color var(--transition), background var(--transition); transition: transform var(--transition), box-shadow var(--transition), border-color var(--transition), background var(--transition);
} }
.stat-card:hover { .stat-card:hover {
transform: translateY(-2px); transform: translateY(-1px);
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.08); box-shadow: var(--shadow-xs);
}
.stat-card:active {
transform: translateY(0) scale(0.98);
} }
.stat-card.active { .stat-card.active {
border-color: rgba(37, 99, 235, 0.3); border-color: rgba(37, 99, 235, 0.3);
background: rgba(240, 246, 255, 0.96); background: rgba(240, 246, 255, 0.96);
box-shadow: 0 10px 28px rgba(37, 99, 235, 0.12); box-shadow: 0 10px 24px rgba(37, 99, 235, 0.08);
}
/* Income stat card with gradient */
.stat-card:has(.income) {
background: linear-gradient(135deg, rgba(5, 150, 105, 0.08) 0%, rgba(5, 150, 105, 0.02) 100%);
border-color: rgba(5, 150, 105, 0.15);
}
.stat-card:has(.income):hover {
background: linear-gradient(135deg, rgba(5, 150, 105, 0.12) 0%, rgba(5, 150, 105, 0.04) 100%);
border-color: rgba(5, 150, 105, 0.22);
box-shadow: 0 8px 24px rgba(5, 150, 105, 0.1);
}
.stat-card:has(.income).active {
background: linear-gradient(135deg, rgba(5, 150, 105, 0.16) 0%, rgba(5, 150, 105, 0.06) 100%);
border-color: rgba(5, 150, 105, 0.3);
box-shadow: 0 10px 28px rgba(5, 150, 105, 0.14);
}
/* Expense stat card with gradient */
.stat-card:has(.expense) {
background: linear-gradient(135deg, rgba(220, 38, 38, 0.08) 0%, rgba(220, 38, 38, 0.02) 100%);
border-color: rgba(220, 38, 38, 0.15);
}
.stat-card:has(.expense):hover {
background: linear-gradient(135deg, rgba(220, 38, 38, 0.12) 0%, rgba(220, 38, 38, 0.04) 100%);
border-color: rgba(220, 38, 38, 0.22);
box-shadow: 0 8px 24px rgba(220, 38, 38, 0.1);
}
.stat-card:has(.expense).active {
background: linear-gradient(135deg, rgba(220, 38, 38, 0.16) 0%, rgba(220, 38, 38, 0.06) 100%);
border-color: rgba(220, 38, 38, 0.3);
box-shadow: 0 10px 28px rgba(220, 38, 38, 0.14);
} }
.stat-card--balance { .stat-card--balance {
background: linear-gradient(135deg, rgba(37, 99, 235, 0.08) 0%, rgba(37, 99, 235, 0.02) 100%); background: rgba(255, 255, 255, 0.82);
border-color: rgba(37, 99, 235, 0.15);
}
.stat-card--balance:hover {
background: linear-gradient(135deg, rgba(37, 99, 235, 0.12) 0%, rgba(37, 99, 235, 0.04) 100%);
border-color: rgba(37, 99, 235, 0.22);
box-shadow: 0 8px 24px rgba(37, 99, 235, 0.1);
}
.stat-card--balance.active {
background: linear-gradient(135deg, rgba(37, 99, 235, 0.16) 0%, rgba(37, 99, 235, 0.06) 100%);
border-color: rgba(37, 99, 235, 0.3);
box-shadow: 0 10px 28px rgba(37, 99, 235, 0.14);
} }
.label { .label {
@@ -1870,53 +1817,31 @@ textarea:focus-visible {
.bottom-nav__inner { .bottom-nav__inner {
display: grid; display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr)); grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 4px; gap: 6px;
padding: 6px 8px calc(6px + env(safe-area-inset-bottom)); padding: 10px 10px 11px;
border-radius: 20px 20px 0 0; border-radius: 22px 22px 0 0;
background: rgba(255, 255, 255, 0.88); background: rgba(255, 255, 255, 0.84);
border: 1px solid rgba(255, 255, 255, 0.7); border: 1px solid rgba(255, 255, 255, 0.66);
box-shadow: 0 -8px 24px rgba(15, 23, 42, 0.05); box-shadow: 0 -12px 30px rgba(15, 23, 42, 0.06);
backdrop-filter: blur(24px); backdrop-filter: blur(24px);
} }
.bottom-nav__item { .bottom-nav__item {
min-height: 48px; min-height: 54px;
display: flex; display: grid;
flex-direction: column; justify-items: center;
align-items: center; align-content: center;
justify-content: center; gap: 4px;
gap: 3px; padding: 7px 4px;
padding: 5px 4px; border-radius: 16px;
border-radius: 14px;
color: var(--text-tertiary); color: var(--text-tertiary);
cursor: pointer; cursor: pointer;
transition: background var(--transition), color var(--transition), transform var(--transition); transition: background var(--transition), color var(--transition), transform var(--transition);
position: relative;
overflow: hidden;
}
.bottom-nav__item::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
background: rgba(37, 99, 235, 0.1);
border-radius: 50%;
transform: translate(-50%, -50%);
transition: width 0.3s ease, height 0.3s ease;
}
.bottom-nav__item:active::before {
width: 100%;
height: 100%;
} }
.bottom-nav__item span { .bottom-nav__item span {
font-size: 0.7rem; font-size: 0.74rem;
font-weight: 600; font-weight: 700;
letter-spacing: 0.02em;
} }
.bottom-nav__item.is-active { .bottom-nav__item.is-active {
@@ -1924,18 +1849,6 @@ textarea:focus-visible {
color: var(--primary); color: var(--primary);
} }
.bottom-nav__item.is-active::before {
display: none;
}
.bottom-nav__item .icon {
transition: transform 0.2s ease;
}
.bottom-nav__item:active .icon {
transform: scale(0.9);
}
.checkbox-label { .checkbox-label {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
@@ -2278,133 +2191,6 @@ textarea:focus-visible {
to { transform: rotate(360deg); } to { transform: rotate(360deg); }
} }
/* Swipeable Item */ .is-spinning {
.swipeable-item { animation: spin 1s linear infinite;
position: relative;
overflow: hidden;
}
.swipeable-content {
position: relative;
z-index: 1;
background: var(--surface);
cursor: grab;
}
.swipeable-content.is-swiping {
cursor: grabbing;
}
.swipeable-actions {
position: absolute;
top: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
padding: 0 12px;
background: transparent;
z-index: 0;
}
.swipeable-action {
width: 56px;
height: 56px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 14px;
cursor: pointer;
transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.swipeable-action:active {
transform: scale(0.92);
}
.swipeable-action--edit {
background: var(--primary-soft);
color: var(--primary);
}
.swipeable-action--delete {
background: var(--expense-soft);
color: var(--expense);
}
/* Empty State Enhanced */
.empty-state-enhanced {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 24px;
text-align: center;
}
.empty-state-illustration {
position: relative;
margin-bottom: 20px;
}
.empty-state-bg {
width: 80px;
height: 80px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 24px;
background: linear-gradient(135deg, rgba(37, 99, 235, 0.1) 0%, rgba(37, 99, 235, 0.04) 100%);
color: var(--primary);
position: relative;
}
.empty-state-bg::before {
content: '';
position: absolute;
inset: -8px;
background: radial-gradient(circle, rgba(37, 99, 235, 0.08) 0%, transparent 70%);
border-radius: 50%;
z-index: -1;
}
.empty-state-title {
font-size: 1.1rem;
font-weight: 700;
color: var(--text);
margin-bottom: 8px;
}
.empty-state-description {
font-size: 0.9rem;
color: var(--text-secondary);
line-height: 1.5;
margin-bottom: 20px;
max-width: 240px;
}
.empty-state-action {
min-width: 140px;
min-height: 44px;
}
.empty-state-action .icon {
margin-right: 6px;
}
.page-transition {
opacity: 1;
transform: translateY(0);
transition: opacity 250ms ease-out, transform 250ms ease-out;
}
.page-transition--entering {
opacity: 0;
transform: translateY(12px);
}
.page-transition--active {
opacity: 1;
transform: translateY(0);
} }