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:
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user