A:软件列表展示使用虚拟列表处理

This commit is contained in:
jimmy committed 2025-07-27 16:10:50 +08:00
1 parent 355dd1044f
commit 53fa1aa4af
3 files changed
+223 -63

No files matched your search

+2 -1
View File
@@ -26,7 +26,8 @@
"unplugin-vue-components": "^28.7.0", "unplugin-vue-components": "^28.7.0",
"vicons": "^0.0.1", "vicons": "^0.0.1",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-router": "^4.5.0" "vue-router": "^4.5.0",
"vue-virtual-scroller": "2.0.0-beta.8"
}, },
"devDependencies": { "devDependencies": {
"@tsconfig/node22": "^22.0.1", "@tsconfig/node22": "^22.0.1",
+38
View File
@@ -47,6 +47,9 @@ importers:
vue-router: vue-router:
specifier: ^4.5.0 specifier: ^4.5.0
version: 4.5.1(vue@3.5.14(typescript@5.8.3)) version: 4.5.1(vue@3.5.14(typescript@5.8.3))
vue-virtual-scroller:
specifier: 2.0.0-beta.8
version: 2.0.0-beta.8(vue@3.5.14(typescript@5.8.3))
devDependencies: devDependencies:
'@tsconfig/node22': '@tsconfig/node22':
specifier: ^22.0.1 specifier: ^22.0.1
@@ -1750,6 +1753,9 @@ packages:
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==, tarball: https://registry.npmmirror.com/minipass/-/minipass-7.1.2.tgz} resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==, tarball: https://registry.npmmirror.com/minipass/-/minipass-7.1.2.tgz}
engines: {node: '>=16 || 14 >=14.17'} engines: {node: '>=16 || 14 >=14.17'}
mitt@2.1.0:
resolution: {integrity: sha512-ILj2TpLiysu2wkBbWjAmww7TkZb65aiQO+DkVdUTBpBXq+MHYiETENkKFMtsJZX1Lf4pe4QOrTSjIfUwN5lRdg==, tarball: https://registry.npmmirror.com/mitt/-/mitt-2.1.0.tgz}
mitt@3.0.1: mitt@3.0.1:
resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==, tarball: https://registry.npmmirror.com/mitt/-/mitt-3.0.1.tgz} resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==, tarball: https://registry.npmmirror.com/mitt/-/mitt-3.0.1.tgz}
@@ -2345,6 +2351,16 @@ packages:
peerDependencies: peerDependencies:
eslint: ^8.57.0 || ^9.0.0 eslint: ^8.57.0 || ^9.0.0
vue-observe-visibility@2.0.0-alpha.1:
resolution: {integrity: sha512-flFbp/gs9pZniXR6fans8smv1kDScJ8RS7rEpMjhVabiKeq7Qz3D9+eGsypncjfIyyU84saU88XZ0zjbD6Gq/g==, tarball: https://registry.npmmirror.com/vue-observe-visibility/-/vue-observe-visibility-2.0.0-alpha.1.tgz}
peerDependencies:
vue: ^3.0.0
vue-resize@2.0.0-alpha.1:
resolution: {integrity: sha512-7+iqOueLU7uc9NrMfrzbG8hwMqchfVfSzpVlCMeJQe4pyibqyoifDNbKTZvwxZKDvGkB+PdFeKvnGZMoEb8esg==, tarball: https://registry.npmmirror.com/vue-resize/-/vue-resize-2.0.0-alpha.1.tgz}
peerDependencies:
vue: ^3.0.0
vue-router@4.5.1: vue-router@4.5.1:
resolution: {integrity: sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==, tarball: https://registry.npmmirror.com/vue-router/-/vue-router-4.5.1.tgz} resolution: {integrity: sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==, tarball: https://registry.npmmirror.com/vue-router/-/vue-router-4.5.1.tgz}
peerDependencies: peerDependencies:
@@ -2356,6 +2372,11 @@ packages:
peerDependencies: peerDependencies:
typescript: '>=5.0.0' typescript: '>=5.0.0'
vue-virtual-scroller@2.0.0-beta.8:
resolution: {integrity: sha512-b8/f5NQ5nIEBRTNi6GcPItE4s7kxNHw2AIHLtDp+2QvqdTjVN0FgONwX9cr53jWRgnu+HRLPaWDOR2JPI5MTfQ==, tarball: https://registry.npmmirror.com/vue-virtual-scroller/-/vue-virtual-scroller-2.0.0-beta.8.tgz}
peerDependencies:
vue: ^3.2.0
vue@3.5.14: vue@3.5.14:
resolution: {integrity: sha512-LbOm50/vZFG6Mhy6KscQYXZMQ0LMCC/y40HDJPPvGFQ+i/lUH+PJHR6C3assgOQiXdl6tAfsXHbXYVBZZu65ew==, tarball: https://registry.npmmirror.com/vue/-/vue-3.5.14.tgz} resolution: {integrity: sha512-LbOm50/vZFG6Mhy6KscQYXZMQ0LMCC/y40HDJPPvGFQ+i/lUH+PJHR6C3assgOQiXdl6tAfsXHbXYVBZZu65ew==, tarball: https://registry.npmmirror.com/vue/-/vue-3.5.14.tgz}
peerDependencies: peerDependencies:
@@ -4077,6 +4098,8 @@ snapshots:
minipass@7.1.2: {} minipass@7.1.2: {}
mitt@2.1.0: {}
mitt@3.0.1: {} mitt@3.0.1: {}
mlly@1.7.4: mlly@1.7.4:
@@ -4694,6 +4717,14 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
vue-observe-visibility@2.0.0-alpha.1(vue@3.5.14(typescript@5.8.3)):
dependencies:
vue: 3.5.14(typescript@5.8.3)
vue-resize@2.0.0-alpha.1(vue@3.5.14(typescript@5.8.3)):
dependencies:
vue: 3.5.14(typescript@5.8.3)
vue-router@4.5.1(vue@3.5.14(typescript@5.8.3)): vue-router@4.5.1(vue@3.5.14(typescript@5.8.3)):
dependencies: dependencies:
'@vue/devtools-api': 6.6.4 '@vue/devtools-api': 6.6.4
@@ -4705,6 +4736,13 @@ snapshots:
'@vue/language-core': 2.2.10(typescript@5.8.3) '@vue/language-core': 2.2.10(typescript@5.8.3)
typescript: 5.8.3 typescript: 5.8.3
vue-virtual-scroller@2.0.0-beta.8(vue@3.5.14(typescript@5.8.3)):
dependencies:
mitt: 2.1.0
vue: 3.5.14(typescript@5.8.3)
vue-observe-visibility: 2.0.0-alpha.1(vue@3.5.14(typescript@5.8.3))
vue-resize: 2.0.0-alpha.1(vue@3.5.14(typescript@5.8.3))
vue@3.5.14(typescript@5.8.3): vue@3.5.14(typescript@5.8.3):
dependencies: dependencies:
'@vue/compiler-dom': 3.5.14 '@vue/compiler-dom': 3.5.14
@@ -22,18 +22,37 @@
</div> </div>
</div> </div>
<!-- 应用列表容器 - 添加独立滚动 --> <!-- 应用列表容器 - 虚拟滚动 -->
<div class="app-list-container"> <div class="app-list-container">
<div <!-- 空状态显示 -->
<div v-if="filteredApplications.length === 0" class="empty-state">
<n-icon :component="DesktopOutline" size="48" class="empty-icon" />
<div class="empty-title">
{{ isFiltering && filterText ? '没有找到匹配的应用' : '暂无运行的应用' }}
</div>
<div class="empty-subtitle">
{{
isFiltering && filterText
? `没有找到包含 "${filterText}" 的应用程序,请尝试其他搜索词`
: '系统中没有检测到正在运行的应用程序'
}}
</div>
</div>
<!-- 虚拟滚动列表 -->
<RecycleScroller
v-else
ref="recycleScrollerRef"
class="app-list scrollbar-primary scrollbar-thin" class="app-list scrollbar-primary scrollbar-thin"
ref="appListRef" :items="filteredApplications"
:item-size="itemHeight"
key-field="id"
v-slot="{ item: app, index }"
tabindex="0" tabindex="0"
@keydown="handleKeyDown" @keydown="handleKeyDown"
@blur="handleListBlur" @blur="handleListBlur"
> >
<div <div
v-for="(app, index) in filteredApplications"
:key="app.id"
:ref="(el) => setAppItemRef(el, index)" :ref="(el) => setAppItemRef(el, index)"
class="app-item" class="app-item"
:class="{ :class="{
@@ -103,32 +122,18 @@
></div> ></div>
</div> </div>
</div> </div>
</RecycleScroller>
<!-- 空状态 -->
<div v-if="filteredApplications.length === 0" class="empty-state">
<n-icon :component="DesktopOutline" size="48" class="empty-icon" />
<div class="empty-title">
{{ isFiltering && filterText ? '没有找到匹配的应用' : '暂无运行的应用' }}
</div>
<div class="empty-subtitle">
{{
isFiltering && filterText
? `没有找到包含 "${filterText}" 的应用程序,请尝试其他搜索词`
: '系统中没有检测到正在运行的应用程序'
}}
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, defineProps, watch, onMounted, onUnmounted, nextTick, computed } from 'vue' import { ref, defineProps, watch, onMounted, onUnmounted, nextTick, computed, defineExpose } from 'vue'
import { NIcon, NHighlight, useThemeVars } from 'naive-ui' import { NIcon, NHighlight, useThemeVars } from 'naive-ui'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { DesktopOutline } from '@vicons/ionicons5' import { DesktopOutline } from '@vicons/ionicons5'
import { RecycleScroller } from 'vue-virtual-scroller'
import { httpClient } from '@/utils/http.ts' import { httpClient } from '@/utils/http.ts'
import { ResponseModel, SubscriptionInfo } from '@/types/response' import { ResponseModel, SubscriptionInfo } from '@/types/response'
import { useWebSocketStore } from '@/stores/websocketStore' import { useWebSocketStore } from '@/stores/websocketStore'
@@ -139,6 +144,7 @@ import { convertFileSize } from '@/utils/fileUtil'
import { FILE_SIZE_UNIT_ENUM } from '@/constants/enums' import { FILE_SIZE_UNIT_ENUM } from '@/constants/enums'
import lodash from 'lodash' import lodash from 'lodash'
import { getGradientColor } from '@/utils/colorUtils' import { getGradientColor } from '@/utils/colorUtils'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
// Props // Props
const props = defineProps<{ const props = defineProps<{
@@ -183,10 +189,13 @@ const getProcessCount = (app: ApplicationType) => {
} }
// Refs // Refs
const appListRef = ref<HTMLDivElement>() const recycleScrollerRef = ref<InstanceType<typeof RecycleScroller>>()
const appItemRefs = ref<(HTMLDivElement | null)[]>([]) const appItemRefs = ref<(HTMLDivElement | null)[]>([])
const focusedIndex = ref<number>(-1) // 添加聚焦索引 const focusedIndex = ref<number>(-1) // 添加聚焦索引
// 虚拟滚动配置
const itemHeight = 88 // 每个应用项的固定高度 (增加间距)
// 设置应用项目的 ref // 设置应用项目的 ref
const setAppItemRef = (el: unknown, index: number) => { const setAppItemRef = (el: unknown, index: number) => {
if (el) { if (el) {
@@ -223,14 +232,23 @@ onMounted(() => {
// 监听应用列表变化,自动选中第一个 // 监听应用列表变化,自动选中第一个
watch( watch(
filteredApplications, filteredApplications,
async (newAppInfos) => { async (newAppInfos, oldAppInfos) => {
// 重置聚焦索引 // 重置聚焦索引
// focusedIndex.value = -1 // focusedIndex.value = -1
// 虚拟滚动列表数据更新处理
await nextTick()
// 强制虚拟滚动器更新布局(处理数据变化)
if (recycleScrollerRef.value) {
// 通知虚拟滚动器数据已变化
recycleScrollerRef.value.$forceUpdate()
}
// 如果有应用且当前没有选中的应用 // 如果有应用且当前没有选中的应用
if (newAppInfos.length > 0 && !selectedApp.value) { if (newAppInfos.length > 0 && !selectedApp.value) {
await nextTick() await nextTick()
selectApp(newAppInfos[0]) selectApp(newAppInfos[0], true) // 自动选中时需要滚动
} }
// 如果当前选中的应用不在列表中了,选中第一个 // 如果当前选中的应用不在列表中了,选中第一个
else if ( else if (
@@ -239,19 +257,52 @@ onMounted(() => {
!newAppInfos.find((app) => app.id === selectedApp.value?.id) !newAppInfos.find((app) => app.id === selectedApp.value?.id)
) { ) {
await nextTick() await nextTick()
selectApp(newAppInfos[0]) selectApp(newAppInfos[0], true) // 自动选中时需要滚动
} }
// 如果没有应用了,清空选中 // 如果没有应用了,清空选中
else if (newAppInfos.length === 0) { else if (newAppInfos.length === 0) {
applicationStore.setSelectedApp(null) applicationStore.setSelectedApp(null)
} }
// 如果选中的应用仍在列表中,但位置可能发生了变化,滚动到新位置
else if (selectedApp.value && newAppInfos.length > 0) {
const newIndex = newAppInfos.findIndex((app) => app.id === selectedApp.value?.id)
const oldIndex = oldAppInfos?.findIndex((app) => app.id === selectedApp.value?.id) ?? -1
// 如果位置发生了变化,或者这是过滤操作的结果,滚动到新位置
if (newIndex !== -1 && (newIndex !== oldIndex || newAppInfos.length !== oldAppInfos?.length)) {
await nextTick()
scrollToFocusedItem(newIndex)
}
}
}, },
{ immediate: true }, { immediate: true, deep: false },
)
// 监听过滤状态变化,确保虚拟列表正确更新
watch(
[isFiltering, filterText],
async () => {
await nextTick()
// 过滤状态变化时,强制更新虚拟滚动器
if (recycleScrollerRef.value) {
recycleScrollerRef.value.$forceUpdate()
// 如果有选中的应用,滚动到对应位置
if (selectedApp.value) {
const index = filteredApplications.value.findIndex((app) => app.id === selectedApp.value?.id)
if (index !== -1) {
setTimeout(() => {
scrollToFocusedItem(index)
}, 100) // 延迟一点确保虚拟滚动器已更新
}
}
}
}
) )
// 自动聚焦到列表容器 // 自动聚焦到列表容器
nextTick(() => { nextTick(() => {
appListRef.value?.focus() recycleScrollerRef.value?.$el?.focus()
}) })
}) })
@@ -320,9 +371,9 @@ const handleKeyDown = (event: KeyboardEvent) => {
case 'Enter': case 'Enter':
event.preventDefault() event.preventDefault()
// 如果有聚焦的应用,选中它 // 如果有聚焦的应用,选中它(键盘操作需要滚动)
if (focusedIndex.value >= 0 && focusedIndex.value < filteredApplications.value.length) { if (focusedIndex.value >= 0 && focusedIndex.value < filteredApplications.value.length) {
selectApp(filteredApplications.value[focusedIndex.value]) selectApp(filteredApplications.value[focusedIndex.value], true)
} }
break break
@@ -344,20 +395,58 @@ const handleKeyDown = (event: KeyboardEvent) => {
} }
} }
// 滚动到聚焦的项目 // 滚动到聚焦的项目 - 虚拟滚动版本
const scrollToFocusedItem = (index: number) => { const scrollToFocusedItem = (index: number) => {
nextTick(() => { nextTick(() => {
const itemEl = appItemRefs.value[index] if (recycleScrollerRef.value && index >= 0 && index < filteredApplications.value.length) {
if (itemEl && appListRef.value) { try {
const listRect = appListRef.value.getBoundingClientRect() // 使用虚拟滚动的 scrollToItem 方法
const itemRect = itemEl.getBoundingClientRect() recycleScrollerRef.value.scrollToItem(index)
} catch (error) {
console.warn('Virtual scroller scrollToItem failed:', error)
// 如果虚拟滚动失败,尝试手动滚动
const viewport = recycleScrollerRef.value.$el?.querySelector('.vue-recycle-scroller__viewport')
if (viewport) {
const targetPosition = index * itemHeight
viewport.scrollTo({
top: targetPosition,
behavior: 'smooth'
})
}
}
}
})
}
// 如果项目不在可见区域内,滚动到中心位置 // 滚动到指定的应用(外部调用接口)
if (itemRect.top < listRect.top || itemRect.bottom > listRect.bottom) { const scrollToApp = (appId: string) => {
itemEl.scrollIntoView({ const index = filteredApplications.value.findIndex((app) => app.id === appId)
behavior: 'smooth', if (index !== -1) {
block: 'center', scrollToFocusedItem(index)
}) // 可选:同时设置聚焦和选中
focusedIndex.value = index
selectApp(filteredApplications.value[index])
}
}
// 滚动到选中的应用
const scrollToSelectedApp = () => {
if (selectedApp.value) {
const index = filteredApplications.value.findIndex((app) => app.id === selectedApp.value?.id)
if (index !== -1) {
scrollToFocusedItem(index)
}
}
}
// 刷新虚拟滚动器(用于处理数据同步问题)
const refreshVirtualScroller = () => {
nextTick(() => {
if (recycleScrollerRef.value) {
try {
recycleScrollerRef.value.$forceUpdate()
} catch (error) {
console.warn('Virtual scroller refresh failed:', error)
} }
} }
}) })
@@ -386,8 +475,9 @@ const getFirstChar = (name: string): string => {
} }
// 选择应用 // 选择应用
const selectApp = (app: ApplicationType) => { const selectApp = (app: ApplicationType, shouldScroll = false) => {
applicationStore.setSelectedApp(app) applicationStore.setSelectedApp(app)
// 选中后,如果是通过键盘选中的,保持聚焦在当前项 // 选中后,如果是通过键盘选中的,保持聚焦在当前项
// 如果是通过鼠标点击的,会在 mouseLeave 时自动清除聚焦 // 如果是通过鼠标点击的,会在 mouseLeave 时自动清除聚焦
const selectedIndex = filteredApplications.value.findIndex((a: ApplicationType) => a.id === app.id) const selectedIndex = filteredApplications.value.findIndex((a: ApplicationType) => a.id === app.id)
@@ -397,6 +487,13 @@ const selectApp = (app: ApplicationType) => {
// 通过鼠标选中的,清除聚焦 // 通过鼠标选中的,清除聚焦
focusedIndex.value = -1 focusedIndex.value = -1
} }
// 只在需要滚动时才滚动(避免用户点击时的意外滚动)
if (shouldScroll && selectedIndex !== -1) {
nextTick(() => {
scrollToFocusedItem(selectedIndex)
})
}
} }
// 鼠标悬停事件 // 鼠标悬停事件
@@ -409,6 +506,14 @@ const handleMouseLeave = () => {
// 鼠标离开时清除聚焦 // 鼠标离开时清除聚焦
focusedIndex.value = -1 focusedIndex.value = -1
} }
// 暴露给父组件的方法
defineExpose({
scrollToApp,
scrollToSelectedApp,
scrollToFocusedItem,
refreshVirtualScroller,
})
</script> </script>
<style scoped> <style scoped>
@@ -498,42 +603,52 @@ const handleMouseLeave = () => {
flex-direction: column; flex-direction: column;
} }
/* 应用列表 - 独立滚动 */ /* 应用列表 - 虚拟滚动适配 */
.app-list { .app-list {
padding: 5px;
flex: 1; flex: 1;
overflow-y: auto; /* 启用垂直滚动 */ height: 100%; /* 虚拟滚动需要明确的高度 */
overflow-x: hidden;
display: flex;
flex-direction: column;
gap: 8px;
padding-top: 5px;
padding-left: 4px;
padding-right: 8px; /* 为滚动条留出空间 */
margin-right: -8px; /* 抵消padding */
outline: none; /* 移除聚焦时的默认轮廓 */ outline: none; /* 移除聚焦时的默认轮廓 */
} }
/* 自定义滚动条样式 */ /* 虚拟滚动器内部样式适配 */
.app-list::-webkit-scrollbar { .app-list .vue-recycle-scroller {
height: 100%;
}
.app-list .vue-recycle-scroller__viewport {
overflow-y: auto !important; /* 启用垂直滚动 */
overflow-x: hidden !important;
padding-top: 5px;
padding-left: 4px;
padding-right: 8px; /* 为滚动条留出空间 */
}
.app-list .vue-recycle-scroller__item-wrapper {
padding-bottom: 12px; /* 项目间间距 - 增加 */
}
/* 自定义滚动条样式 - 适配虚拟滚动 */
.app-list .vue-recycle-scroller__viewport::-webkit-scrollbar {
width: 6px; width: 6px;
} }
.app-list::-webkit-scrollbar-track { .app-list .vue-recycle-scroller__viewport::-webkit-scrollbar-track {
background: transparent; background: transparent;
} }
.app-list::-webkit-scrollbar-thumb { .app-list .vue-recycle-scroller__viewport::-webkit-scrollbar-thumb {
background: var(--border-primary); background: var(--border-primary);
border-radius: 3px; border-radius: 3px;
transition: background 0.2s; transition: background 0.2s;
} }
.app-list::-webkit-scrollbar-thumb:hover { .app-list .vue-recycle-scroller__viewport::-webkit-scrollbar-thumb:hover {
background: var(--text-quaternary); background: var(--text-quaternary);
} }
/* Firefox 滚动条 */ /* Firefox 滚动条 - 适配虚拟滚动 */
.app-list { .app-list .vue-recycle-scroller__viewport {
scrollbar-width: thin; scrollbar-width: thin;
scrollbar-color: var(--border-primary) transparent; scrollbar-color: var(--border-primary) transparent;
} }
@@ -545,6 +660,7 @@ const handleMouseLeave = () => {
border: 1px solid var(--border-primary); border: 1px solid var(--border-primary);
border-radius: 12px; border-radius: 12px;
padding: 16px; padding: 16px;
margin: 4px;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 16px; gap: 16px;
@@ -553,6 +669,11 @@ const handleMouseLeave = () => {
position: relative; position: relative;
overflow: hidden; overflow: hidden;
flex-shrink: 0; /* 防止压缩 */ flex-shrink: 0; /* 防止压缩 */
/* 修复模糊问题 */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
transform-style: preserve-3d;
backface-visibility: hidden;
} }
.app-item:hover { .app-item:hover {
@@ -568,7 +689,7 @@ const handleMouseLeave = () => {
box-shadow: box-shadow:
0 0 0 4px var(--monitor-accent-primary-alpha), 0 0 0 4px var(--monitor-accent-primary-alpha),
0 8px 25px -5px rgba(59, 130, 246, 0.3); 0 8px 25px -5px rgba(59, 130, 246, 0.3);
transform: translateY(-1px) scale(1.01); transform: translateY(-1px);
position: relative; position: relative;
z-index: 5; z-index: 5;
} }
@@ -941,11 +1062,11 @@ const handleMouseLeave = () => {
border-left-color: rgba(255, 255, 255, 0.08); border-left-color: rgba(255, 255, 255, 0.08);
} }
.app-list::-webkit-scrollbar-thumb { .app-list .vue-recycle-scroller__viewport::-webkit-scrollbar-thumb {
background: var(--border-primary); background: var(--border-primary);
} }
.app-list::-webkit-scrollbar-thumb:hover { .app-list .vue-recycle-scroller__viewport::-webkit-scrollbar-thumb:hover {
background: var(--text-quaternary); background: var(--text-quaternary);
} }
} }