perf: 优化文件浏览性能、扫描流程和前端 ref 冲突

- BrowseDirectoryAsync: 将分组/过滤/排序/分页从内存推到数据库,
    避免每次翻页加载全量文件实体(20万文件场景从 ~200MB 降到 KB 级)
  - ScanRootAsync: 使用 ExecuteUpdateAsync 原子抢占扫描锁防止并发扫描;
    缩略图/视频时长从扫描循环内同步生成改为扫描完成后 SemaphoreSlim(4) 批量生成,
    扫描耗时从数小时降到数十秒
  - ScanDueRootsAsync: 增加 LastScanStartedAt 判断,跳过正在执行中的扫描
  - ClientPage: 6 个重复的 ref="mediaPlayer" 改为 :ref="setMediaPlayer" 函数引用,
    修复 Vue 3 v-for 内字符串 ref 互相覆盖导致的播放器事件指向错误实例问题
This commit is contained in:
2026-06-12 15:45:06 +08:00
parent 8d9c7f17ff
commit 8c92d2fbac
2 changed files with 312 additions and 182 deletions
+128 -124
View File
@@ -36,7 +36,11 @@ const recentLoading = ref(false)
const viewMode = ref<'list' | 'grid'>('list')
const qrModal = ref<InstanceType<typeof QrCodeModal> | null>(null)
const mediaPlayer = ref<MediaPlayerHandle | MediaPlayerHandle[] | null>(null)
const mediaPlayer = ref<MediaPlayerHandle | null>(null)
function setMediaPlayer(el: unknown) {
mediaPlayer.value = (el as MediaPlayerHandle) ?? null
}
const searchQuery = ref('')
const searchResults = ref<FileRecordDto[]>([])
@@ -91,12 +95,11 @@ const clientTitle = computed(() => {
})
function getVideoElement() {
const player = Array.isArray(mediaPlayer.value) ? mediaPlayer.value[0] : mediaPlayer.value
return player?.getVideoElement() ?? null
return mediaPlayer.value?.getVideoElement() ?? null
}
function getMediaPlayer() {
return Array.isArray(mediaPlayer.value) ? mediaPlayer.value[0] : mediaPlayer.value
return mediaPlayer.value
}
function setError(error: unknown) {
@@ -461,51 +464,51 @@ onBeforeUnmount(() => {
<div v-else-if="viewMode === 'grid'" class="file-grid">
<template v-for="file in searchResults" :key="file.id">
<FileCard :file="file" show-created-time @select="selectSearchFile" />
<SelectedMediaPlayerHost
v-if="selectedFile?.id === file.id"
ref="mediaPlayer"
:selected-file="selectedFile"
:selected-root="selectedRoot"
:selected-media-url="selectedMediaUrl"
:selected-thumbnail-url="selectedThumbnailUrl"
:show-resume-prompt="showResumePrompt"
:resume-position="resumePosition"
:text-preview="textPreview"
@resume-playback="resumePlayback"
@dismiss-resume="dismissResume"
@loadedmetadata="seekToResumePosition"
@canplay="seekToResumePosition"
@play="handleVideoPlay"
@timeupdate="handleVideoTimeUpdate"
@pause="handleVideoPause"
@ended="handleVideoEnded"
@seeked="handleVideoPause"
/>
<SelectedMediaPlayerHost
v-if="selectedFile?.id === file.id"
:ref="setMediaPlayer"
:selected-file="selectedFile"
:selected-root="selectedRoot"
:selected-media-url="selectedMediaUrl"
:selected-thumbnail-url="selectedThumbnailUrl"
:show-resume-prompt="showResumePrompt"
:resume-position="resumePosition"
:text-preview="textPreview"
@resume-playback="resumePlayback"
@dismiss-resume="dismissResume"
@loadedmetadata="seekToResumePosition"
@canplay="seekToResumePosition"
@play="handleVideoPlay"
@timeupdate="handleVideoTimeUpdate"
@pause="handleVideoPause"
@ended="handleVideoEnded"
@seeked="handleVideoPause"
/>
</template>
</div>
<div v-else class="file-list">
<template v-for="file in searchResults" :key="file.id">
<FileListItem :file="file" show-created-time @select="selectSearchFile" />
<SelectedMediaPlayerHost
v-if="selectedFile?.id === file.id"
ref="mediaPlayer"
:selected-file="selectedFile"
:selected-root="selectedRoot"
:selected-media-url="selectedMediaUrl"
:selected-thumbnail-url="selectedThumbnailUrl"
:show-resume-prompt="showResumePrompt"
:resume-position="resumePosition"
:text-preview="textPreview"
@resume-playback="resumePlayback"
@dismiss-resume="dismissResume"
@loadedmetadata="seekToResumePosition"
@canplay="seekToResumePosition"
@play="handleVideoPlay"
@timeupdate="handleVideoTimeUpdate"
@pause="handleVideoPause"
@ended="handleVideoEnded"
@seeked="handleVideoPause"
/>
<SelectedMediaPlayerHost
v-if="selectedFile?.id === file.id"
:ref="setMediaPlayer"
:selected-file="selectedFile"
:selected-root="selectedRoot"
:selected-media-url="selectedMediaUrl"
:selected-thumbnail-url="selectedThumbnailUrl"
:show-resume-prompt="showResumePrompt"
:resume-position="resumePosition"
:text-preview="textPreview"
@resume-playback="resumePlayback"
@dismiss-resume="dismissResume"
@loadedmetadata="seekToResumePosition"
@canplay="seekToResumePosition"
@play="handleVideoPlay"
@timeupdate="handleVideoTimeUpdate"
@pause="handleVideoPause"
@ended="handleVideoEnded"
@seeked="handleVideoPause"
/>
</template>
</div>
<MobilePager
@@ -529,26 +532,26 @@ onBeforeUnmount(() => {
<div v-else-if="viewMode === 'grid'" class="file-grid">
<template v-for="file in recentFiles" :key="file.id">
<FileCard :file="file" :selected="selectedFile?.id === file.id" @select="selectFile" />
<SelectedMediaPlayerHost
v-if="selectedFile?.id === file.id"
ref="mediaPlayer"
:selected-file="selectedFile"
:selected-root="selectedRoot"
:selected-media-url="selectedMediaUrl"
:selected-thumbnail-url="selectedThumbnailUrl"
:show-resume-prompt="showResumePrompt"
:resume-position="resumePosition"
:text-preview="textPreview"
@resume-playback="resumePlayback"
@dismiss-resume="dismissResume"
@loadedmetadata="seekToResumePosition"
@canplay="seekToResumePosition"
@play="handleVideoPlay"
@timeupdate="handleVideoTimeUpdate"
@pause="handleVideoPause"
@ended="handleVideoEnded"
@seeked="handleVideoPause"
/>
<SelectedMediaPlayerHost
v-if="selectedFile?.id === file.id"
:ref="setMediaPlayer"
:selected-file="selectedFile"
:selected-root="selectedRoot"
:selected-media-url="selectedMediaUrl"
:selected-thumbnail-url="selectedThumbnailUrl"
:show-resume-prompt="showResumePrompt"
:resume-position="resumePosition"
:text-preview="textPreview"
@resume-playback="resumePlayback"
@dismiss-resume="dismissResume"
@loadedmetadata="seekToResumePosition"
@canplay="seekToResumePosition"
@play="handleVideoPlay"
@timeupdate="handleVideoTimeUpdate"
@pause="handleVideoPause"
@ended="handleVideoEnded"
@seeked="handleVideoPause"
/>
</template>
</div>
<div v-else class="file-list">
@@ -559,26 +562,26 @@ onBeforeUnmount(() => {
:show-last-played="activeTab === 'recent-played'"
@select="selectFile"
/>
<SelectedMediaPlayerHost
v-if="selectedFile?.id === file.id"
ref="mediaPlayer"
:selected-file="selectedFile"
:selected-root="selectedRoot"
:selected-media-url="selectedMediaUrl"
:selected-thumbnail-url="selectedThumbnailUrl"
:show-resume-prompt="showResumePrompt"
:resume-position="resumePosition"
:text-preview="textPreview"
@resume-playback="resumePlayback"
@dismiss-resume="dismissResume"
@loadedmetadata="seekToResumePosition"
@canplay="seekToResumePosition"
@play="handleVideoPlay"
@timeupdate="handleVideoTimeUpdate"
@pause="handleVideoPause"
@ended="handleVideoEnded"
@seeked="handleVideoPause"
/>
<SelectedMediaPlayerHost
v-if="selectedFile?.id === file.id"
:ref="setMediaPlayer"
:selected-file="selectedFile"
:selected-root="selectedRoot"
:selected-media-url="selectedMediaUrl"
:selected-thumbnail-url="selectedThumbnailUrl"
:show-resume-prompt="showResumePrompt"
:resume-position="resumePosition"
:text-preview="textPreview"
@resume-playback="resumePlayback"
@dismiss-resume="dismissResume"
@loadedmetadata="seekToResumePosition"
@canplay="seekToResumePosition"
@play="handleVideoPlay"
@timeupdate="handleVideoTimeUpdate"
@pause="handleVideoPause"
@ended="handleVideoEnded"
@seeked="handleVideoPause"
/>
</template>
</div>
</section>
@@ -628,26 +631,26 @@ onBeforeUnmount(() => {
show-created-time
@select="selectFile"
/>
<SelectedMediaPlayerHost
v-if="selectedFile?.id === file.id"
ref="mediaPlayer"
:selected-file="selectedFile"
:selected-root="selectedRoot"
:selected-media-url="selectedMediaUrl"
:selected-thumbnail-url="selectedThumbnailUrl"
:show-resume-prompt="showResumePrompt"
:resume-position="resumePosition"
:text-preview="textPreview"
@resume-playback="resumePlayback"
@dismiss-resume="dismissResume"
@loadedmetadata="seekToResumePosition"
@canplay="seekToResumePosition"
@play="handleVideoPlay"
@timeupdate="handleVideoTimeUpdate"
@pause="handleVideoPause"
@ended="handleVideoEnded"
@seeked="handleVideoPause"
/>
<SelectedMediaPlayerHost
v-if="selectedFile?.id === file.id"
:ref="setMediaPlayer"
:selected-file="selectedFile"
:selected-root="selectedRoot"
:selected-media-url="selectedMediaUrl"
:selected-thumbnail-url="selectedThumbnailUrl"
:show-resume-prompt="showResumePrompt"
:resume-position="resumePosition"
:text-preview="textPreview"
@resume-playback="resumePlayback"
@dismiss-resume="dismissResume"
@loadedmetadata="seekToResumePosition"
@canplay="seekToResumePosition"
@play="handleVideoPlay"
@timeupdate="handleVideoTimeUpdate"
@pause="handleVideoPause"
@ended="handleVideoEnded"
@seeked="handleVideoPause"
/>
</template>
</div>
@@ -659,26 +662,26 @@ onBeforeUnmount(() => {
show-created-time
@select="selectFile"
/>
<SelectedMediaPlayerHost
v-if="selectedFile?.id === file.id"
ref="mediaPlayer"
:selected-file="selectedFile"
:selected-root="selectedRoot"
:selected-media-url="selectedMediaUrl"
:selected-thumbnail-url="selectedThumbnailUrl"
:show-resume-prompt="showResumePrompt"
:resume-position="resumePosition"
:text-preview="textPreview"
@resume-playback="resumePlayback"
@dismiss-resume="dismissResume"
@loadedmetadata="seekToResumePosition"
@canplay="seekToResumePosition"
@play="handleVideoPlay"
@timeupdate="handleVideoTimeUpdate"
@pause="handleVideoPause"
@ended="handleVideoEnded"
@seeked="handleVideoPause"
/>
<SelectedMediaPlayerHost
v-if="selectedFile?.id === file.id"
:ref="setMediaPlayer"
:selected-file="selectedFile"
:selected-root="selectedRoot"
:selected-media-url="selectedMediaUrl"
:selected-thumbnail-url="selectedThumbnailUrl"
:show-resume-prompt="showResumePrompt"
:resume-position="resumePosition"
:text-preview="textPreview"
@resume-playback="resumePlayback"
@dismiss-resume="dismissResume"
@loadedmetadata="seekToResumePosition"
@canplay="seekToResumePosition"
@play="handleVideoPlay"
@timeupdate="handleVideoTimeUpdate"
@pause="handleVideoPause"
@ended="handleVideoEnded"
@seeked="handleVideoPause"
/>
</template>
</div>
@@ -698,6 +701,7 @@ onBeforeUnmount(() => {
<p v-else-if="!loading" class="empty-state">加载中...</p>
</template>
</main>
<QrCodeModal ref="qrModal" />