From 7d0550825bad03c88e740bbb44bf36bed00e3b4b Mon Sep 17 00:00:00 2001 From: jxxghp Date: Sun, 28 Jun 2026 10:56:33 +0800 Subject: [PATCH] fix: adjust header margins for consistency in resource and site card list views --- src/pages/resource.vue | 2 +- src/views/reorganize/TransferHistoryView.vue | 678 ++++++++++++++++++- src/views/site/SiteCardListView.vue | 2 +- src/views/user/UserListView.vue | 5 +- 4 files changed, 664 insertions(+), 23 deletions(-) diff --git a/src/pages/resource.vue b/src/pages/resource.vue index 3d86fa8c..33f6ac96 100644 --- a/src/pages/resource.vue +++ b/src/pages/resource.vue @@ -1353,7 +1353,7 @@ onUnmounted(() => { -
+
display.mdAndUp.value) +const isMobile = computed(() => display.smAndDown.value) // PWA模式检测 const { appMode } = usePWA() const { useProgressSSE } = useBackground() @@ -49,6 +51,7 @@ const canManage = computed(() => ) let syncingRouteQuery = false let fetchDataRequestSeed = 0 +let mobileFetchDataRequestSeed = 0 // 组合式输入法状态 const isComposing = ref(false) @@ -75,8 +78,10 @@ const redoTargetStorage = ref() // 已选中的数据 const selected = ref([]) +// 从季集字符串中提取可排序的数字。 const getNum = (s?: string) => (s ? parseInt(s.replace(/[^0-9]/g, ''), 10) || 0 : 0) +// 按媒体类型、标题和剧集序号排序历史记录。 function sortByTitle(a: TransferHistory, b: TransferHistory) { if (a.type !== b.type) { return (a.type ?? '').localeCompare(b.type ?? '') @@ -95,6 +100,7 @@ function sortByTitle(a: TransferHistory, b: TransferHistory) { return 0 } +// 按源文件体积排序历史记录。 function sortBySourceSize(a: TransferHistory, b: TransferHistory) { return (a.src_fileitem?.size ?? 0) - (b.src_fileitem?.size ?? 0) } @@ -194,6 +200,27 @@ const pageRangeValues = pageRange.map(item => item.value) // 数据列表 const dataList = ref([]) +// 移动端历史记录列表,独立于桌面分页表格。 +const mobileDataList = ref([]) + +// 移动端每次触底加载的记录数。 +const mobilePageSize = 25 + +// 移动端触底加载页码。 +const mobileCurrentPage = ref(1) + +// 移动端是否还有下一页。 +const mobileHasMore = ref(true) + +// 移动端无限滚动组件重置键。 +const mobileInfiniteKey = ref(0) + +// 移动端加载状态。 +const mobileLoading = ref(false) + +// 移动端已展开完整路径的记录 ID。 +const mobileExpandedPathIds = ref([]) + // 搜索 const search = ref(getRouteQueryString(route.query.search)) @@ -363,9 +390,14 @@ const debouncedReloadSearchPage = debounce(() => { void reloadPage(true) }, 1000) +// 延迟刷新移动端无限列表,输入完成后再从第一页重新加载。 +const debouncedReloadMobileSearchPage = debounce(() => { + resetMobileHistory() +}, 600) + // 切换页签 watch([() => currentPage.value, () => itemsPerPage.value], () => { - if (syncingRouteQuery) return + if (syncingRouteQuery || !isDesktop.value) return debouncedReloadPage() }) @@ -374,6 +406,11 @@ watch([() => currentPage.value, () => itemsPerPage.value], () => { watch([() => search.value, () => isComposing.value], () => { if (syncingRouteQuery || isComposing.value) return + if (isMobile.value) { + debouncedReloadMobileSearchPage() + return + } + debouncedReloadSearchPage() }) @@ -381,7 +418,7 @@ watch([() => search.value, () => isComposing.value], () => { watch( () => group.value, () => { - if (syncingRouteQuery) return + if (syncingRouteQuery || !isDesktop.value) return void reloadPage() }, @@ -391,11 +428,25 @@ watch( watch( () => route.query, () => { - void refreshDataFromRouteQuery() + if (isDesktop.value) { + void refreshDataFromRouteQuery() + } else { + syncMobileSearchFromRouteQuery() + resetMobileHistory() + } }, { deep: true }, ) +// 响应桌面与移动端断点切换,进入对应布局后刷新对应数据源。 +watch(isDesktop, desktop => { + if (desktop) { + void refreshDataFromRouteQuery() + } else { + resetMobileHistory() + } +}) + // 获取历史记录数据,keep-alive 重新进入时可静默刷新,避免表格出现重新加载感。 async function fetchData(page = currentPage.value, count = itemsPerPage.value, options: { silent?: boolean } = {}) { const requestSeed = ++fetchDataRequestSeed @@ -419,9 +470,7 @@ async function fetchData(page = currentPage.value, count = itemsPerPage.value, o isRefreshed.value = true dataList.value = list totalItems.value = ensureNumber(result.data?.total, 0) - searchHintList.value = ['失败', '成功', ...new Set(list.map((item: TransferHistory) => item.title || ''))].filter( - (title): title is string => title !== '', - ) + updateSearchHintList(list) return { list, @@ -437,6 +486,98 @@ async function fetchData(page = currentPage.value, count = itemsPerPage.value, o } } +// 更新搜索建议,移动端追加加载时会合并已加载记录的标题。 +function updateSearchHintList(list: TransferHistory[]) { + searchHintList.value = ['失败', '成功', ...new Set(list.map((item: TransferHistory) => item.title || ''))].filter( + (title): title is string => title !== '', + ) +} + +// 重置移动端无限列表,让 VInfiniteScroll 从第一页重新触发加载。 +function resetMobileHistory() { + mobileFetchDataRequestSeed++ + mobileDataList.value = [] + mobileCurrentPage.value = 1 + mobileHasMore.value = true + mobileLoading.value = false + isRefreshed.value = false + totalItems.value = 0 + mobileExpandedPathIds.value = [] + selected.value = [] + mobileInfiniteKey.value++ +} + +// 移动端只从路由同步搜索词,不接收桌面分页和分组状态。 +function syncMobileSearchFromRouteQuery() { + syncingRouteQuery = true + try { + search.value = getRouteQueryString(route.query.search) + } finally { + void nextTick(() => { + syncingRouteQuery = false + }) + } +} + +// 移动端触底加载历史记录,并将新页追加到虚拟列表数据源。 +async function loadMobileHistory({ done }: { done: (status: 'ok' | 'empty' | 'error') => void }) { + if (mobileLoading.value) { + done('ok') + return + } + + if (!mobileHasMore.value) { + done('empty') + return + } + + const requestSeed = ++mobileFetchDataRequestSeed + + try { + mobileLoading.value = true + const result: { [key: string]: any } = await api.get('history/transfer', { + params: { + page: mobileCurrentPage.value, + count: mobilePageSize, + title: search.value ?? '', + }, + }) + if (requestSeed !== mobileFetchDataRequestSeed) { + done('ok') + return + } + + const list: TransferHistory[] = Array.isArray(result.data?.list) ? result.data.list : [] + const total = ensureNumber(result.data?.total, 0) + + isRefreshed.value = true + totalItems.value = total + + if (list.length === 0) { + mobileHasMore.value = false + updateSearchHintList(mobileDataList.value) + done('empty') + return + } + + const existingIds = new Set(mobileDataList.value.map(item => item.id)) + const newItems = list.filter(item => !existingIds.has(item.id)) + + mobileDataList.value = [...mobileDataList.value, ...newItems] + mobileCurrentPage.value++ + mobileHasMore.value = mobileDataList.value.length < total && list.length >= mobilePageSize + updateSearchHintList(mobileDataList.value) + done(mobileHasMore.value ? 'ok' : 'empty') + } catch (error) { + console.error(error) + done('error') + } finally { + if (requestSeed === mobileFetchDataRequestSeed) { + mobileLoading.value = false + } + } +} + // 从路由查询参数中取出单值字符串,空搜索统一返回 null 以保持输入框 placeholder 可见。 function getRouteQueryString(value: unknown): string | null { if (Array.isArray(value)) { @@ -468,6 +609,11 @@ async function refreshDataFromRouteQuery(options: { silent?: boolean } = {}) { // 操作完成后刷新列表;如果当前页被删空,则跳回最后一个有效页。 async function refreshDataAfterOperation() { + if (isMobile.value) { + resetMobileHistory() + return + } + const result = await fetchData() if (!result) return @@ -825,11 +971,109 @@ 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 } +// 已选历史记录 ID 集合,供移动端卡片和分组选择状态复用。 +const selectedIdSet = computed(() => new Set(selected.value.map(item => item.id))) + +// 拼接移动端展示用的季集文本。 +function getHistoryEpisodeText(item: TransferHistory) { + return `${item.seasons || ''}${item.episodes || ''}` +} + +// 获取移动端卡片标题,剧集记录会追加季集信息。 +function getHistoryDisplayTitle(item: TransferHistory) { + const title = item.title || t('common.unknown') + const episodeText = getHistoryEpisodeText(item) + + return item.type === '电视剧' && episodeText ? `${title} ${episodeText}` : title +} + +// 获取移动端卡片副标题,优先展示二级分类和年份。 +function getHistorySubtitle(item: TransferHistory) { + return [item.category, item.year].filter(Boolean).join(' / ') +} + +// 获取存储展示名称,配置缺失时回退到原始存储标识。 +function getHistoryStorageName(storage?: string) { + if (!storage) return t('common.unknown') + + return storageDict.value[storage] || storage +} + +// 获取移动端卡片状态对应的主题色。 +function getHistoryStatusColor(item: TransferHistory) { + return item.status ? 'success' : 'error' +} + +// 获取移动端卡片状态文本。 +function getHistoryStatusText(item: TransferHistory) { + return item.status ? t('transferHistory.status.success') : t('transferHistory.status.failed') +} + +// 将历史记录时间压缩成移动端卡片展示文本。 +function getHistoryDateText(date?: string) { + if (!date) return '' + + const match = date.match(/^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2})/) + if (!match) return date + + const [, year, month, day, hour, minute] = match + const recordDate = new Date(Number(year), Number(month) - 1, Number(day)) + const today = new Date() + const todayDate = new Date(today.getFullYear(), today.getMonth(), today.getDate()) + const diffDays = Math.round((todayDate.getTime() - recordDate.getTime()) / 86400000) + const time = `${hour}:${minute}` + + if (diffDays === 0) return `今天 ${time}` + if (diffDays === 1) return `昨天 ${time}` + + return `${month}-${day} ${time}` +} + +// 判断移动端路径是否已展开。 +function isMobilePathExpanded(item: TransferHistory) { + return mobileExpandedPathIds.value.includes(item.id) +} + +// 切换移动端路径完整展示状态。 +function toggleMobilePathExpanded(item: TransferHistory) { + if (isMobilePathExpanded(item)) { + mobileExpandedPathIds.value = mobileExpandedPathIds.value.filter(id => id !== item.id) + return + } + + mobileExpandedPathIds.value = [...mobileExpandedPathIds.value, item.id] +} + +// 判断指定历史记录是否已被选中。 +function isHistorySelected(item: TransferHistory) { + return selectedIdSet.value.has(item.id) +} + +// 批量设置历史记录选中状态,并按 ID 去重。 +function updateHistorySelection(items: readonly TransferHistory[], checked: boolean | null) { + const itemIds = new Set(items.map(item => item.id)) + + if (checked) { + const selectedIds = new Set(selected.value.map(item => item.id)) + + selected.value = [...selected.value, ...items.filter(item => !selectedIds.has(item.id))] + return + } + + selected.value = selected.value.filter(item => !itemIds.has(item.id)) +} + +// 切换单条历史记录的选中状态。 +function toggleHistorySelection(item: TransferHistory, checked: boolean | null) { + updateHistorySelection([item], checked) +} + // 按标题分组后的选中数量统计,键为标题,值为对应分组的选中数 const selectedCountsGroupedByTitle = computed(() => { return selected.value.reduce( @@ -845,12 +1089,7 @@ const selectedCountsGroupedByTitle = computed(() => { // 控制分组内所有子项的选中状态 const toggleGroupSelection = (checked: boolean | null, items: readonly any[]) => { const values = items.map(item => item.value) - if (checked) { - selected.value = [...new Set([...selected.value, ...values])] - } else { - const itemsSet = new Set(values) - selected.value = selected.value.filter(item => !itemsSet.has(item)) - } + updateHistorySelection(values, checked) } const historyDynamicIcon = computed(() => (selected.value.length > 0 ? 'mdi-chevron-up' : 'mdi-timer-sand-paused')) @@ -910,7 +1149,11 @@ useDynamicButton({ // 初始加载数据 onMounted(() => { void loadStorages() - void refreshDataFromRouteQuery() + if (isDesktop.value) { + void refreshDataFromRouteQuery() + } else { + resetMobileHistory() + } }) onActivated(() => { @@ -919,14 +1162,17 @@ onActivated(() => { return } - if (!loading.value) { + if (isDesktop.value && !loading.value) { void refreshDataFromRouteQuery({ silent: true }) + } else if (isMobile.value && !mobileLoading.value) { + resetMobileHistory() } }) onUnmounted(() => { debouncedReloadPage.cancel() debouncedReloadSearchPage.cancel() + debouncedReloadMobileSearchPage.cancel() stopAiRedoProgress() closeProgressDialog() aiRedoProgressDialogController?.close() @@ -934,7 +1180,7 @@ onUnmounted(() => {