Browse Source

V1.1.6闪烁问题

liuq 3 weeks ago
parent
commit
71a1909564

+ 23 - 5
src/renderer/src/components/AppMappingList.tsx

@@ -109,7 +109,7 @@ const AppMappingList: React.FC<AppMappingListProps> = ({ token, currentUserId })
     })
   }, [recentStorageKey])
 
-  // 加载启动台全量列表(无服务端分页);silent:不顶全屏 loading、不闪清空,失败仅 warn
+  // 加载启动台全量列表(无服务端分页);silent:不顶全屏 loading;刷新保留当前列表直至新数据返回,避免图标/卡片整页重挂载闪烁
   const loadMappings = useCallback(
     async (options?: { silent?: boolean }) => {
       const silent = options?.silent === true
@@ -121,7 +121,6 @@ const AppMappingList: React.FC<AppMappingListProps> = ({ token, currentUserId })
       try {
         if (!silent) {
           setIsRefreshing(true)
-          setMappings([])
           setIsLoading(true)
           setError(null)
         }
@@ -313,7 +312,17 @@ const AppMappingList: React.FC<AppMappingListProps> = ({ token, currentUserId })
   )
 
   return (
-    <div style={{ flex: 1, padding: '40px', overflowY: 'auto' }}>
+    <div
+      style={{
+        flex: 1,
+        minHeight: 0,
+        padding: '40px',
+        overflow: 'hidden',
+        display: 'flex',
+        flexDirection: 'column',
+      }}
+    >
+      <div style={{ flexShrink: 0 }}>
       <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '30px' }}>
         <h2 style={{ margin: 0, color: '#333', WebkitAppRegion: 'drag' } as React.CSSProperties}>应用中心</h2>
         <div style={{ display: 'flex', alignItems: 'center', gap: '8px', WebkitAppRegion: 'no-drag' } as React.CSSProperties}>
@@ -480,11 +489,20 @@ const AppMappingList: React.FC<AppMappingListProps> = ({ token, currentUserId })
         </div>
       )}
 
-      {/* 应用列表 */}
+      </div>
+
+      {/* 应用列表:仅此区域滚动,顶部标题/搜索/最近打开固定 */}
       <div
         ref={scrollContainerRef}
         onScroll={handleScroll}
-        style={{ WebkitAppRegion: 'no-drag' } as React.CSSProperties}
+        style={
+          {
+            flex: 1,
+            minHeight: 0,
+            overflowY: 'auto',
+            WebkitAppRegion: 'no-drag',
+          } as React.CSSProperties
+        }
       >
         {/* 下拉刷新提示 */}
         {isRefreshing && (

+ 54 - 15
src/renderer/src/components/LaunchpadAppIcon.tsx

@@ -1,4 +1,4 @@
-import React, { useEffect, useState } from 'react'
+import React, { useEffect, useLayoutEffect, useMemo, useState } from 'react'
 import type { LaunchpadIconResolvePayload, LaunchpadIconResolveResult } from '../types/ipcLaunchpad'
 import { getAvatarPlaceholder, type ApplicationIconShape } from '../utils/avatarUtils'
 
@@ -7,6 +7,23 @@ function isProbablyHttpIconUrl(raw: string): boolean {
   return /^https?:\/\//i.test(s)
 }
 
+/** 解析后的自定义协议 URL;key 与主进程 `isLaunchpadCacheCurrent` 一致,避免签名 URL 每次变导致缓存失效 */
+const launchpadResolvedIconCache = new Map<string, string>()
+
+/** 有 `icon_object_key` 时用 appId+objectKey;否则用整段 URL(与主进程仅比 URL 分支一致) */
+function launchpadIconCacheKey(
+  appId: string,
+  iconUrlTrim: string,
+  iconObjectKey: string | null | undefined
+): string {
+  const objTrim =
+    typeof iconObjectKey === 'string' && iconObjectKey.trim() ? iconObjectKey.trim() : null
+  if (objTrim != null) {
+    return `${appId}\0obj:${objTrim}`
+  }
+  return `${appId}\0url:${iconUrlTrim}`
+}
+
 interface LaunchpadAppIconProps {
   appName: string
   appId: string
@@ -29,21 +46,31 @@ export function LaunchpadAppIcon({
   extraStyle,
   applicationIconShape = 'roundedSquare',
 }: LaunchpadAppIconProps): JSX.Element {
-  const [resolvedSrc, setResolvedSrc] = useState<string | null>(null)
+  const cacheKey = useMemo(() => {
+    const urlTrim = typeof iconUrl === 'string' ? iconUrl.trim() : ''
+    if (!urlTrim || !isProbablyHttpIconUrl(urlTrim)) return null
+    return launchpadIconCacheKey(appId, urlTrim, iconObjectKey)
+  }, [appId, iconUrl, iconObjectKey])
+
+  /** 仅当 `key === 当前 cacheKey` 时参与展示,避免换应用瞬间错用上一 IPC 结果 */
+  const [ipcMatch, setIpcMatch] = useState<{ key: string; src: string } | null>(null)
   const [usePlaceholder, setUsePlaceholder] = useState(false)
 
-  useEffect(() => {
+  useLayoutEffect(() => {
     setUsePlaceholder(false)
+    setIpcMatch((prev) => (prev && cacheKey && prev.key === cacheKey ? prev : null))
+  }, [cacheKey])
+
+  useEffect(() => {
+    if (!cacheKey) return
 
     const urlTrim = typeof iconUrl === 'string' ? iconUrl.trim() : ''
-    if (!urlTrim || !isProbablyHttpIconUrl(urlTrim)) {
-      setResolvedSrc(null)
-      return
-    }
+    if (!urlTrim || !isProbablyHttpIconUrl(urlTrim)) return
 
     const invoke = window.electron?.ipcRenderer?.invoke
     if (!invoke) {
-      setResolvedSrc(null)
+      launchpadResolvedIconCache.delete(cacheKey)
+      setIpcMatch((prev) => (prev?.key === cacheKey ? null : prev))
       return
     }
 
@@ -60,19 +87,28 @@ export function LaunchpadAppIcon({
 
         if (cancelled) return
         if (res.kind === 'custom' && res.url) {
-          setResolvedSrc(res.url)
+          launchpadResolvedIconCache.set(cacheKey, res.url)
+          setIpcMatch({ key: cacheKey, src: res.url })
         } else {
-          setResolvedSrc(null)
+          launchpadResolvedIconCache.delete(cacheKey)
+          setIpcMatch((prev) => (prev?.key === cacheKey ? null : prev))
         }
       } catch {
-        if (!cancelled) setResolvedSrc(null)
+        if (!cancelled) {
+          launchpadResolvedIconCache.delete(cacheKey)
+          setIpcMatch((prev) => (prev?.key === cacheKey ? null : prev))
+        }
       }
     })()
 
     return () => {
       cancelled = true
     }
-  }, [appId, iconUrl, iconObjectKey])
+  }, [cacheKey, appId, iconUrl, iconObjectKey])
+
+  const cachedSrc = cacheKey ? launchpadResolvedIconCache.get(cacheKey) ?? null : null
+  const ipcSrc = ipcMatch?.key === cacheKey ? ipcMatch.src : null
+  const displaySrc = cachedSrc ?? ipcSrc
 
   const placeholderNode = getAvatarPlaceholder(
     appName,
@@ -83,7 +119,7 @@ export function LaunchpadAppIcon({
     applicationIconShape
   )
 
-  if (!resolvedSrc || usePlaceholder) {
+  if (!displaySrc || usePlaceholder) {
     return <>{placeholderNode}</>
   }
 
@@ -92,10 +128,13 @@ export function LaunchpadAppIcon({
 
   return (
     <img
-      src={resolvedSrc}
+      src={displaySrc}
       alt=""
       draggable={false}
-      onError={() => setUsePlaceholder(true)}
+      onError={() => {
+        if (cacheKey) launchpadResolvedIconCache.delete(cacheKey)
+        setUsePlaceholder(true)
+      }}
       style={{
         width: sizePx,
         height: sizePx,