新增资源管理并修复了朋友圈的资源缓存路径

This commit is contained in:
cc
2026-04-06 23:32:59 +08:00
parent 20c5381211
commit d128bedffa
23 changed files with 3860 additions and 86 deletions

View File

@@ -66,6 +66,33 @@ type OverviewStatsStatus = 'loading' | 'ready' | 'error'
type SnsExportScope = { kind: 'all' } | { kind: 'selected'; usernames: string[] }
const SIDEBAR_USER_PROFILE_CACHE_KEY = 'sidebar_user_profile_cache_v1'
const SNS_CACHE_MIGRATION_PROMPT_SESSION_KEY = 'sns_cache_migration_prompted_v1'
interface SnsCacheMigrationItem {
label: string
sourceDir: string
targetDir: string
fileCount: number
}
interface SnsCacheMigrationStatus {
totalFiles: number
legacyBaseDir?: string
currentBaseDir?: string
items: SnsCacheMigrationItem[]
}
interface SnsCacheMigrationProgress {
status: 'running' | 'done' | 'error'
phase: 'copying' | 'cleanup' | 'done' | 'error'
current: number
total: number
copied: number
skipped: number
remaining: number
message?: string
currentItemLabel?: string
}
const readSidebarUserProfileCache = (): SidebarUserProfile | null => {
try {
@@ -162,6 +189,12 @@ export default function SnsPage() {
const [triggerInstalled, setTriggerInstalled] = useState<boolean | null>(null)
const [triggerLoading, setTriggerLoading] = useState(false)
const [triggerMessage, setTriggerMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null)
const [showCacheMigrationDialog, setShowCacheMigrationDialog] = useState(false)
const [cacheMigrationStatus, setCacheMigrationStatus] = useState<SnsCacheMigrationStatus | null>(null)
const [cacheMigrationProgress, setCacheMigrationProgress] = useState<SnsCacheMigrationProgress | null>(null)
const [cacheMigrationRunning, setCacheMigrationRunning] = useState(false)
const [cacheMigrationDone, setCacheMigrationDone] = useState(false)
const [cacheMigrationError, setCacheMigrationError] = useState<string | null>(null)
const postsContainerRef = useRef<HTMLDivElement>(null)
const jumpCalendarWrapRef = useRef<HTMLDivElement | null>(null)
@@ -185,6 +218,7 @@ export default function SnsPage() {
const contactsCountBatchTimerRef = useRef<number | null>(null)
const jumpDateCountsCacheRef = useRef<Map<string, Record<string, number>>>(new Map())
const jumpDateRequestSeqRef = useRef(0)
const checkedCacheMigrationRef = useRef(false)
// Sync posts ref
useEffect(() => {
@@ -595,6 +629,133 @@ export default function SnsPage() {
}
}, [persistSnsPageCache])
const markCacheMigrationPrompted = useCallback(() => {
try {
window.sessionStorage.setItem(SNS_CACHE_MIGRATION_PROMPT_SESSION_KEY, '1')
} catch {
// ignore session storage failures
}
}, [])
const hasCacheMigrationPrompted = useCallback(() => {
try {
return window.sessionStorage.getItem(SNS_CACHE_MIGRATION_PROMPT_SESSION_KEY) === '1'
} catch {
return false
}
}, [])
const checkCacheMigrationStatus = useCallback(async () => {
if (checkedCacheMigrationRef.current) return
checkedCacheMigrationRef.current = true
if (hasCacheMigrationPrompted()) return
try {
const result = await window.electronAPI.sns.getCacheMigrationStatus()
if (!result?.success || !result.needed) return
const totalFiles = Math.max(0, Number(result.totalFiles || 0))
const items = Array.isArray(result.items)
? result.items.map((item) => ({
label: String(item.label || '').trim(),
sourceDir: String(item.sourceDir || '').trim(),
targetDir: String(item.targetDir || '').trim(),
fileCount: Math.max(0, Number(item.fileCount || 0))
})).filter((item) => item.label && item.sourceDir && item.targetDir && item.fileCount > 0)
: []
if (totalFiles <= 0 || items.length === 0) return
setCacheMigrationStatus({
totalFiles,
legacyBaseDir: result.legacyBaseDir,
currentBaseDir: result.currentBaseDir,
items
})
setCacheMigrationProgress(null)
setCacheMigrationDone(false)
setCacheMigrationError(null)
setShowCacheMigrationDialog(true)
markCacheMigrationPrompted()
} catch (error) {
console.error('Failed to check SNS cache migration status:', error)
}
}, [hasCacheMigrationPrompted, markCacheMigrationPrompted])
const startCacheMigration = useCallback(async () => {
const total = Math.max(0, cacheMigrationStatus?.totalFiles || 0)
setCacheMigrationError(null)
setCacheMigrationDone(false)
setCacheMigrationRunning(true)
setCacheMigrationProgress({
status: 'running',
phase: 'copying',
current: 0,
total,
copied: 0,
skipped: 0,
remaining: total,
message: '准备迁移...'
})
const removeProgress = window.electronAPI.sns.onCacheMigrationProgress((payload) => {
if (!payload) return
setCacheMigrationProgress({
status: payload.status,
phase: payload.phase,
current: Math.max(0, Number(payload.current || 0)),
total: Math.max(0, Number(payload.total || 0)),
copied: Math.max(0, Number(payload.copied || 0)),
skipped: Math.max(0, Number(payload.skipped || 0)),
remaining: Math.max(0, Number(payload.remaining || 0)),
message: payload.message,
currentItemLabel: payload.currentItemLabel
})
if (payload.status === 'done') {
setCacheMigrationDone(true)
setCacheMigrationError(null)
} else if (payload.status === 'error') {
setCacheMigrationError(payload.message || '迁移失败')
}
})
try {
const result = await window.electronAPI.sns.startCacheMigration()
if (!result?.success) {
setCacheMigrationError(result?.error || '迁移失败')
} else {
const totalFiles = Math.max(0, Number(result.totalFiles || 0))
if (totalFiles === 0) {
setCacheMigrationDone(true)
setCacheMigrationProgress({
status: 'done',
phase: 'done',
current: 0,
total: 0,
copied: 0,
skipped: 0,
remaining: 0,
message: result.message || '无需迁移'
})
} else {
// 兜底:若 done 事件因时序原因未到达,仍以返回结果收敛到完成态。
setCacheMigrationDone(true)
setCacheMigrationProgress((prev) => prev || {
status: 'done',
phase: 'done',
current: totalFiles,
total: totalFiles,
copied: Math.max(0, Number(result.copied || 0)),
skipped: Math.max(0, Number(result.skipped || 0)),
remaining: 0,
message: '迁移完成'
})
}
}
} catch (error) {
setCacheMigrationError(String((error as Error)?.message || error || '迁移失败'))
} finally {
removeProgress()
setCacheMigrationRunning(false)
}
}, [cacheMigrationStatus?.totalFiles])
const renderOverviewRangeText = () => {
if (overviewStatsStatus === 'error') {
return (
@@ -1256,7 +1417,8 @@ export default function SnsPage() {
void hydrateSnsPageCache()
loadContacts()
loadOverviewStats()
}, [hydrateSnsPageCache, loadContacts, loadOverviewStats])
void checkCacheMigrationStatus()
}, [checkCacheMigrationStatus, hydrateSnsPageCache, loadContacts, loadOverviewStats])
useEffect(() => {
const syncCurrentUserProfile = async () => {
@@ -1659,6 +1821,117 @@ export default function SnsPage() {
</div>
)}
{showCacheMigrationDialog && cacheMigrationStatus && (
<div
className="modal-overlay"
onClick={() => {
if (cacheMigrationRunning) return
setShowCacheMigrationDialog(false)
}}
>
<div className="sns-cache-migration-dialog" onClick={(e) => e.stopPropagation()}>
<button
className="close-btn sns-cache-migration-close"
onClick={() => !cacheMigrationRunning && setShowCacheMigrationDialog(false)}
disabled={cacheMigrationRunning}
>
<X size={18} />
</button>
<div className="sns-cache-migration-header">
<div className="sns-cache-migration-title"></div>
<div className="sns-cache-migration-subtitle">
</div>
</div>
<div className="sns-cache-migration-body">
<div className="sns-cache-migration-meta">
<span></span>
<strong>{cacheMigrationStatus.totalFiles}</strong>
</div>
{cacheMigrationProgress && (
<div className="sns-cache-migration-progress">
<div className="sns-cache-migration-progress-bar">
<div
className="sns-cache-migration-progress-fill"
style={{
width: cacheMigrationProgress.total > 0
? `${Math.min(100, Math.round((cacheMigrationProgress.current / cacheMigrationProgress.total) * 100))}%`
: '100%'
}}
/>
</div>
<div className="sns-cache-migration-progress-text">
<span>{cacheMigrationProgress.message || '迁移中...'}</span>
<span>
{cacheMigrationProgress.copied} {cacheMigrationProgress.remaining} {cacheMigrationProgress.skipped}
</span>
</div>
</div>
)}
{!cacheMigrationProgress && (
<div className="sns-cache-migration-items">
{cacheMigrationStatus.items.map((item, idx) => (
<div className="sns-cache-migration-item" key={`${item.label}-${idx}`}>
<div className="sns-cache-migration-item-title">{item.label}</div>
<div className="sns-cache-migration-item-detail">
{item.fileCount} · {item.sourceDir} {item.targetDir}
</div>
</div>
))}
</div>
)}
{cacheMigrationError && (
<div className="sns-cache-migration-error">
<AlertCircle size={14} />
<span>{cacheMigrationError}</span>
</div>
)}
{cacheMigrationDone && !cacheMigrationError && (
<div className="sns-cache-migration-success">
<CheckCircle size={14} />
<span></span>
</div>
)}
</div>
<div className="sns-cache-migration-actions">
{!cacheMigrationDone ? (
<>
<button
className="sns-cache-migration-btn secondary"
onClick={() => setShowCacheMigrationDialog(false)}
disabled={cacheMigrationRunning}
>
</button>
<button
className="sns-cache-migration-btn primary"
onClick={() => { void startCacheMigration() }}
disabled={cacheMigrationRunning}
>
{cacheMigrationRunning ? '迁移中...' : '开始迁移'}
</button>
</>
) : (
<button
className="sns-cache-migration-btn primary"
onClick={() => setShowCacheMigrationDialog(false)}
disabled={cacheMigrationRunning}
>
</button>
)}
</div>
</div>
</div>
)}
{/* 朋友圈防删除插件对话框 */}
{showTriggerDialog && (
<div className="modal-overlay" onClick={() => { setShowTriggerDialog(false); setTriggerMessage(null) }}>