From a8db14a06992929e4210b44edeacdfe030611fb1 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Mon, 13 Jul 2026 12:33:45 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E5=A4=9A=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E6=BA=90=E8=AF=86=E5=88=AB=E7=BC=93=E5=AD=98=E7=AE=A1?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/types.ts | 16 +- ...chePanel.vue => RecognitionCachePanel.vue} | 178 +++++++++++------- src/locales/en-US.ts | 12 +- src/locales/zh-CN.ts | 12 +- src/locales/zh-TW.ts | 12 +- src/views/system/CacheView.vue | 23 ++- 6 files changed, 165 insertions(+), 88 deletions(-) rename src/components/cache/{TmdbRecognitionCachePanel.vue => RecognitionCachePanel.vue} (75%) diff --git a/src/api/types.ts b/src/api/types.ts index cbc30761..86c80ed5 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -1723,12 +1723,16 @@ export interface TorrentCacheData { data: TorrentCacheItem[] } -// TheMovieDb 识别缓存项 -export interface TmdbRecognitionCacheItem { +// 媒体识别缓存项 +export interface RecognitionCacheItem { // 缓存键 key: string // TMDB ID,0 表示未识别 - tmdb_id: number + tmdb_id?: number + // 豆瓣 ID,0 表示未识别 + douban_id?: string | number + // 当前识别数据源对应的统一 ID + recognition_id?: string // 识别后的标题 title: string // 识别后的年份 @@ -1741,8 +1745,8 @@ export interface TmdbRecognitionCacheItem { backdrop_path?: string } -// TheMovieDb 识别缓存数据 -export interface TmdbRecognitionCacheData { +// 媒体识别缓存数据 +export interface RecognitionCacheData { // 缓存总数 count: number // 已识别数量 @@ -1750,7 +1754,7 @@ export interface TmdbRecognitionCacheData { // 未识别数量 unrecognized: number // 缓存数据 - data: TmdbRecognitionCacheItem[] + data: RecognitionCacheItem[] } // 订阅分享统计 diff --git a/src/components/cache/TmdbRecognitionCachePanel.vue b/src/components/cache/RecognitionCachePanel.vue similarity index 75% rename from src/components/cache/TmdbRecognitionCachePanel.vue rename to src/components/cache/RecognitionCachePanel.vue index 24c94d82..58568d4a 100644 --- a/src/components/cache/TmdbRecognitionCachePanel.vue +++ b/src/components/cache/RecognitionCachePanel.vue @@ -3,13 +3,14 @@ import { useToast } from 'vue-toastification' import { useDisplay } from 'vuetify' import { useI18n } from 'vue-i18n' import api from '@/api' -import type { ApiResponse, TmdbRecognitionCacheData, TmdbRecognitionCacheItem } from '@/api/types' +import type { ApiResponse, RecognitionCacheData, RecognitionCacheItem } from '@/api/types' import { useConfirm } from '@/composables/useConfirm' import { useGlobalSettingsStore } from '@/stores' import { getDisplayImageUrl } from '@/utils/imageUtils' type RecognitionStatusFilter = 'all' | 'recognized' | 'unrecognized' type InfiniteScrollStatus = 'ok' | 'empty' | 'loading' | 'error' +type RecognitionCacheSource = 'tmdb' | 'douban' const MOBILE_CACHE_PAGE_SIZE = 20 const MOBILE_CACHE_ITEM_HEIGHT = 144 @@ -19,14 +20,28 @@ const display = useDisplay() const createConfirm = useConfirm() const $toast = useToast() const globalSettingsStore = useGlobalSettingsStore() -const globalSettings = globalSettingsStore.globalSettings const isMobile = computed(() => display.smAndDown.value) +const recognitionSource = computed(() => + globalSettingsStore.globalSettings.RECOGNIZE_SOURCE === 'douban' ? 'douban' : 'tmdb', +) +const recognitionSourceName = computed(() => + recognitionSource.value === 'douban' + ? t('setting.cache.recognitionSource.douban') + : t('setting.cache.recognitionSource.themoviedb'), +) +const recognitionIdLabel = computed(() => + recognitionSource.value === 'douban' ? t('setting.cache.doubanId') : t('setting.cache.tmdbId'), +) +const recognitionCacheEndpoint = computed(() => `${recognitionSource.value}/cache`) +const recognitionFilterPlaceholder = computed(() => + t('setting.cache.filterRecognitionCache', { source: recognitionSourceName.value }), +) const loading = ref(false) const searchFilter = ref('') const statusFilter = ref('all') const selectedItems = ref([]) -const cacheData = ref({ +const cacheData = ref({ count: 0, recognized: 0, unrecognized: 0, @@ -34,6 +49,7 @@ const cacheData = ref({ }) const mobileVisibleCount = ref(MOBILE_CACHE_PAGE_SIZE) const mobileInfiniteKey = ref(0) +let cacheLoadRequestId = 0 const statusOptions = computed(() => [ { title: t('setting.cache.allStatuses'), value: 'all' }, @@ -46,7 +62,7 @@ const tableHeaders = computed(() => [ { title: t('setting.cache.poster'), key: 'poster', sortable: false, width: '76px' }, { title: t('setting.cache.cacheKey'), key: 'key', sortable: true }, { title: t('setting.cache.recognitionResult'), key: 'result', sortable: false, width: '220px' }, - { title: t('setting.cache.tmdbId'), key: 'tmdb_id', sortable: true, width: '110px' }, + { title: recognitionIdLabel.value, key: 'recognition_id', sortable: true, width: '120px' }, { title: t('setting.cache.recognitionStatus'), key: 'status', sortable: true, width: '120px' }, { title: t('setting.cache.actions'), key: 'actions', sortable: false, width: '72px' }, ]) @@ -56,10 +72,10 @@ const filteredData = computed(() => { return cacheData.value.data.filter(item => { const matchesKeyword = !keyword || - [item.key, item.title, item.year, String(item.tmdb_id)].some(value => value.toLowerCase().includes(keyword)) + [item.key, item.title, item.year, getRecognitionId(item)].some(value => value.toLowerCase().includes(keyword)) const matchesStatus = statusFilter.value === 'all' || - (statusFilter.value === 'recognized' ? item.tmdb_id > 0 : item.tmdb_id === 0) + (statusFilter.value === 'recognized' ? isRecognized(item) : !isRecognized(item)) return matchesKeyword && matchesStatus }) }) @@ -89,35 +105,42 @@ function loadMoreMobileCache({ done }: { done: (status: InfiniteScrollStatus) => done(mobileHasMore.value ? 'ok' : 'empty') } -/** 加载 TheMovieDb 识别缓存列表。 */ +/** 加载当前识别数据源对应的缓存列表。 */ async function loadCacheData(showSuccess = false) { + const requestId = ++cacheLoadRequestId try { loading.value = true - const response = (await api.get('tmdb/cache')) as unknown as ApiResponse - cacheData.value = response.data ?? { count: 0, recognized: 0, unrecognized: 0, data: [] } + const response = (await api.get(recognitionCacheEndpoint.value)) as unknown as ApiResponse + if (requestId !== cacheLoadRequestId) return + const responseData = response.data ?? { count: 0, recognized: 0, unrecognized: 0, data: [] } + cacheData.value = { + ...responseData, + data: responseData.data.map(item => ({ ...item, recognition_id: getRecognitionId(item) })), + } selectedItems.value = selectedItems.value.filter(key => cacheData.value.data.some(item => item.key === key)) resetMobilePagination() if (showSuccess) $toast.success(t('setting.cache.listRefreshSuccess')) } catch (error) { + if (requestId !== cacheLoadRequestId) return console.error(error) $toast.error(t('setting.cache.loadFailed')) } finally { - loading.value = false + if (requestId === cacheLoadRequestId) loading.value = false } } -/** 清空全部 TheMovieDb 识别缓存。 */ +/** 清空当前识别数据源的全部识别缓存。 */ async function clearAllCache() { const confirmed = await createConfirm({ type: 'warn', title: t('common.confirm'), - content: t('setting.cache.tmdbClearConfirm'), + content: t('setting.cache.recognitionClearConfirm', { source: recognitionSourceName.value }), }) if (!confirmed) return try { loading.value = true - const response = (await api.delete('tmdb/cache')) as unknown as ApiResponse + const response = (await api.delete(recognitionCacheEndpoint.value)) as unknown as ApiResponse if (!response.success) throw new Error(response.message) $toast.success(response.message || t('setting.cache.clearSuccess')) await loadCacheData() @@ -130,13 +153,13 @@ async function clearAllCache() { } } -/** 请求后端删除指定 TheMovieDb 识别缓存。 */ +/** 请求当前识别数据源接口删除指定识别缓存。 */ async function deleteCacheItem(key: string) { - const response = (await api.delete(`tmdb/cache/${encodeURIComponent(key)}`)) as unknown as ApiResponse + const response = (await api.delete(`${recognitionCacheEndpoint.value}/${encodeURIComponent(key)}`)) as unknown as ApiResponse if (!response.success) throw new Error(response.message) } -/** 删除桌面端表格中选中的 TheMovieDb 识别缓存。 */ +/** 删除桌面端表格中选中的识别缓存。 */ async function deleteSelectedItems() { if (selectedItems.value.length === 0) { $toast.warning(t('setting.cache.selectDeleteWarning')) @@ -158,8 +181,8 @@ async function deleteSelectedItems() { } } -/** 删除单条 TheMovieDb 识别缓存。 */ -async function deleteSingleItem(item: TmdbRecognitionCacheItem) { +/** 删除单条识别缓存。 */ +async function deleteSingleItem(item: RecognitionCacheItem) { try { loading.value = true await deleteCacheItem(item.key) @@ -173,13 +196,25 @@ async function deleteSingleItem(item: TmdbRecognitionCacheItem) { } } -/** 获取 TheMovieDb 缓存海报的可展示地址。 */ -function getPosterUrl(item: TmdbRecognitionCacheItem): string { +/** 获取识别缓存海报的可展示地址。 */ +function getPosterUrl(item: RecognitionCacheItem): string { if (!item.poster_path) return '' const sourceUrl = item.poster_path.startsWith('/') - ? `https://${globalSettings.TMDB_IMAGE_DOMAIN}/t/p/w300${item.poster_path}` + ? `https://${globalSettingsStore.globalSettings.TMDB_IMAGE_DOMAIN}/t/p/w300${item.poster_path}` : item.poster_path - return getDisplayImageUrl(sourceUrl, globalSettings.GLOBAL_IMAGE_CACHE) + return getDisplayImageUrl(sourceUrl, globalSettingsStore.globalSettings.GLOBAL_IMAGE_CACHE) +} + +/** 获取当前识别数据源对应的媒体 ID。 */ +function getRecognitionId(item: RecognitionCacheItem): string { + const recognitionId = recognitionSource.value === 'douban' ? item.douban_id : item.tmdb_id + return recognitionId ? String(recognitionId) : '' +} + +/** 判断识别缓存条目是否包含有效媒体 ID。 */ +function isRecognized(item: RecognitionCacheItem): boolean { + const recognitionId = getRecognitionId(item) + return Boolean(recognitionId && recognitionId !== '0') } /** 获取本地化的媒体类型名称。 */ @@ -197,8 +232,8 @@ function getMediaTypeColor(mediaType: string): string { } /** 获取识别状态的本地化名称。 */ -function getRecognitionStatusLabel(item: TmdbRecognitionCacheItem): string { - return item.tmdb_id > 0 ? t('setting.cache.recognized') : t('setting.cache.unrecognized') +function getRecognitionStatusLabel(item: RecognitionCacheItem): string { + return isRecognized(item) ? t('setting.cache.recognized') : t('setting.cache.unrecognized') } onMounted(() => { @@ -208,10 +243,17 @@ onMounted(() => { watch([searchFilter, statusFilter], () => { resetMobilePagination() }) + +watch(recognitionSource, () => { + searchFilter.value = '' + statusFilter.value = 'all' + selectedItems.value = [] + void loadCacheData() +})