diff --git a/src/components/dialog/ShortcutToolDialog.vue b/src/components/dialog/ShortcutToolDialog.vue index 75cf969e..d3fc496d 100644 --- a/src/components/dialog/ShortcutToolDialog.vue +++ b/src/components/dialog/ShortcutToolDialog.vue @@ -1,9 +1,10 @@ + + diff --git a/src/locales/en-US.ts b/src/locales/en-US.ts index 6253967e..09568e26 100644 --- a/src/locales/en-US.ts +++ b/src/locales/en-US.ts @@ -2214,6 +2214,8 @@ export default { stopped: 'Stopped', waiting: 'Waiting', executeSuccess: 'Scheduled job execution request submitted successfully!', + mobileWaitingAfter: 'In {time}', + mobileNoNextRun: 'No schedule', }, subscribe: { basicSettings: 'Basic Settings', @@ -2265,6 +2267,7 @@ export default { filterByTitle: 'Filter by Title', filterBySite: 'Filter by Site', selectSite: 'Select Site', + loadingMore: 'Loading...', refresh: 'Refresh Cache', deleteSelected: 'Delete Selected', clearAll: 'Clear All Cache', diff --git a/src/locales/zh-CN.ts b/src/locales/zh-CN.ts index 4c2bf1a9..4400eb4a 100644 --- a/src/locales/zh-CN.ts +++ b/src/locales/zh-CN.ts @@ -2172,6 +2172,8 @@ export default { stopped: '已停止', waiting: '等待', executeSuccess: '定时作业执行请求提交成功!', + mobileWaitingAfter: '{time}之后', + mobileNoNextRun: '暂无排期', }, subscribe: { basicSettings: '基础设置', @@ -2220,6 +2222,7 @@ export default { filterByTitle: '按标题筛选', filterBySite: '按站点筛选', selectSite: '选择站点', + loadingMore: '加载中...', refresh: '刷新缓存', deleteSelected: '删除选中', clearAll: '清空缓存', diff --git a/src/locales/zh-TW.ts b/src/locales/zh-TW.ts index b817218c..36e30ca0 100644 --- a/src/locales/zh-TW.ts +++ b/src/locales/zh-TW.ts @@ -2173,6 +2173,8 @@ export default { stopped: '已停止', waiting: '等待', executeSuccess: '定時作業執行請求提交成功!', + mobileWaitingAfter: '{time}之後', + mobileNoNextRun: '暫無排程', }, subscribe: { basicSettings: '基礎設置', @@ -2221,6 +2223,7 @@ export default { filterByTitle: '按標題篩選', filterBySite: '按站點篩選', selectSite: '選擇站點', + loadingMore: '加載中...', refresh: '刷新緩存', deleteSelected: '刪除選中', clearAll: '清空緩存', diff --git a/src/views/system/CacheView.vue b/src/views/system/CacheView.vue index e58ce5c7..a7974c83 100644 --- a/src/views/system/CacheView.vue +++ b/src/views/system/CacheView.vue @@ -8,15 +8,24 @@ import { useConfirm } from '@/composables/useConfirm' import { useGlobalSettingsStore } from '@/stores' import { usePWA } from '@/composables/usePWA' import { openSharedDialog } from '@/composables/useSharedDialog' +import { useDisplay } from 'vuetify' const CacheReidentifyDialog = defineAsyncComponent(() => import('@/components/dialog/CacheReidentifyDialog.vue')) +type InfiniteScrollStatus = 'ok' | 'empty' | 'loading' | 'error' + +const MOBILE_CACHE_PAGE_SIZE = 20 + // 国际化 const { t } = useI18n() // PWA模式检测 const { appMode } = usePWA() +// 显示器宽度 +const display = useDisplay() +const isMobile = computed(() => display.smAndDown.value) + // 全局设置 const globalSettingsStore = useGlobalSettingsStore() const globalSettings = globalSettingsStore.globalSettings @@ -66,18 +75,37 @@ const loading = ref(false) const currentReidentifyItem = ref(null) +// 移动端已经追加到虚拟列表的数据条数 +const mobileVisibleCount = ref(MOBILE_CACHE_PAGE_SIZE) + let reidentifyDialogController: ReturnType | null = null const tableStyle = computed(() => { return appMode ? '' : 'height: calc(100vh - 21rem - env(safe-area-inset-bottom)' }) -// 调用API加载缓存数据 +// 移动端虚拟列表数据 +const mobileVisibleData = computed(() => filteredData.value.slice(0, mobileVisibleCount.value)) + +// 移动端是否还有未追加的数据页 +const mobileHasMore = computed(() => mobileVisibleData.value.length < filteredData.value.length) + +// 移动端无限滚动组件刷新键 +const mobileInfiniteKey = ref(0) + +/** 重置移动端分页,让筛选或刷新后的列表从第一页开始展示。 */ +function resetMobilePagination() { + mobileVisibleCount.value = MOBILE_CACHE_PAGE_SIZE + mobileInfiniteKey.value++ +} + +/** 调用 API 加载缓存数据。 */ async function loadCacheData() { try { loading.value = true const res: any = await api.get('torrent/cache') cacheData.value = res.data + resetMobilePagination() } catch (e) { console.log(e) $toast.error(t('setting.cache.loadFailed')) @@ -86,7 +114,23 @@ async function loadCacheData() { } } -// 清空所有缓存 +/** 追加移动端下一页数据,并通过虚拟滚动限制实际渲染节点数量。 */ +function loadMoreMobileCache({ done }: { done: (status: InfiniteScrollStatus) => void }) { + if (loading.value) { + done('ok') + return + } + + if (!mobileHasMore.value) { + done('empty') + return + } + + mobileVisibleCount.value = Math.min(mobileVisibleCount.value + MOBILE_CACHE_PAGE_SIZE, filteredData.value.length) + done(mobileHasMore.value ? 'ok' : 'empty') +} + +/** 清空所有缓存。 */ async function clearAllCache() { const isConfirmed = await createConfirm({ type: 'warn', @@ -109,7 +153,7 @@ async function clearAllCache() { } } -// 刷新缓存 +/** 刷新缓存数据。 */ async function refreshCache() { try { loading.value = true @@ -124,7 +168,7 @@ async function refreshCache() { } } -// 删除选中的缓存项 +/** 删除桌面端表格中选中的缓存项。 */ async function deleteSelectedItems() { if (selectedItems.value.length === 0) { $toast.warning(t('setting.cache.selectDeleteWarning')) @@ -153,7 +197,7 @@ async function deleteSelectedItems() { } } -// 删除单个缓存项 +/** 删除单个缓存项。 */ async function deleteSingleItem(item: TorrentCacheItem) { try { loading.value = true @@ -173,7 +217,7 @@ async function deleteSingleItem(item: TorrentCacheItem) { } } -// 打开重新识别对话框 +/** 打开重新识别对话框。 */ function openReidentifyDialog(item: TorrentCacheItem) { currentReidentifyItem.value = item reidentifyDialogController?.close() @@ -197,7 +241,7 @@ function openReidentifyDialog(item: TorrentCacheItem) { ) } -// 重新识别 +/** 执行缓存项重新识别。 */ async function performReidentify(payload: { doubanId?: string; tmdbId?: number } = {}) { if (!currentReidentifyItem.value) return @@ -229,11 +273,13 @@ async function performReidentify(payload: { doubanId?: string; tmdbId?: number } } } -// 获取媒体类型颜色 +/** 获取媒体类型对应的主题颜色。 */ function getMediaTypeColor(type: string): string { switch (type) { + case 'movie': case t('setting.cache.mediaType.movie'): return 'primary' + case 'tv': case t('setting.cache.mediaType.tv'): return 'success' default: @@ -241,7 +287,27 @@ function getMediaTypeColor(type: string): string { } } -// 打开详情页面 +/** 生成移动端缓存卡片的稳定渲染键。 */ +function getMobileCacheItemKey(item: TorrentCacheItem, index: number): string { + return item.hash || [item.domain, item.title, index].join('-') +} + +/** 获取移动端缓存卡片使用的媒体标题。 */ +function getMobileMediaTitle(item: TorrentCacheItem): string { + return item.media_name || item.description || t('setting.cache.unrecognized') +} + +/** 获取移动端缓存卡片展示的识别补充信息。 */ +function getMobileMediaMeta(item: TorrentCacheItem): string { + return [item.media_year, item.season_episode].filter(Boolean).join(' · ') +} + +/** 获取移动端缓存卡片展示的资源补充信息。 */ +function getMobileResourceMeta(item: TorrentCacheItem): string { + return [formatDateDifference(item.pubdate || ''), item.resource_term, item.site_name].filter(Boolean).join(' · ') +} + +/** 打开缓存项的站点详情页面。 */ function openPageUrl(url: string) { window.open(url, '_blank') } @@ -249,10 +315,175 @@ function openPageUrl(url: string) { onMounted(() => { loadCacheData() }) + +watch([titleFilter, siteFilter], () => { + resetMobilePagination() +})