页面交互与动画优化

This commit is contained in:
cc
2026-03-12 23:52:46 +08:00
parent 4e80f93b30
commit 4966cdbfac
9 changed files with 597 additions and 380 deletions

View File

@@ -209,16 +209,7 @@ function AnnualReportPage() {
return (
<div className="annual-report-page">
<Loader2 size={32} className="spin" style={{ color: 'var(--text-tertiary)' }} />
<p style={{ color: 'var(--text-tertiary)', marginTop: 16 }}>...</p>
<div className="load-telemetry compact">
<p><span className="label"></span>{getStrategyLabel({ loadStrategy, loadPhase, hasYearsLoadFinished, hasSwitchedStrategy, nativeTimedOut })}</p>
<p><span className="label"></span>{loadStatusText || '正在加载年份数据...'}</p>
<p>
<span className="label"></span>{formatLoadElapsed(nativeElapsedMs)}{nativeTimedOut ? '(超时)' : ''} {' '}
<span className="label"></span>{formatLoadElapsed(scanElapsedMs)} {' '}
<span className="label"></span>{formatLoadElapsed(totalElapsedMs)}
</p>
</div>
<p style={{ color: 'var(--text-tertiary)', marginTop: 16 }}>...</p>
</div>
)
}
@@ -264,30 +255,6 @@ function AnnualReportPage() {
<Sparkles size={32} className="header-icon" />
<h1 className="page-title"></h1>
<p className="page-desc"></p>
{loadedYearCount > 0 && (
<p className={`page-desc load-summary ${isYearStatusComplete ? 'complete' : 'loading'}`}>
{isYearStatusComplete ? (
<> {loadedYearCount} {formatLoadElapsed(totalElapsedMs)}</>
) : (
<>
{loadedYearCount} <span className="dot-ellipsis" aria-hidden="true">...</span>
{formatLoadElapsed(totalElapsedMs)}
</>
)}
</p>
)}
<div className={`load-telemetry ${isYearStatusComplete ? 'complete' : 'loading'}`}>
<p><span className="label"></span>{strategyLabel}</p>
<p>
<span className="label"></span>
{loadStatusText || (isYearStatusComplete ? '全部年份已加载完毕' : '正在加载年份数据...')}
</p>
<p>
<span className="label"></span>{formatLoadElapsed(nativeElapsedMs)}{nativeTimedOut ? '(超时)' : ''} {' '}
<span className="label"></span>{formatLoadElapsed(scanElapsedMs)} {' '}
<span className="label"></span>{formatLoadElapsed(totalElapsedMs)}
</p>
</div>
<div className="report-sections">
<section className="report-section">
@@ -311,7 +278,6 @@ function AnnualReportPage() {
</div>
))}
</div>
{renderYearLoadStatus()}
</div>
<button
@@ -358,7 +324,6 @@ function AnnualReportPage() {
</div>
))}
</div>
{renderYearLoadStatus()}
</div>
<button

View File

@@ -3854,12 +3854,6 @@ function ChatPage(props: ChatPageProps) {
<button className="icon-btn refresh-btn" onClick={handleRefresh} disabled={isLoadingSessions || isRefreshingSessions}>
<RefreshCw size={16} className={(isLoadingSessions || isRefreshingSessions) ? 'spin' : ''} />
</button>
{isSessionListSyncing && (
<div className="session-sync-indicator">
<Loader2 size={12} className="spin" />
<span></span>
</div>
)}
</div>
</div>
{/* 折叠群 header */}

View File

@@ -891,28 +891,6 @@ function ContactsPage() {
</label>
</div>
<div className="contacts-count">
{filteredContacts.length} / {contacts.length}
{contactsUpdatedAt && (
<span className="contacts-cache-meta">
{contactsDataSource === 'cache' ? '缓存' : '最新'} · {contactsUpdatedAtLabel}
</span>
)}
{contacts.length > 0 && (
<span className="contacts-cache-meta">
{avatarCachedCount}/{contacts.length}
{avatarCacheUpdatedAtLabel ? ` · 更新于 ${avatarCacheUpdatedAtLabel}` : ''}
</span>
)}
{isLoading && contacts.length > 0 && (
<span className="contacts-cache-meta syncing">...</span>
)}
{avatarEnrichProgress.running && (
<span className="avatar-enrich-progress">
{avatarEnrichProgress.loaded}/{avatarEnrichProgress.total}
</span>
)}
</div>
{exportMode && (
<div className="selection-toolbar">

View File

@@ -11,6 +11,33 @@
padding: 28px 32px;
background: rgba(15, 23, 42, 0.28);
backdrop-filter: blur(10px);
animation: settingsFadeIn 0.2s ease;
&.closing {
animation: settingsFadeOut 0.2s ease forwards;
}
}
@keyframes settingsFadeIn {
from {
opacity: 0;
backdrop-filter: blur(0);
}
to {
opacity: 1;
backdrop-filter: blur(10px);
}
}
@keyframes settingsFadeOut {
from {
opacity: 1;
backdrop-filter: blur(10px);
}
to {
opacity: 0;
backdrop-filter: blur(0);
}
}
.settings-page {
@@ -25,6 +52,33 @@
border-radius: 24px;
box-shadow: 0 28px 80px rgba(15, 23, 42, 0.22);
overflow: hidden;
animation: settingsSlideUp 0.3s ease;
&.closing {
animation: settingsSlideDown 0.2s ease forwards;
}
}
@keyframes settingsSlideUp {
from {
opacity: 0;
transform: translateY(30px) scale(0.96);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
@keyframes settingsSlideDown {
from {
opacity: 1;
transform: translateY(0) scale(1);
}
to {
opacity: 0;
transform: translateY(20px) scale(0.98);
}
}
.settings-header {

View File

@@ -134,6 +134,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
const [isClearingAnalyticsCache, setIsClearingAnalyticsCache] = useState(false)
const [isClearingImageCache, setIsClearingImageCache] = useState(false)
const [isClearingAllCache, setIsClearingAllCache] = useState(false)
const [isClosing, setIsClosing] = useState(false)
const saveTimersRef = useRef<Record<string, ReturnType<typeof setTimeout>>>({})
// 安全设置 state
@@ -203,7 +204,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
if (!onClose) return
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
onClose()
handleClose()
}
}
document.addEventListener('keydown', handleKeyDown)
@@ -445,6 +446,14 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
setTimeout(() => setMessage(null), 3000)
}
const handleClose = () => {
if (!onClose) return
setIsClosing(true)
setTimeout(() => {
onClose()
}, 200)
}
type WxidKeys = {
decryptKey: string
imageXorKey: number | null
@@ -2076,8 +2085,8 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
)
return (
<div className="settings-modal-overlay" onClick={() => onClose?.()}>
<div className="settings-page" onClick={(event) => event.stopPropagation()}>
<div className={`settings-modal-overlay ${isClosing ? 'closing' : ''}`} onClick={handleClose}>
<div className={`settings-page ${isClosing ? 'closing' : ''}`} onClick={(event) => event.stopPropagation()}>
{message && <div className={`message-toast ${message.success ? 'success' : 'error'}`}>{message.text}</div>}
{/* 多账号选择对话框 */}
@@ -2116,7 +2125,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
<Plug size={16} /> {isTesting ? '测试中...' : '测试连接'}
</button>
{onClose && (
<button type="button" className="settings-close-btn" onClick={onClose} aria-label="关闭设置">
<button type="button" className="settings-close-btn" onClick={handleClose} aria-label="关闭设置">
<X size={18} />
</button>
)}