|
@@ -99,6 +99,10 @@ const AppMappingList: React.FC<AppMappingListProps> = ({ token }) => {
|
|
|
// 滚动容器引用
|
|
// 滚动容器引用
|
|
|
const scrollContainerRef = useRef<HTMLDivElement>(null)
|
|
const scrollContainerRef = useRef<HTMLDivElement>(null)
|
|
|
const searchTimerRef = useRef<NodeJS.Timeout | null>(null)
|
|
const searchTimerRef = useRef<NodeJS.Timeout | null>(null)
|
|
|
|
|
+ // 请求版本号,避免慢请求覆盖新搜索结果
|
|
|
|
|
+ const loadIdRef = useRef(0)
|
|
|
|
|
+ const searchKeywordRef = useRef(searchKeyword)
|
|
|
|
|
+ searchKeywordRef.current = searchKeyword
|
|
|
|
|
|
|
|
// 同步 skipRef
|
|
// 同步 skipRef
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
@@ -137,6 +141,8 @@ const AppMappingList: React.FC<AppMappingListProps> = ({ token }) => {
|
|
|
const loadMappings = useCallback(async (isRefresh: boolean = false) => {
|
|
const loadMappings = useCallback(async (isRefresh: boolean = false) => {
|
|
|
if (isLoading || (isLoadingMore && !isRefresh)) return
|
|
if (isLoading || (isLoadingMore && !isRefresh)) return
|
|
|
|
|
|
|
|
|
|
+ const currentLoadId = (loadIdRef.current += 1)
|
|
|
|
|
+
|
|
|
try {
|
|
try {
|
|
|
if (isRefresh) {
|
|
if (isRefresh) {
|
|
|
setIsRefreshing(true)
|
|
setIsRefreshing(true)
|
|
@@ -150,28 +156,30 @@ const AppMappingList: React.FC<AppMappingListProps> = ({ token }) => {
|
|
|
setError(null)
|
|
setError(null)
|
|
|
|
|
|
|
|
const currentSkip = isRefresh ? 0 : skipRef.current
|
|
const currentSkip = isRefresh ? 0 : skipRef.current
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
|
|
+ const keyword = searchKeywordRef.current.trim() || undefined
|
|
|
const result = await api.getLaunchpadApps(token, {
|
|
const result = await api.getLaunchpadApps(token, {
|
|
|
skip: currentSkip,
|
|
skip: currentSkip,
|
|
|
limit: pageSize,
|
|
limit: pageSize,
|
|
|
- app_name: searchKeyword || undefined
|
|
|
|
|
|
|
+ app_name: keyword
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
|
|
+ // 仅应用最新请求的结果,避免慢请求覆盖搜索结果
|
|
|
|
|
+ if (currentLoadId !== loadIdRef.current) return
|
|
|
|
|
+
|
|
|
if (isRefresh) {
|
|
if (isRefresh) {
|
|
|
setMappings(result.items)
|
|
setMappings(result.items)
|
|
|
setTotal(result.total)
|
|
setTotal(result.total)
|
|
|
setHasMore(result.items.length < result.total)
|
|
setHasMore(result.items.length < result.total)
|
|
|
} else {
|
|
} else {
|
|
|
- // 去重(避免重复数据)
|
|
|
|
|
setMappings(prev => {
|
|
setMappings(prev => {
|
|
|
const existingIds = new Set(prev.map(m => `${m.app_id}_${m.mapped_key}`))
|
|
const existingIds = new Set(prev.map(m => `${m.app_id}_${m.mapped_key}`))
|
|
|
const uniqueNewMappings = result.items.filter(
|
|
const uniqueNewMappings = result.items.filter(
|
|
|
m => !existingIds.has(`${m.app_id}_${m.mapped_key}`)
|
|
m => !existingIds.has(`${m.app_id}_${m.mapped_key}`)
|
|
|
)
|
|
)
|
|
|
-
|
|
|
|
|
- setHasMore(prev.length + uniqueNewMappings.length < result.total)
|
|
|
|
|
-
|
|
|
|
|
- return [...prev, ...uniqueNewMappings]
|
|
|
|
|
|
|
+ const nextList = [...prev, ...uniqueNewMappings]
|
|
|
|
|
+ setHasMore(nextList.length < result.total)
|
|
|
|
|
+ return nextList
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -181,14 +189,17 @@ const AppMappingList: React.FC<AppMappingListProps> = ({ token }) => {
|
|
|
setIsLoadingMore(false)
|
|
setIsLoadingMore(false)
|
|
|
}
|
|
}
|
|
|
} catch (err: any) {
|
|
} catch (err: any) {
|
|
|
|
|
+ if (currentLoadId !== loadIdRef.current) return
|
|
|
logger.error('AppMappingList: Failed to load mappings', err)
|
|
logger.error('AppMappingList: Failed to load mappings', err)
|
|
|
setError(err.message || '加载应用列表失败')
|
|
setError(err.message || '加载应用列表失败')
|
|
|
setIsRefreshing(false)
|
|
setIsRefreshing(false)
|
|
|
setIsLoadingMore(false)
|
|
setIsLoadingMore(false)
|
|
|
} finally {
|
|
} finally {
|
|
|
- setIsLoading(false)
|
|
|
|
|
|
|
+ if (currentLoadId === loadIdRef.current) {
|
|
|
|
|
+ setIsLoading(false)
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
- }, [token, searchKeyword, pageSize])
|
|
|
|
|
|
|
+ }, [token, pageSize])
|
|
|
|
|
|
|
|
// 初始化加载
|
|
// 初始化加载
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
@@ -242,10 +253,17 @@ const AppMappingList: React.FC<AppMappingListProps> = ({ token }) => {
|
|
|
}, 100)
|
|
}, 100)
|
|
|
}, [isRefreshing, isLoading, isLoadingMore, hasMore, loadMappings])
|
|
}, [isRefreshing, isLoading, isLoadingMore, hasMore, loadMappings])
|
|
|
|
|
|
|
|
|
|
+ // 前端按关键词过滤展示列表(兜底:后端未过滤时也能正确显示)
|
|
|
|
|
+ const displayedMappings = useMemo(() => {
|
|
|
|
|
+ const kw = searchKeyword.trim().toLowerCase()
|
|
|
|
|
+ if (!kw) return mappings
|
|
|
|
|
+ return mappings.filter(m => (m.app_name || '').toLowerCase().includes(kw))
|
|
|
|
|
+ }, [mappings, searchKeyword])
|
|
|
|
|
+
|
|
|
const categorizedApps = useMemo(() => {
|
|
const categorizedApps = useMemo(() => {
|
|
|
const groupMap = new Map<number | null, { categoryId: number | null; categoryName: string; apps: AppMapping[] }>()
|
|
const groupMap = new Map<number | null, { categoryId: number | null; categoryName: string; apps: AppMapping[] }>()
|
|
|
|
|
|
|
|
- for (const app of mappings) {
|
|
|
|
|
|
|
+ for (const app of displayedMappings) {
|
|
|
const key = app.category_id
|
|
const key = app.category_id
|
|
|
if (!groupMap.has(key)) {
|
|
if (!groupMap.has(key)) {
|
|
|
groupMap.set(key, {
|
|
groupMap.set(key, {
|
|
@@ -265,7 +283,7 @@ const AppMappingList: React.FC<AppMappingListProps> = ({ token }) => {
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
return groups
|
|
return groups
|
|
|
- }, [mappings])
|
|
|
|
|
|
|
+ }, [displayedMappings])
|
|
|
|
|
|
|
|
// 处理 SSO 登录
|
|
// 处理 SSO 登录
|
|
|
const handleSsoLogin = async (mapping: AppMapping) => {
|
|
const handleSsoLogin = async (mapping: AppMapping) => {
|
|
@@ -548,7 +566,7 @@ const AppMappingList: React.FC<AppMappingListProps> = ({ token }) => {
|
|
|
))
|
|
))
|
|
|
) : (
|
|
) : (
|
|
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(240px, 1fr))', gap: '20px' }}>
|
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(240px, 1fr))', gap: '20px' }}>
|
|
|
- {mappings.map((mapping) => (
|
|
|
|
|
|
|
+ {displayedMappings.map((mapping) => (
|
|
|
<div
|
|
<div
|
|
|
key={`${mapping.app_id}_${mapping.mapped_key}`}
|
|
key={`${mapping.app_id}_${mapping.mapped_key}`}
|
|
|
className={`browser-card ${!mapping.is_active || loginLoading[mapping.app_id] ? 'disabled' : ''}`}
|
|
className={`browser-card ${!mapping.is_active || loginLoading[mapping.app_id] ? 'disabled' : ''}`}
|
|
@@ -582,17 +600,17 @@ const AppMappingList: React.FC<AppMappingListProps> = ({ token }) => {
|
|
|
)}
|
|
)}
|
|
|
|
|
|
|
|
{/* 没有更多数据提示 */}
|
|
{/* 没有更多数据提示 */}
|
|
|
- {!hasMore && mappings.length > 0 && (
|
|
|
|
|
|
|
+ {!hasMore && displayedMappings.length > 0 && (
|
|
|
<div style={{ textAlign: 'center', padding: '20px', color: '#999' }}>
|
|
<div style={{ textAlign: 'center', padding: '20px', color: '#999' }}>
|
|
|
没有更多了
|
|
没有更多了
|
|
|
</div>
|
|
</div>
|
|
|
)}
|
|
)}
|
|
|
|
|
|
|
|
{/* 空状态 */}
|
|
{/* 空状态 */}
|
|
|
- {!isLoading && mappings.length === 0 && !error && (
|
|
|
|
|
|
|
+ {!isLoading && displayedMappings.length === 0 && !error && (
|
|
|
<div style={{ textAlign: 'center', padding: '60px 20px', color: '#999' }}>
|
|
<div style={{ textAlign: 'center', padding: '60px 20px', color: '#999' }}>
|
|
|
<div style={{ fontSize: '64px', marginBottom: '16px', opacity: 0.5 }}>📱</div>
|
|
<div style={{ fontSize: '64px', marginBottom: '16px', opacity: 0.5 }}>📱</div>
|
|
|
- <div style={{ fontSize: '16px' }}>暂无应用</div>
|
|
|
|
|
|
|
+ <div style={{ fontSize: '16px' }}>{searchKeyword.trim() ? '未找到匹配的应用' : '暂无应用'}</div>
|
|
|
</div>
|
|
</div>
|
|
|
)}
|
|
)}
|
|
|
</div>
|
|
</div>
|