mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-06-30 12:01:37 +08:00
refactor: optimize performance by centralizing state calculations and stabilizing virtual list data refs
This commit is contained in:
@@ -8,6 +8,16 @@ import { useI18n } from 'vue-i18n'
|
||||
import { useBackgroundOptimization } from '@/composables/useBackgroundOptimization'
|
||||
import CryptoJS from 'crypto-js'
|
||||
|
||||
type TransferTask = TransferQueue['tasks'][number]
|
||||
|
||||
interface MediaTaskGroup {
|
||||
media: TransferQueue['media']
|
||||
titleYear: string
|
||||
tasks: TransferTask[]
|
||||
total: number
|
||||
completed: number
|
||||
}
|
||||
|
||||
// 多语言支持
|
||||
const { t } = useI18n()
|
||||
const { useProgressSSE } = useBackgroundOptimization()
|
||||
@@ -29,9 +39,6 @@ const overallProgress = ref({
|
||||
// 文件进度映射
|
||||
const fileProgressMap = ref<Map<string, { enable: boolean; value: number }>>(new Map())
|
||||
|
||||
// 数据可刷新标志
|
||||
const refreshFlag = ref(false)
|
||||
|
||||
// 进度是否激活
|
||||
const progressActive = ref(false)
|
||||
|
||||
@@ -58,49 +65,58 @@ function getStateColor(state: string) {
|
||||
else return 'error'
|
||||
}
|
||||
|
||||
// 从dataList中提取所有的媒体信息,合并相同title_year的记录
|
||||
const mediaList = computed(() => {
|
||||
const mediaMap = new Map<string, any>()
|
||||
// 按媒体聚合队列,避免模板中按 tab 重复扫描 dataList
|
||||
const mediaTaskGroups = computed<MediaTaskGroup[]>(() => {
|
||||
const groupMap = new Map<string, MediaTaskGroup>()
|
||||
|
||||
dataList.value.forEach(item => {
|
||||
const titleYear = item.media.title_year || ''
|
||||
if (!mediaMap.has(titleYear)) {
|
||||
mediaMap.set(titleYear, item.media)
|
||||
let group = groupMap.get(titleYear)
|
||||
|
||||
if (!group) {
|
||||
group = {
|
||||
media: item.media,
|
||||
titleYear,
|
||||
tasks: [],
|
||||
total: 0,
|
||||
completed: 0,
|
||||
}
|
||||
groupMap.set(titleYear, group)
|
||||
}
|
||||
|
||||
group.tasks.push(...item.tasks)
|
||||
group.total += item.tasks.length
|
||||
group.completed += item.tasks.filter(task => task.state === 'completed').length
|
||||
})
|
||||
|
||||
return Array.from(mediaMap.values())
|
||||
return Array.from(groupMap.values())
|
||||
})
|
||||
|
||||
// 从dataList中提取所有的媒体信息,合并相同title_year的记录
|
||||
const mediaList = computed(() => {
|
||||
return mediaTaskGroups.value.map(group => group.media)
|
||||
})
|
||||
|
||||
// 按media计算总数和完成数,返回 x/x
|
||||
function getMediaCount(title_year: string) {
|
||||
// 按title_year查询出所有media列表
|
||||
const medias = dataList.value.filter(item => item.media.title_year === title_year)
|
||||
// 计算media下任务的总数
|
||||
const total = medias.reduce((acc, cur) => acc + cur.tasks.length, 0)
|
||||
// 计算media下任务的完成数
|
||||
const completed = medias.reduce((acc, cur) => acc + cur.tasks.filter(task => task.state === 'completed').length, 0)
|
||||
return `${completed} / ${total}`
|
||||
const group = mediaTaskGroups.value.find(item => item.titleYear === title_year)
|
||||
return `${group?.completed ?? 0} / ${group?.total ?? 0}`
|
||||
}
|
||||
|
||||
// 根据媒体信息获取对应的整理任务,合并相同title_year的所有任务
|
||||
const activeTasks = computed(() => {
|
||||
const tasks = dataList.value.filter(item => item.media.title_year === activeTab.value).flatMap(item => item.tasks)
|
||||
return tasks
|
||||
return mediaTaskGroups.value.find(item => item.titleYear === activeTab.value)?.tasks ?? []
|
||||
})
|
||||
|
||||
// 根据媒体title_year获取对应的任务列表
|
||||
function getTasksByMedia(title_year: string) {
|
||||
return dataList.value.filter(item => item.media.title_year === title_year).flatMap(item => item.tasks)
|
||||
return mediaTaskGroups.value.find(item => item.titleYear === title_year)?.tasks ?? []
|
||||
}
|
||||
|
||||
// 计算整体进度
|
||||
const overallProgressComputed = computed(() => {
|
||||
if (dataList.value.length === 0) return 0
|
||||
|
||||
const allTasks = dataList.value.flatMap(item => item.tasks)
|
||||
const totalTasks = allTasks.length
|
||||
const completedTasks = allTasks.filter(task => task.state === 'completed').length
|
||||
const totalTasks = mediaTaskGroups.value.reduce((total, group) => total + group.total, 0)
|
||||
const completedTasks = mediaTaskGroups.value.reduce((total, group) => total + group.completed, 0)
|
||||
|
||||
return totalTasks > 0 ? (completedTasks / totalTasks) * 100 : 0
|
||||
})
|
||||
|
||||
@@ -129,33 +129,36 @@ function globToRegex(pattern: string, flags: string = ''): RegExp {
|
||||
}
|
||||
|
||||
// 通用过滤
|
||||
const getFilteredItems = (type: 'dir' | 'file') => {
|
||||
const filteredItems = computed(() => {
|
||||
const filterValue = filter.value
|
||||
if (!filterValue) {
|
||||
return items.value.filter(item => item.type === type)
|
||||
return items.value
|
||||
}
|
||||
|
||||
// 通配符模式
|
||||
if (filterValue.includes('*') || filterValue.includes('?')) {
|
||||
const flags = ignoreCase.value ? 'i' : ''
|
||||
const regex = globToRegex(filterValue, flags)
|
||||
return items.value.filter(item => item.type === type && regex.test(item.name ?? ''))
|
||||
return items.value.filter(item => regex.test(item.name ?? ''))
|
||||
}
|
||||
|
||||
// 子字符串模式
|
||||
if (ignoreCase.value) {
|
||||
const lowerCaseFilter = filterValue.toLowerCase()
|
||||
return items.value.filter(item => item.type === type && (item.name ?? '').toLowerCase().includes(lowerCaseFilter))
|
||||
return items.value.filter(item => (item.name ?? '').toLowerCase().includes(lowerCaseFilter))
|
||||
} else {
|
||||
return items.value.filter(item => item.type === type && (item.name ?? '').includes(filterValue))
|
||||
return items.value.filter(item => (item.name ?? '').includes(filterValue))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 目录过滤
|
||||
const dirs = computed(() => getFilteredItems('dir'))
|
||||
const dirs = computed(() => filteredItems.value.filter(item => item.type === 'dir'))
|
||||
|
||||
// 文件过滤
|
||||
const files = computed(() => getFilteredItems('file'))
|
||||
const files = computed(() => filteredItems.value.filter(item => item.type === 'file'))
|
||||
|
||||
// 虚拟列表数据,保持引用稳定,避免模板内联展开数组导致虚拟列表重算。
|
||||
const displayItems = computed(() => [...dirs.value, ...files.value])
|
||||
// 是否文件
|
||||
const isFile = computed(() => inProps.item.type == 'file')
|
||||
|
||||
@@ -716,7 +719,7 @@ onUnmounted(() => {
|
||||
class="text-high-emphasis file-list-container"
|
||||
:style="{ height: `${listAvailableHeight}px`, maxHeight: `${listAvailableHeight}px` }"
|
||||
>
|
||||
<VVirtualScroll :items="[...dirs, ...files]" style="block-size: 100%">
|
||||
<VVirtualScroll :items="displayItems" style="block-size: 100%">
|
||||
<template #default="{ item }">
|
||||
<VHover>
|
||||
<template #default="hover">
|
||||
|
||||
@@ -14,6 +14,11 @@ const display = useDisplay()
|
||||
|
||||
const { appMode } = usePWA()
|
||||
|
||||
type TreeRow =
|
||||
| { type: 'root'; key: string; level: number }
|
||||
| { type: 'loading'; key: string; path: string; level: number }
|
||||
| { type: 'directory'; key: string; dir: FileItem; level: number }
|
||||
|
||||
// 计算列表可用高度
|
||||
// componentOffset = FileToolbar(48) = 48
|
||||
const { availableHeight } = useAvailableHeight(48, 300)
|
||||
@@ -132,37 +137,6 @@ async function loadRootDirectories() {
|
||||
await loadSubdirectories('/')
|
||||
}
|
||||
|
||||
// 检索所有目录节点
|
||||
function getAllDirectories() {
|
||||
const allDirs: { dir: FileItem; level: number; parentPath: string }[] = []
|
||||
|
||||
// 添加根目录的子目录
|
||||
if (treeCache.value['/']) {
|
||||
treeCache.value['/'].forEach(dir => {
|
||||
allDirs.push({ dir, level: 0, parentPath: '/' })
|
||||
addSubdirectories(dir.path || '', 1, allDirs)
|
||||
})
|
||||
}
|
||||
|
||||
return allDirs
|
||||
}
|
||||
|
||||
// 递归添加子目录
|
||||
function addSubdirectories(
|
||||
parentPath: string,
|
||||
level: number,
|
||||
result: { dir: FileItem; level: number; parentPath: string }[],
|
||||
) {
|
||||
if (treeCache.value[parentPath]) {
|
||||
treeCache.value[parentPath].forEach(dir => {
|
||||
result.push({ dir, level, parentPath })
|
||||
if (isFolderExpanded(dir.path || '')) {
|
||||
addSubdirectories(dir.path || '', level + 1, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 监听当前路径变化,自动展开当前路径
|
||||
watch(
|
||||
() => props.currentPath,
|
||||
@@ -224,38 +198,51 @@ const rootDirectories = computed(() => {
|
||||
return treeCache.value['/'] || []
|
||||
})
|
||||
|
||||
// 扁平化的目录树
|
||||
const flattenedDirectories = computed(() => {
|
||||
return getAllDirectories()
|
||||
})
|
||||
// 只生成当前可见的目录行,避免折叠/隐藏节点继续留在 DOM 中
|
||||
const visibleTreeRows = computed<TreeRow[]>(() => {
|
||||
const rows: TreeRow[] = [{ type: 'root', key: 'root', level: 0 }]
|
||||
|
||||
// 检查路径是否为指定目录的子目录或后代
|
||||
function isChildOrDescendant(path: string, ancestorPath: string) {
|
||||
if (!path || !ancestorPath) return false
|
||||
if (ancestorPath === '/') return true
|
||||
|
||||
// 确保路径以斜杠结尾,便于比较
|
||||
const normalizedPath = path.endsWith('/') ? path : path + '/'
|
||||
const normalizedAncestorPath = ancestorPath.endsWith('/') ? ancestorPath : ancestorPath + '/'
|
||||
|
||||
// 检查路径是否以祖先路径开头,但不是祖先路径本身
|
||||
return normalizedPath.startsWith(normalizedAncestorPath) && normalizedPath !== normalizedAncestorPath
|
||||
}
|
||||
|
||||
// 计算目录相对于其祖先的缩进级别
|
||||
function getIndentLevel(path: string, ancestorPath: string) {
|
||||
if (!path || !ancestorPath) return 0
|
||||
|
||||
// 根目录特殊处理
|
||||
if (ancestorPath === '/') {
|
||||
return path.split('/').filter(p => p).length - 1
|
||||
if (loading.value['/']) {
|
||||
rows.push({ type: 'loading', key: 'loading:/', path: '/', level: 0 })
|
||||
return rows
|
||||
}
|
||||
|
||||
// 计算路径中斜杠的数量差异
|
||||
const pathParts = path.split('/').filter(p => p).length
|
||||
const ancestorParts = ancestorPath.split('/').filter(p => p).length
|
||||
rootDirectories.value.forEach(dir => addVisibleDirectoryRows(dir, 0, rows))
|
||||
|
||||
return pathParts - ancestorParts
|
||||
return rows
|
||||
})
|
||||
|
||||
function addVisibleDirectoryRows(dir: FileItem, level: number, rows: TreeRow[]) {
|
||||
const path = dir.path || ''
|
||||
|
||||
rows.push({
|
||||
type: 'directory',
|
||||
key: path || `${level}:${dir.name}`,
|
||||
dir,
|
||||
level,
|
||||
})
|
||||
|
||||
if (!path || !isFolderExpanded(path)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (loading.value[path]) {
|
||||
rows.push({
|
||||
type: 'loading',
|
||||
key: `loading:${path}`,
|
||||
path,
|
||||
level: level + 1,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
treeCache.value[path]?.forEach(child => addVisibleDirectoryRows(child, level + 1, rows))
|
||||
}
|
||||
|
||||
function getTreeRowStyle(level: number) {
|
||||
return {
|
||||
paddingInlineStart: level > 0 ? `${16 + level * 12}px` : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载时初始加载
|
||||
@@ -267,117 +254,75 @@ onMounted(async () => {
|
||||
|
||||
<template>
|
||||
<VCard class="file-navigator rounded-e-0 rounded-t-0" v-if="!isMobile" :height="`${availableHeight}px`">
|
||||
<div class="tree-container">
|
||||
<!-- 根目录项 -->
|
||||
<div
|
||||
class="tree-item root-item"
|
||||
:class="{ 'active': currentPath === '/' }"
|
||||
@click="
|
||||
handleFolderClick({
|
||||
storage: storage,
|
||||
type: 'dir',
|
||||
name: '/',
|
||||
path: '/',
|
||||
})
|
||||
"
|
||||
>
|
||||
<div class="folder-content">
|
||||
<VIcon icon="mdi-home" class="me-2" color="primary" />
|
||||
<span>{{ t('file.rootDirectory') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 加载根目录 -->
|
||||
<div v-if="loading['/']" class="tree-loading">
|
||||
<VProgressCircular indeterminate size="24" color="primary" class="ma-2" />
|
||||
<span>{{ t('file.loadingDirectoryStructure') }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 目录树结构 -->
|
||||
<template v-else>
|
||||
<!-- 一级目录(根目录下的目录) -->
|
||||
<div v-for="directory in rootDirectories" :key="directory.path" class="tree-item-container">
|
||||
<!-- 目录项 -->
|
||||
<div class="tree-item" :class="{ 'active': currentPath === directory.path }">
|
||||
<div class="folder-toggle" @click.stop="toggleFolder(directory.path || '')">
|
||||
<VProgressCircular
|
||||
v-if="loading[directory.path || '']"
|
||||
indeterminate
|
||||
size="14"
|
||||
width="2"
|
||||
color="primary"
|
||||
/>
|
||||
<VIcon
|
||||
v-else
|
||||
size="small"
|
||||
:icon="isFolderExpanded(directory.path || '') ? 'mdi-chevron-down' : 'mdi-chevron-right'"
|
||||
/>
|
||||
</div>
|
||||
<div class="folder-content" @click.stop="handleFolderClick(directory)">
|
||||
<VIcon
|
||||
size="small"
|
||||
:icon="renderFolderIcon(isFolderExpanded(directory.path || ''))"
|
||||
:color="currentPath === directory.path ? 'primary' : 'amber-darken-1'"
|
||||
class="me-1"
|
||||
/>
|
||||
<span class="folder-name">
|
||||
{{ directory.name }}
|
||||
</span>
|
||||
</div>
|
||||
<VVirtualScroll :items="visibleTreeRows" :item-height="32" class="tree-container">
|
||||
<template #default="{ item }">
|
||||
<div
|
||||
v-if="item.type === 'root'"
|
||||
:key="item.key"
|
||||
class="tree-item root-item"
|
||||
:class="{ 'active': currentPath === '/' }"
|
||||
@click="
|
||||
handleFolderClick({
|
||||
storage: storage,
|
||||
type: 'dir',
|
||||
name: '/',
|
||||
path: '/',
|
||||
})
|
||||
"
|
||||
>
|
||||
<div class="folder-content">
|
||||
<VIcon icon="mdi-home" class="me-2" color="primary" />
|
||||
<span>{{ t('file.rootDirectory') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 子目录容器 - 如果该目录被展开,显示其所有子目录 -->
|
||||
<div v-if="isFolderExpanded(directory.path || '')">
|
||||
<!-- 加载中状态 -->
|
||||
<div v-if="loading[directory.path || '']" class="tree-loading pl-8">
|
||||
<VProgressCircular indeterminate size="14" color="primary" class="ma-2" />
|
||||
<span class="text-caption">{{ t('common.loading') }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="item.type === 'loading'"
|
||||
:key="item.key"
|
||||
class="tree-loading"
|
||||
:style="getTreeRowStyle(item.level)"
|
||||
>
|
||||
<VProgressCircular indeterminate size="14" color="primary" class="ma-2" />
|
||||
<span class="text-caption">
|
||||
{{ item.path === '/' ? t('file.loadingDirectoryStructure') : t('common.loading') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 所有层级的子目录列表 -->
|
||||
<div v-else>
|
||||
<!-- 遍历所有扁平化的目录列表,查找对应层级的目录 -->
|
||||
<div
|
||||
v-for="item in flattenedDirectories"
|
||||
:key="item.dir.path"
|
||||
v-show="isChildOrDescendant(item.dir.path || '', directory.path || '')"
|
||||
class="tree-item"
|
||||
:class="{ 'active': currentPath === item.dir.path }"
|
||||
:style="{ paddingLeft: 16 + getIndentLevel(item.dir.path || '', directory.path || '') * 12 + 'px' }"
|
||||
>
|
||||
<!-- 展开/折叠按钮 -->
|
||||
<div class="folder-toggle" @click.stop="toggleFolder(item.dir.path || '')">
|
||||
<VProgressCircular
|
||||
v-if="loading[item.dir.path || '']"
|
||||
indeterminate
|
||||
size="14"
|
||||
width="2"
|
||||
color="primary"
|
||||
/>
|
||||
<VIcon
|
||||
v-else
|
||||
size="small"
|
||||
:icon="isFolderExpanded(item.dir.path || '') ? 'mdi-chevron-down' : 'mdi-chevron-right'"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 文件夹图标和名称 -->
|
||||
<div class="folder-content" @click.stop="handleFolderClick(item.dir)">
|
||||
<VIcon
|
||||
size="small"
|
||||
:icon="renderFolderIcon(isFolderExpanded(item.dir.path || ''))"
|
||||
:color="currentPath === item.dir.path ? 'primary' : 'amber-darken-1'"
|
||||
class="me-1"
|
||||
/>
|
||||
<span class="folder-name">
|
||||
{{ item.dir.name }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
:key="item.key"
|
||||
class="tree-item"
|
||||
:class="{ 'active': currentPath === item.dir.path }"
|
||||
:style="getTreeRowStyle(item.level)"
|
||||
>
|
||||
<div class="folder-toggle" @click.stop="toggleFolder(item.dir.path || '')">
|
||||
<VProgressCircular
|
||||
v-if="loading[item.dir.path || '']"
|
||||
indeterminate
|
||||
size="14"
|
||||
width="2"
|
||||
color="primary"
|
||||
/>
|
||||
<VIcon
|
||||
v-else
|
||||
size="small"
|
||||
:icon="isFolderExpanded(item.dir.path || '') ? 'mdi-chevron-down' : 'mdi-chevron-right'"
|
||||
/>
|
||||
</div>
|
||||
<div class="folder-content" @click.stop="handleFolderClick(item.dir)">
|
||||
<VIcon
|
||||
size="small"
|
||||
:icon="renderFolderIcon(isFolderExpanded(item.dir.path || ''))"
|
||||
:color="currentPath === item.dir.path ? 'primary' : 'amber-darken-1'"
|
||||
class="me-1"
|
||||
/>
|
||||
<span class="folder-name">
|
||||
{{ item.dir.name }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</VVirtualScroll>
|
||||
</VCard>
|
||||
</template>
|
||||
|
||||
@@ -402,8 +347,8 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
.tree-container {
|
||||
overflow: hidden auto;
|
||||
flex: 1;
|
||||
min-block-size: 0;
|
||||
}
|
||||
|
||||
.tree-item-container {
|
||||
|
||||
@@ -39,6 +39,8 @@ interface SearchParams {
|
||||
sites: string
|
||||
}
|
||||
|
||||
const resourceSearchParamsStorageKey = 'MP_ResourceSearchParams'
|
||||
|
||||
function createSearchParams(query: LocationQuery): SearchParams {
|
||||
return {
|
||||
keyword: query?.keyword?.toString() ?? '',
|
||||
@@ -51,15 +53,62 @@ function createSearchParams(query: LocationQuery): SearchParams {
|
||||
}
|
||||
}
|
||||
|
||||
function getSearchParamsKey(params: SearchParams): string {
|
||||
return JSON.stringify(params)
|
||||
function normalizeSearchParams(params?: Partial<SearchParams> | null): SearchParams {
|
||||
return {
|
||||
keyword: params?.keyword?.toString() ?? '',
|
||||
type: params?.type?.toString() ?? '',
|
||||
area: params?.area?.toString() ?? '',
|
||||
title: params?.title?.toString() ?? '',
|
||||
year: params?.year?.toString() ?? '',
|
||||
season: params?.season?.toString() ?? '',
|
||||
sites: params?.sites?.toString() ?? '',
|
||||
}
|
||||
}
|
||||
|
||||
function hasSearchKeyword(params: SearchParams): boolean {
|
||||
return params.keyword.trim().length > 0
|
||||
}
|
||||
|
||||
function createSearchRequestToken(): string {
|
||||
return `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
||||
}
|
||||
|
||||
const activeSearchParams = ref<SearchParams>(createSearchParams(route.query))
|
||||
function loadStoredSearchParams(): SearchParams | null {
|
||||
try {
|
||||
const rawParams = localStorage.getItem(resourceSearchParamsStorageKey)
|
||||
if (!rawParams) return null
|
||||
|
||||
const params = normalizeSearchParams(JSON.parse(rawParams) as Partial<SearchParams>)
|
||||
return hasSearchKeyword(params) ? params : null
|
||||
} catch (error) {
|
||||
console.warn('读取资源搜索参数失败:', error)
|
||||
localStorage.removeItem(resourceSearchParamsStorageKey)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function saveStoredSearchParams(params: SearchParams) {
|
||||
if (!hasSearchKeyword(params)) return
|
||||
localStorage.setItem(resourceSearchParamsStorageKey, JSON.stringify(params))
|
||||
}
|
||||
|
||||
const initialSearchParams = createSearchParams(route.query)
|
||||
const activeSearchParams = ref<SearchParams>(initialSearchParams)
|
||||
const lastSearchParams = ref<SearchParams | null>(
|
||||
hasSearchKeyword(initialSearchParams) ? { ...initialSearchParams } : loadStoredSearchParams(),
|
||||
)
|
||||
|
||||
function rememberSearchParams(params: SearchParams) {
|
||||
if (!hasSearchKeyword(params)) return
|
||||
|
||||
const nextParams = { ...params }
|
||||
lastSearchParams.value = nextParams
|
||||
saveStoredSearchParams(nextParams)
|
||||
}
|
||||
|
||||
if (hasSearchKeyword(initialSearchParams)) {
|
||||
rememberSearchParams(initialSearchParams)
|
||||
}
|
||||
|
||||
// 查询TMDBID或标题
|
||||
const keyword = computed(() => activeSearchParams.value.keyword)
|
||||
@@ -552,13 +601,17 @@ function changeViewType(newType: string) {
|
||||
}
|
||||
|
||||
// 获取搜索列表数据
|
||||
async function fetchData(options: { force?: boolean } = {}) {
|
||||
const currentSearchParams = { ...activeSearchParams.value }
|
||||
async function fetchData(options: { force?: boolean; params?: SearchParams } = {}) {
|
||||
const currentSearchParams = { ...(options.params ?? activeSearchParams.value) }
|
||||
if (hasSearchKeyword(currentSearchParams)) {
|
||||
activeSearchParams.value = { ...currentSearchParams }
|
||||
rememberSearchParams(currentSearchParams)
|
||||
}
|
||||
const requestToken = options.force || Boolean(currentSearchParams.keyword) ? createSearchRequestToken() : undefined
|
||||
|
||||
try {
|
||||
enableFilterAnimation.value = true
|
||||
if (!currentSearchParams.keyword) {
|
||||
if (!hasSearchKeyword(currentSearchParams)) {
|
||||
// 查询上次搜索结果
|
||||
const results = await api.get('search/last', {
|
||||
params: requestToken ? { _ts: requestToken } : undefined,
|
||||
@@ -593,11 +646,12 @@ async function fetchData(options: { force?: boolean } = {}) {
|
||||
// 重新搜索(使用相同参数重新触发搜索)
|
||||
async function refreshSearch() {
|
||||
if (isRefreshing.value || progressActive.value) return
|
||||
const refreshParams = lastSearchParams.value ?? activeSearchParams.value
|
||||
isRefreshing.value = true
|
||||
try {
|
||||
// 重新搜索时退出 AI 视图,其余状态由 fetchData 内部重置
|
||||
showingAiResults.value = false
|
||||
await fetchData({ force: true })
|
||||
await fetchData({ force: true, params: refreshParams })
|
||||
} catch (error) {
|
||||
console.error('重新搜索失败:', error)
|
||||
} finally {
|
||||
@@ -885,7 +939,7 @@ watch(
|
||||
if (Object.keys(query).length === 0) return
|
||||
|
||||
const nextSearchParams = createSearchParams(query)
|
||||
if (getSearchParamsKey(nextSearchParams) === getSearchParamsKey(activeSearchParams.value)) return
|
||||
if (!hasSearchKeyword(nextSearchParams)) return
|
||||
|
||||
activeSearchParams.value = nextSearchParams
|
||||
void fetchData()
|
||||
|
||||
@@ -180,9 +180,10 @@ const pageRange = [
|
||||
{ title: '100', value: 100 },
|
||||
{ title: '500', value: 500 },
|
||||
{ title: '1000', value: 1000 },
|
||||
{ title: 'All', value: -1 },
|
||||
]
|
||||
|
||||
const pageRangeValues = pageRange.map(item => item.value)
|
||||
|
||||
// 数据列表
|
||||
const dataList = ref<TransferHistory[]>([])
|
||||
|
||||
@@ -209,7 +210,7 @@ const groupBy = ref<any>([
|
||||
])
|
||||
|
||||
// 每页条数
|
||||
const itemsPerPage = ref<number>(ensureNumber(route.query.itemsPerPage, 50))
|
||||
const itemsPerPage = ref<number>(ensurePageSize(route.query.itemsPerPage, 50))
|
||||
|
||||
// 当前页码
|
||||
const currentPage = ref<number>(ensureNumber(route.query.currentPage, 1))
|
||||
@@ -270,7 +271,7 @@ const TransferDict: { [key: string]: string } = {
|
||||
// 分页提示
|
||||
const pageTip = computed(() => {
|
||||
const begin = itemsPerPage.value * (currentPage.value - 1) + 1
|
||||
const end = itemsPerPage.value * currentPage.value === -1 ? 'ALL' : itemsPerPage.value * currentPage.value
|
||||
const end = Math.min(itemsPerPage.value * currentPage.value, totalItems.value)
|
||||
return {
|
||||
begin,
|
||||
end,
|
||||
@@ -280,7 +281,7 @@ const pageTip = computed(() => {
|
||||
// 分页总数
|
||||
const totalPage = computed(() => {
|
||||
const total = Math.ceil(totalItems.value / itemsPerPage.value)
|
||||
return total
|
||||
return Math.max(1, total)
|
||||
})
|
||||
|
||||
// 切换页签
|
||||
@@ -663,6 +664,11 @@ function ensureNumber(value: any, defaultValue: number = 0) {
|
||||
return value
|
||||
}
|
||||
|
||||
function ensurePageSize(value: any, defaultValue: number = 50) {
|
||||
const pageSize = ensureNumber(value, defaultValue)
|
||||
return pageRangeValues.includes(pageSize) ? pageSize : defaultValue
|
||||
}
|
||||
|
||||
// 按标题分组后的选中数量统计,键为标题,值为对应分组的选中数
|
||||
const selectedCountsGroupedByTitle = computed(() => {
|
||||
return selected.value.reduce(
|
||||
|
||||
Reference in New Issue
Block a user