mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-05 23:56:42 +08:00
feat: 支持多类缓存管理
This commit is contained in:
@@ -1723,6 +1723,36 @@ export interface TorrentCacheData {
|
|||||||
data: TorrentCacheItem[]
|
data: TorrentCacheItem[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TheMovieDb 识别缓存项
|
||||||
|
export interface TmdbRecognitionCacheItem {
|
||||||
|
// 缓存键
|
||||||
|
key: string
|
||||||
|
// TMDB ID,0 表示未识别
|
||||||
|
tmdb_id: number
|
||||||
|
// 识别后的标题
|
||||||
|
title: string
|
||||||
|
// 识别后的年份
|
||||||
|
year: string
|
||||||
|
// 媒体类型
|
||||||
|
media_type: string
|
||||||
|
// TMDB 海报相对路径
|
||||||
|
poster_path?: string
|
||||||
|
// TMDB 背景图相对路径
|
||||||
|
backdrop_path?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// TheMovieDb 识别缓存数据
|
||||||
|
export interface TmdbRecognitionCacheData {
|
||||||
|
// 缓存总数
|
||||||
|
count: number
|
||||||
|
// 已识别数量
|
||||||
|
recognized: number
|
||||||
|
// 未识别数量
|
||||||
|
unrecognized: number
|
||||||
|
// 缓存数据
|
||||||
|
data: TmdbRecognitionCacheItem[]
|
||||||
|
}
|
||||||
|
|
||||||
// 订阅分享统计
|
// 订阅分享统计
|
||||||
export interface SubscribeShareStatistics {
|
export interface SubscribeShareStatistics {
|
||||||
// 分享人
|
// 分享人
|
||||||
|
|||||||
+660
@@ -0,0 +1,660 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
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 { useConfirm } from '@/composables/useConfirm'
|
||||||
|
import { useGlobalSettingsStore } from '@/stores'
|
||||||
|
import { getDisplayImageUrl } from '@/utils/imageUtils'
|
||||||
|
|
||||||
|
type RecognitionStatusFilter = 'all' | 'recognized' | 'unrecognized'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const display = useDisplay()
|
||||||
|
const createConfirm = useConfirm()
|
||||||
|
const $toast = useToast()
|
||||||
|
const globalSettingsStore = useGlobalSettingsStore()
|
||||||
|
const globalSettings = globalSettingsStore.globalSettings
|
||||||
|
|
||||||
|
const isMobile = computed(() => display.smAndDown.value)
|
||||||
|
const loading = ref(false)
|
||||||
|
const searchFilter = ref('')
|
||||||
|
const statusFilter = ref<RecognitionStatusFilter>('all')
|
||||||
|
const selectedItems = ref<string[]>([])
|
||||||
|
const cacheData = ref<TmdbRecognitionCacheData>({
|
||||||
|
count: 0,
|
||||||
|
recognized: 0,
|
||||||
|
unrecognized: 0,
|
||||||
|
data: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
const statusOptions = computed(() => [
|
||||||
|
{ title: t('setting.cache.allStatuses'), value: 'all' },
|
||||||
|
{ title: t('setting.cache.recognizedOnly'), value: 'recognized' },
|
||||||
|
{ title: t('setting.cache.unrecognizedOnly'), value: 'unrecognized' },
|
||||||
|
])
|
||||||
|
|
||||||
|
const tableHeaders = computed(() => [
|
||||||
|
{ title: '', key: 'data-table-select', sortable: false, width: '48px' },
|
||||||
|
{ 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: t('setting.cache.recognitionStatus'), key: 'status', sortable: true, width: '120px' },
|
||||||
|
{ title: t('setting.cache.actions'), key: 'actions', sortable: false, width: '72px' },
|
||||||
|
])
|
||||||
|
|
||||||
|
const filteredData = computed(() => {
|
||||||
|
const keyword = searchFilter.value.trim().toLowerCase()
|
||||||
|
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))
|
||||||
|
const matchesStatus =
|
||||||
|
statusFilter.value === 'all' ||
|
||||||
|
(statusFilter.value === 'recognized' ? item.tmdb_id > 0 : item.tmdb_id === 0)
|
||||||
|
return matchesKeyword && matchesStatus
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 加载 TheMovieDb 识别缓存列表。 */
|
||||||
|
async function loadCacheData(showSuccess = false) {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
const response = (await api.get('tmdb/cache')) as unknown as ApiResponse<TmdbRecognitionCacheData>
|
||||||
|
cacheData.value = response.data ?? { count: 0, recognized: 0, unrecognized: 0, data: [] }
|
||||||
|
selectedItems.value = selectedItems.value.filter(key => cacheData.value.data.some(item => item.key === key))
|
||||||
|
if (showSuccess) $toast.success(t('setting.cache.listRefreshSuccess'))
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
$toast.error(t('setting.cache.loadFailed'))
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 清空全部 TheMovieDb 识别缓存。 */
|
||||||
|
async function clearAllCache() {
|
||||||
|
const confirmed = await createConfirm({
|
||||||
|
type: 'warn',
|
||||||
|
title: t('common.confirm'),
|
||||||
|
content: t('setting.cache.tmdbClearConfirm'),
|
||||||
|
})
|
||||||
|
if (!confirmed) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
const response = (await api.delete('tmdb/cache')) as unknown as ApiResponse
|
||||||
|
if (!response.success) throw new Error(response.message)
|
||||||
|
$toast.success(response.message || t('setting.cache.clearSuccess'))
|
||||||
|
await loadCacheData()
|
||||||
|
selectedItems.value = []
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
$toast.error(t('setting.cache.clearFailed'))
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 请求后端删除指定 TheMovieDb 识别缓存。 */
|
||||||
|
async function deleteCacheItem(key: string) {
|
||||||
|
const response = (await api.delete(`tmdb/cache/${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'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteCount = selectedItems.value.length
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
await Promise.all(selectedItems.value.map(deleteCacheItem))
|
||||||
|
$toast.success(t('setting.cache.deleteSelectedSuccess', { count: deleteCount }))
|
||||||
|
await loadCacheData()
|
||||||
|
selectedItems.value = []
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
$toast.error(t('setting.cache.deleteSelectedFailed'))
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除单条 TheMovieDb 识别缓存。 */
|
||||||
|
async function deleteSingleItem(item: TmdbRecognitionCacheItem) {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
await deleteCacheItem(item.key)
|
||||||
|
$toast.success(t('setting.cache.deleteSuccess'))
|
||||||
|
await loadCacheData()
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
$toast.error(t('setting.cache.deleteFailed'))
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取 TheMovieDb 缓存海报的可展示地址。 */
|
||||||
|
function getPosterUrl(item: TmdbRecognitionCacheItem): string {
|
||||||
|
if (!item.poster_path) return ''
|
||||||
|
const sourceUrl = item.poster_path.startsWith('/')
|
||||||
|
? `https://${globalSettings.TMDB_IMAGE_DOMAIN}/t/p/w300${item.poster_path}`
|
||||||
|
: item.poster_path
|
||||||
|
return getDisplayImageUrl(sourceUrl, globalSettings.GLOBAL_IMAGE_CACHE)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取本地化的媒体类型名称。 */
|
||||||
|
function getMediaTypeLabel(mediaType: string): string {
|
||||||
|
if (mediaType === 'movie') return t('setting.cache.mediaType.movie')
|
||||||
|
if (mediaType === 'tv') return t('setting.cache.mediaType.tv')
|
||||||
|
return t('setting.cache.mediaType.unknown')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取媒体类型对应的主题颜色。 */
|
||||||
|
function getMediaTypeColor(mediaType: string): string {
|
||||||
|
if (mediaType === 'movie') return 'primary'
|
||||||
|
if (mediaType === 'tv') return 'success'
|
||||||
|
return 'secondary'
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取识别状态的本地化名称。 */
|
||||||
|
function getRecognitionStatusLabel(item: TmdbRecognitionCacheItem): string {
|
||||||
|
return item.tmdb_id > 0 ? t('setting.cache.recognized') : t('setting.cache.unrecognized')
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
void loadCacheData()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="tmdb-cache-panel">
|
||||||
|
<div class="cache-panel-toolbar">
|
||||||
|
<div class="cache-panel-stats">
|
||||||
|
<div class="cache-panel-stat cache-panel-stat--primary">
|
||||||
|
<VIcon icon="mdi-database-outline" :size="isMobile ? 32 : 22" />
|
||||||
|
<div>
|
||||||
|
<strong>{{ cacheData.count }}</strong>
|
||||||
|
<span>{{ t('setting.cache.totalCount') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="cache-panel-stat cache-panel-stat--success">
|
||||||
|
<VIcon icon="mdi-check-decagram-outline" :size="isMobile ? 32 : 22" />
|
||||||
|
<div>
|
||||||
|
<strong>{{ cacheData.recognized }}</strong>
|
||||||
|
<span>{{ t('setting.cache.recognized') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="!isMobile" class="cache-panel-stat cache-panel-stat--warning">
|
||||||
|
<VIcon icon="mdi-help-circle-outline" size="22" />
|
||||||
|
<div>
|
||||||
|
<strong>{{ cacheData.unrecognized }}</strong>
|
||||||
|
<span>{{ t('setting.cache.unrecognized') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="!isMobile" class="cache-panel-actions">
|
||||||
|
<VBtn icon variant="text" color="primary" :loading="loading" @click="loadCacheData(true)">
|
||||||
|
<VIcon icon="mdi-refresh" />
|
||||||
|
<VTooltip activator="parent" location="bottom">{{ t('setting.cache.refreshList') }}</VTooltip>
|
||||||
|
</VBtn>
|
||||||
|
<VBtn
|
||||||
|
icon
|
||||||
|
variant="text"
|
||||||
|
color="warning"
|
||||||
|
:disabled="selectedItems.length === 0"
|
||||||
|
:loading="loading"
|
||||||
|
@click="deleteSelectedItems"
|
||||||
|
>
|
||||||
|
<VIcon icon="mdi-delete-sweep-outline" />
|
||||||
|
<VTooltip activator="parent" location="bottom">
|
||||||
|
{{ t('setting.cache.deleteSelected') }} ({{ selectedItems.length }})
|
||||||
|
</VTooltip>
|
||||||
|
</VBtn>
|
||||||
|
<VBtn icon variant="text" color="error" :loading="loading" @click="clearAllCache">
|
||||||
|
<VIcon icon="mdi-delete-variant" />
|
||||||
|
<VTooltip activator="parent" location="bottom">{{ t('setting.cache.clearAll') }}</VTooltip>
|
||||||
|
</VBtn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cache-panel-filters">
|
||||||
|
<VTextField
|
||||||
|
v-model="searchFilter"
|
||||||
|
class="cache-panel-filter"
|
||||||
|
:label="isMobile ? undefined : t('setting.cache.filterRecognitionCache')"
|
||||||
|
:placeholder="isMobile ? t('setting.cache.filterRecognitionCache') : undefined"
|
||||||
|
prepend-inner-icon="mdi-magnify"
|
||||||
|
variant="outlined"
|
||||||
|
:density="isMobile ? 'comfortable' : 'compact'"
|
||||||
|
:single-line="isMobile"
|
||||||
|
clearable
|
||||||
|
hide-details
|
||||||
|
/>
|
||||||
|
<VSelect
|
||||||
|
v-model="statusFilter"
|
||||||
|
class="cache-panel-filter"
|
||||||
|
:label="isMobile ? undefined : t('setting.cache.recognitionStatus')"
|
||||||
|
:placeholder="isMobile ? t('setting.cache.recognitionStatus') : undefined"
|
||||||
|
:items="statusOptions"
|
||||||
|
prepend-inner-icon="mdi-list-status"
|
||||||
|
variant="outlined"
|
||||||
|
:density="isMobile ? 'comfortable' : 'compact'"
|
||||||
|
:single-line="isMobile"
|
||||||
|
hide-details
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="isMobile" class="cache-panel-mobile-actions">
|
||||||
|
<VBtn variant="tonal" color="primary" :loading="loading" prepend-icon="mdi-refresh" @click="loadCacheData(true)">
|
||||||
|
{{ t('setting.cache.refresh') }}
|
||||||
|
</VBtn>
|
||||||
|
<VBtn variant="tonal" color="error" :loading="loading" prepend-icon="mdi-delete-variant" @click="clearAllCache">
|
||||||
|
{{ t('setting.cache.clearAll') }}
|
||||||
|
</VBtn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="isMobile" class="tmdb-cache-mobile-list">
|
||||||
|
<article v-for="item in filteredData" :key="item.key" class="tmdb-cache-mobile-item">
|
||||||
|
<div class="tmdb-cache-poster">
|
||||||
|
<VImg v-if="getPosterUrl(item)" :src="getPosterUrl(item)" :alt="item.title || item.key" cover />
|
||||||
|
<VIcon v-else icon="mdi-image-off-outline" size="28" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tmdb-cache-mobile-item__content">
|
||||||
|
<div class="tmdb-cache-mobile-item__title">
|
||||||
|
{{ item.title || t('setting.cache.unrecognized') }}
|
||||||
|
</div>
|
||||||
|
<div class="tmdb-cache-mobile-item__meta">
|
||||||
|
<VChip size="x-small" variant="tonal" :color="getMediaTypeColor(item.media_type)">
|
||||||
|
{{ getMediaTypeLabel(item.media_type) }}
|
||||||
|
</VChip>
|
||||||
|
<span v-if="item.year">{{ item.year }}</span>
|
||||||
|
<span v-if="item.tmdb_id">TMDB #{{ item.tmdb_id }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="tmdb-cache-mobile-item__key">{{ item.key }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<VBtn
|
||||||
|
icon
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
color="error"
|
||||||
|
:aria-label="t('common.delete')"
|
||||||
|
@click="deleteSingleItem(item)"
|
||||||
|
>
|
||||||
|
<VIcon icon="mdi-delete-outline" size="20" />
|
||||||
|
</VBtn>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<div v-if="filteredData.length === 0 && !loading" class="cache-panel-empty">
|
||||||
|
<VIcon icon="mdi-database-search-outline" size="42" />
|
||||||
|
<strong>{{ t('setting.cache.noRecognitionCache') }}</strong>
|
||||||
|
<span>{{ t('setting.cache.noRecognitionCacheHint') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<VDataTable
|
||||||
|
v-else
|
||||||
|
v-model="selectedItems"
|
||||||
|
class="tmdb-cache-table"
|
||||||
|
:headers="tableHeaders"
|
||||||
|
:items="filteredData"
|
||||||
|
:loading="loading"
|
||||||
|
item-value="key"
|
||||||
|
show-select
|
||||||
|
hover
|
||||||
|
fixed-header
|
||||||
|
:items-per-page-text="t('common.itemsPerPage')"
|
||||||
|
:no-data-text="t('common.noDataText')"
|
||||||
|
:loading-text="t('common.loadingText')"
|
||||||
|
>
|
||||||
|
<template #item.poster="{ item }">
|
||||||
|
<div class="tmdb-cache-table__poster">
|
||||||
|
<VImg v-if="getPosterUrl(item)" :src="getPosterUrl(item)" :alt="item.title || item.key" cover />
|
||||||
|
<VIcon v-else icon="mdi-image-off-outline" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #item.key="{ item }">
|
||||||
|
<div class="tmdb-cache-table__key">{{ item.key }}</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #item.result="{ item }">
|
||||||
|
<div class="tmdb-cache-result">
|
||||||
|
<strong>{{ item.title || t('setting.cache.unrecognized') }}</strong>
|
||||||
|
<span v-if="item.year">{{ item.year }}</span>
|
||||||
|
<VChip size="x-small" variant="tonal" :color="getMediaTypeColor(item.media_type)">
|
||||||
|
{{ getMediaTypeLabel(item.media_type) }}
|
||||||
|
</VChip>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #item.tmdb_id="{ item }">
|
||||||
|
<span v-if="item.tmdb_id" class="font-weight-medium">#{{ item.tmdb_id }}</span>
|
||||||
|
<span v-else class="text-medium-emphasis">-</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #item.status="{ item }">
|
||||||
|
<VChip size="small" variant="tonal" :color="item.tmdb_id > 0 ? 'success' : 'warning'">
|
||||||
|
{{ getRecognitionStatusLabel(item) }}
|
||||||
|
</VChip>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #item.actions="{ item }">
|
||||||
|
<VBtn icon size="small" variant="text" color="error" @click="deleteSingleItem(item)">
|
||||||
|
<VIcon icon="mdi-delete-outline" size="18" />
|
||||||
|
<VTooltip activator="parent" location="start">{{ t('common.delete') }}</VTooltip>
|
||||||
|
</VBtn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #no-data>
|
||||||
|
<div class="cache-panel-empty">
|
||||||
|
<VIcon icon="mdi-database-search-outline" size="42" />
|
||||||
|
<strong>{{ t('setting.cache.noRecognitionCache') }}</strong>
|
||||||
|
<span>{{ t('setting.cache.noRecognitionCacheHint') }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</VDataTable>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.tmdb-cache-panel {
|
||||||
|
display: flex;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
flex-direction: column;
|
||||||
|
min-block-size: 0;
|
||||||
|
padding: 20px;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cache-panel-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cache-panel-stats {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cache-panel-stat {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border: var(--app-surface-border);
|
||||||
|
border-radius: var(--app-surface-radius);
|
||||||
|
background: var(--app-grouped-list-background);
|
||||||
|
box-shadow: var(--app-surface-shadow);
|
||||||
|
min-block-size: 58px;
|
||||||
|
min-inline-size: 126px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cache-panel-stat strong,
|
||||||
|
.cache-panel-stat span {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cache-panel-stat strong {
|
||||||
|
color: rgba(var(--v-theme-on-surface), 0.9);
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1.15;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cache-panel-stat span {
|
||||||
|
margin-block-start: 3px;
|
||||||
|
color: rgba(var(--v-theme-on-surface), 0.58);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cache-panel-stat--primary {
|
||||||
|
color: rgb(var(--v-theme-primary));
|
||||||
|
}
|
||||||
|
|
||||||
|
.cache-panel-stat--success {
|
||||||
|
color: rgb(var(--v-theme-success));
|
||||||
|
}
|
||||||
|
|
||||||
|
.cache-panel-stat--warning {
|
||||||
|
color: rgb(var(--v-theme-warning));
|
||||||
|
}
|
||||||
|
|
||||||
|
.cache-panel-actions {
|
||||||
|
display: flex;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cache-panel-filters {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
grid-template-columns: minmax(0, 1fr) minmax(180px, 0.35fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cache-panel-filters :deep(.v-field) {
|
||||||
|
border-radius: var(--app-field-radius);
|
||||||
|
background: var(--app-grouped-list-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cache-panel-mobile-actions {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.cache-panel-mobile-actions :deep(.v-btn) {
|
||||||
|
min-block-size: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tmdb-cache-table {
|
||||||
|
overflow: hidden;
|
||||||
|
border: var(--app-surface-border);
|
||||||
|
border-radius: var(--app-surface-radius);
|
||||||
|
box-shadow: var(--app-surface-shadow);
|
||||||
|
max-block-size: calc(100dvh - 23rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tmdb-cache-table__poster,
|
||||||
|
.tmdb-cache-poster {
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: var(--app-control-radius);
|
||||||
|
background: rgba(var(--v-theme-on-surface), 0.06);
|
||||||
|
color: rgba(var(--v-theme-on-surface), 0.36);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tmdb-cache-table__poster {
|
||||||
|
block-size: 62px;
|
||||||
|
inline-size: 44px;
|
||||||
|
margin-block: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tmdb-cache-table__poster :deep(.v-img),
|
||||||
|
.tmdb-cache-poster :deep(.v-img) {
|
||||||
|
block-size: 100%;
|
||||||
|
inline-size: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tmdb-cache-table__key {
|
||||||
|
max-inline-size: 36rem;
|
||||||
|
color: rgba(var(--v-theme-on-surface), 0.68);
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tmdb-cache-result {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tmdb-cache-result strong {
|
||||||
|
inline-size: 100%;
|
||||||
|
color: rgba(var(--v-theme-on-surface), 0.88);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tmdb-cache-result span {
|
||||||
|
color: rgba(var(--v-theme-on-surface), 0.56);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cache-panel-empty {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
min-block-size: 14rem;
|
||||||
|
padding: 24px;
|
||||||
|
color: rgba(var(--v-theme-on-surface), 0.48);
|
||||||
|
text-align: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cache-panel-empty strong {
|
||||||
|
color: rgba(var(--v-theme-on-surface), 0.78);
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cache-panel-empty span {
|
||||||
|
max-inline-size: 30rem;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 959.98px) {
|
||||||
|
.tmdb-cache-panel {
|
||||||
|
overflow-y: auto;
|
||||||
|
block-size: 100%;
|
||||||
|
padding: 14px 16px calc(18px + env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
|
||||||
|
.cache-panel-toolbar {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cache-panel-stats {
|
||||||
|
display: grid;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
gap: 12px;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.cache-panel-stat {
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: row;
|
||||||
|
min-block-size: 92px;
|
||||||
|
min-inline-size: 0;
|
||||||
|
padding: 18px;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cache-panel-stat strong {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1.05;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cache-panel-stat span {
|
||||||
|
margin-block-start: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cache-panel-filters {
|
||||||
|
gap: 10px;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cache-panel-filter :deep(.v-field__outline) {
|
||||||
|
color: rgba(var(--v-theme-on-surface), 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cache-panel-filter :deep(.v-field__input) {
|
||||||
|
min-block-size: 54px;
|
||||||
|
color: rgba(var(--v-theme-on-surface), 0.72);
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tmdb-cache-mobile-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tmdb-cache-mobile-item {
|
||||||
|
display: grid;
|
||||||
|
align-items: start;
|
||||||
|
border: var(--app-surface-border);
|
||||||
|
border-radius: var(--app-surface-radius);
|
||||||
|
backdrop-filter: var(--app-grouped-list-backdrop-filter);
|
||||||
|
background: var(--app-grouped-list-background);
|
||||||
|
box-shadow: var(--app-surface-shadow);
|
||||||
|
grid-template-columns: 54px minmax(0, 1fr) 36px;
|
||||||
|
padding: 12px;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tmdb-cache-poster {
|
||||||
|
block-size: 78px;
|
||||||
|
inline-size: 54px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tmdb-cache-mobile-item__content {
|
||||||
|
min-inline-size: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tmdb-cache-mobile-item__title {
|
||||||
|
color: rgba(var(--v-theme-on-surface), 0.9);
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tmdb-cache-mobile-item__meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-block-start: 7px;
|
||||||
|
color: rgba(var(--v-theme-on-surface), 0.58);
|
||||||
|
font-size: 12px;
|
||||||
|
gap: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tmdb-cache-mobile-item__key {
|
||||||
|
margin-block-start: 8px;
|
||||||
|
color: rgba(var(--v-theme-on-surface), 0.48);
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.35;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 374.98px) {
|
||||||
|
.tmdb-cache-panel {
|
||||||
|
padding-inline: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cache-panel-stat {
|
||||||
|
padding: 12px;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -129,13 +129,8 @@ const bodyClasses = computed(() => [
|
|||||||
display: flex;
|
display: flex;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background: rgb(var(--v-theme-surface));
|
backdrop-filter: var(--app-grouped-list-backdrop-filter);
|
||||||
}
|
background: var(--app-grouped-list-background);
|
||||||
|
|
||||||
html[data-theme='transparent'] .cache-shortcut-dialog-card,
|
|
||||||
.v-theme--transparent .cache-shortcut-dialog-card {
|
|
||||||
backdrop-filter: blur(var(--transparent-blur, 10px));
|
|
||||||
background: rgba(var(--v-theme-surface), var(--transparent-opacity-heavy, 0.5));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.cache-shortcut-dialog-body {
|
.cache-shortcut-dialog-body {
|
||||||
|
|||||||
+18
-1
@@ -2386,14 +2386,24 @@ export default {
|
|||||||
},
|
},
|
||||||
cache: {
|
cache: {
|
||||||
title: 'Cache Management',
|
title: 'Cache Management',
|
||||||
subtitle: 'Manage cached site resources',
|
subtitle: 'Manage system caches',
|
||||||
|
cacheType: 'Cache Type',
|
||||||
|
torrentCache: 'Resource Cache',
|
||||||
|
tmdbRecognitionCache: 'Recognition Cache',
|
||||||
totalCount: 'Total Count',
|
totalCount: 'Total Count',
|
||||||
siteCount: 'Site Count',
|
siteCount: 'Site Count',
|
||||||
|
recognized: 'Recognized',
|
||||||
|
recognizedOnly: 'Recognized Only',
|
||||||
|
unrecognizedOnly: 'Unrecognized Only',
|
||||||
|
allStatuses: 'All Statuses',
|
||||||
filterByTitle: 'Filter by Title',
|
filterByTitle: 'Filter by Title',
|
||||||
filterBySite: 'Filter by Site',
|
filterBySite: 'Filter by Site',
|
||||||
|
filterRecognitionCache: 'Search cache key, title, or TMDB ID',
|
||||||
selectSite: 'Select Site',
|
selectSite: 'Select Site',
|
||||||
loadingMore: 'Loading...',
|
loadingMore: 'Loading...',
|
||||||
refresh: 'Refresh Cache',
|
refresh: 'Refresh Cache',
|
||||||
|
refreshList: 'Refresh List',
|
||||||
|
listRefreshSuccess: 'Cache list refreshed',
|
||||||
deleteSelected: 'Delete Selected',
|
deleteSelected: 'Delete Selected',
|
||||||
clearAll: 'Clear All Cache',
|
clearAll: 'Clear All Cache',
|
||||||
refreshSuccess: 'Cache refresh completed',
|
refreshSuccess: 'Cache refresh completed',
|
||||||
@@ -2410,15 +2420,20 @@ export default {
|
|||||||
reidentifySuccess: 'Re-identification completed',
|
reidentifySuccess: 'Re-identification completed',
|
||||||
reidentifyFailed: 'Re-identification failed',
|
reidentifyFailed: 'Re-identification failed',
|
||||||
poster: 'Poster',
|
poster: 'Poster',
|
||||||
|
cacheKey: 'Recognition Request',
|
||||||
|
tmdbId: 'TMDB ID',
|
||||||
torrentTitle: 'Title',
|
torrentTitle: 'Title',
|
||||||
site: 'Site',
|
site: 'Site',
|
||||||
size: 'Size',
|
size: 'Size',
|
||||||
publishTime: 'Publish Time',
|
publishTime: 'Publish Time',
|
||||||
recognitionResult: 'Recognition Result',
|
recognitionResult: 'Recognition Result',
|
||||||
|
recognitionStatus: 'Status',
|
||||||
actions: 'Actions',
|
actions: 'Actions',
|
||||||
unrecognized: 'Unrecognized',
|
unrecognized: 'Unrecognized',
|
||||||
noData: 'No cache data',
|
noData: 'No cache data',
|
||||||
noDataHint: 'Click "Refresh Cache" button to get the latest torrent cache',
|
noDataHint: 'Click "Refresh Cache" button to get the latest torrent cache',
|
||||||
|
noRecognitionCache: 'No TheMovieDb recognition cache',
|
||||||
|
noRecognitionCacheHint: 'Recognition records will appear here after media is identified',
|
||||||
reidentifyDialog: {
|
reidentifyDialog: {
|
||||||
title: 'Re-identify',
|
title: 'Re-identify',
|
||||||
torrentInfo: 'Torrent Info',
|
torrentInfo: 'Torrent Info',
|
||||||
@@ -2433,8 +2448,10 @@ export default {
|
|||||||
mediaType: {
|
mediaType: {
|
||||||
movie: 'Movie',
|
movie: 'Movie',
|
||||||
tv: 'TV Show',
|
tv: 'TV Show',
|
||||||
|
unknown: 'Unknown',
|
||||||
},
|
},
|
||||||
clearConfirm: 'Are you sure you want to clear all cache?',
|
clearConfirm: 'Are you sure you want to clear all cache?',
|
||||||
|
tmdbClearConfirm: 'Clear all TheMovieDb recognition cache?',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
dialog: {
|
dialog: {
|
||||||
|
|||||||
+18
-1
@@ -2341,14 +2341,24 @@ export default {
|
|||||||
},
|
},
|
||||||
cache: {
|
cache: {
|
||||||
title: '缓存管理',
|
title: '缓存管理',
|
||||||
subtitle: '管理缓存的站点资源',
|
subtitle: '管理多类系统缓存',
|
||||||
|
cacheType: '缓存类型',
|
||||||
|
torrentCache: '资源缓存',
|
||||||
|
tmdbRecognitionCache: '识别缓存',
|
||||||
totalCount: '总条数',
|
totalCount: '总条数',
|
||||||
siteCount: '站点数',
|
siteCount: '站点数',
|
||||||
|
recognized: '已识别',
|
||||||
|
recognizedOnly: '仅已识别',
|
||||||
|
unrecognizedOnly: '仅未识别',
|
||||||
|
allStatuses: '全部状态',
|
||||||
filterByTitle: '按标题筛选',
|
filterByTitle: '按标题筛选',
|
||||||
filterBySite: '按站点筛选',
|
filterBySite: '按站点筛选',
|
||||||
|
filterRecognitionCache: '搜索缓存键、标题或 TMDB ID',
|
||||||
selectSite: '选择站点',
|
selectSite: '选择站点',
|
||||||
loadingMore: '加载中...',
|
loadingMore: '加载中...',
|
||||||
refresh: '刷新缓存',
|
refresh: '刷新缓存',
|
||||||
|
refreshList: '刷新列表',
|
||||||
|
listRefreshSuccess: '缓存列表已刷新',
|
||||||
deleteSelected: '删除选中',
|
deleteSelected: '删除选中',
|
||||||
clearAll: '清空缓存',
|
clearAll: '清空缓存',
|
||||||
refreshSuccess: '缓存刷新完成',
|
refreshSuccess: '缓存刷新完成',
|
||||||
@@ -2365,15 +2375,20 @@ export default {
|
|||||||
reidentifySuccess: '重新识别完成',
|
reidentifySuccess: '重新识别完成',
|
||||||
reidentifyFailed: '重新识别失败',
|
reidentifyFailed: '重新识别失败',
|
||||||
poster: '海报',
|
poster: '海报',
|
||||||
|
cacheKey: '识别请求',
|
||||||
|
tmdbId: 'TMDB ID',
|
||||||
torrentTitle: '标题',
|
torrentTitle: '标题',
|
||||||
site: '站点',
|
site: '站点',
|
||||||
size: '大小',
|
size: '大小',
|
||||||
publishTime: '发布时间',
|
publishTime: '发布时间',
|
||||||
recognitionResult: '识别结果',
|
recognitionResult: '识别结果',
|
||||||
|
recognitionStatus: '识别状态',
|
||||||
actions: '操作',
|
actions: '操作',
|
||||||
unrecognized: '未识别',
|
unrecognized: '未识别',
|
||||||
noData: '暂无缓存数据',
|
noData: '暂无缓存数据',
|
||||||
noDataHint: '点击"刷新缓存"按钮获取最新的种子缓存',
|
noDataHint: '点击"刷新缓存"按钮获取最新的种子缓存',
|
||||||
|
noRecognitionCache: '暂无 TheMovieDb 识别缓存',
|
||||||
|
noRecognitionCacheHint: '完成媒体识别后,缓存记录会显示在这里',
|
||||||
reidentifyDialog: {
|
reidentifyDialog: {
|
||||||
title: '重新识别',
|
title: '重新识别',
|
||||||
torrentInfo: '种子信息',
|
torrentInfo: '种子信息',
|
||||||
@@ -2388,8 +2403,10 @@ export default {
|
|||||||
mediaType: {
|
mediaType: {
|
||||||
movie: '电影',
|
movie: '电影',
|
||||||
tv: '电视剧',
|
tv: '电视剧',
|
||||||
|
unknown: '未知',
|
||||||
},
|
},
|
||||||
clearConfirm: '确认清空所有缓存吗?',
|
clearConfirm: '确认清空所有缓存吗?',
|
||||||
|
tmdbClearConfirm: '确认清空全部 TheMovieDb 识别缓存吗?',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
dialog: {
|
dialog: {
|
||||||
|
|||||||
+18
-1
@@ -2340,14 +2340,24 @@ export default {
|
|||||||
},
|
},
|
||||||
cache: {
|
cache: {
|
||||||
title: '緩存管理',
|
title: '緩存管理',
|
||||||
subtitle: '管理緩存的站點資源',
|
subtitle: '管理多類系統緩存',
|
||||||
|
cacheType: '緩存類型',
|
||||||
|
torrentCache: '資源緩存',
|
||||||
|
tmdbRecognitionCache: '識別緩存',
|
||||||
totalCount: '總條數',
|
totalCount: '總條數',
|
||||||
siteCount: '站點數',
|
siteCount: '站點數',
|
||||||
|
recognized: '已識別',
|
||||||
|
recognizedOnly: '僅已識別',
|
||||||
|
unrecognizedOnly: '僅未識別',
|
||||||
|
allStatuses: '全部狀態',
|
||||||
filterByTitle: '按標題篩選',
|
filterByTitle: '按標題篩選',
|
||||||
filterBySite: '按站點篩選',
|
filterBySite: '按站點篩選',
|
||||||
|
filterRecognitionCache: '搜索緩存鍵、標題或 TMDB ID',
|
||||||
selectSite: '選擇站點',
|
selectSite: '選擇站點',
|
||||||
loadingMore: '加載中...',
|
loadingMore: '加載中...',
|
||||||
refresh: '刷新緩存',
|
refresh: '刷新緩存',
|
||||||
|
refreshList: '刷新列表',
|
||||||
|
listRefreshSuccess: '緩存列表已刷新',
|
||||||
deleteSelected: '刪除選中',
|
deleteSelected: '刪除選中',
|
||||||
clearAll: '清空緩存',
|
clearAll: '清空緩存',
|
||||||
refreshSuccess: '緩存刷新完成',
|
refreshSuccess: '緩存刷新完成',
|
||||||
@@ -2364,15 +2374,20 @@ export default {
|
|||||||
reidentifySuccess: '重新識別完成',
|
reidentifySuccess: '重新識別完成',
|
||||||
reidentifyFailed: '重新識別失敗',
|
reidentifyFailed: '重新識別失敗',
|
||||||
poster: '海報',
|
poster: '海報',
|
||||||
|
cacheKey: '識別請求',
|
||||||
|
tmdbId: 'TMDB ID',
|
||||||
torrentTitle: '標題',
|
torrentTitle: '標題',
|
||||||
site: '站點',
|
site: '站點',
|
||||||
size: '大小',
|
size: '大小',
|
||||||
publishTime: '發布時間',
|
publishTime: '發布時間',
|
||||||
recognitionResult: '識別結果',
|
recognitionResult: '識別結果',
|
||||||
|
recognitionStatus: '識別狀態',
|
||||||
actions: '操作',
|
actions: '操作',
|
||||||
unrecognized: '未識別',
|
unrecognized: '未識別',
|
||||||
noData: '暫無緩存數據',
|
noData: '暫無緩存數據',
|
||||||
noDataHint: '點擊"刷新緩存"按鈕獲取最新的種子緩存',
|
noDataHint: '點擊"刷新緩存"按鈕獲取最新的種子緩存',
|
||||||
|
noRecognitionCache: '暫無 TheMovieDb 識別緩存',
|
||||||
|
noRecognitionCacheHint: '完成媒體識別後,緩存記錄會顯示在這裡',
|
||||||
reidentifyDialog: {
|
reidentifyDialog: {
|
||||||
title: '重新識別',
|
title: '重新識別',
|
||||||
torrentInfo: '種子信息',
|
torrentInfo: '種子信息',
|
||||||
@@ -2387,8 +2402,10 @@ export default {
|
|||||||
mediaType: {
|
mediaType: {
|
||||||
movie: '電影',
|
movie: '電影',
|
||||||
tv: '電視劇',
|
tv: '電視劇',
|
||||||
|
unknown: '未知',
|
||||||
},
|
},
|
||||||
clearConfirm: '確認清空所有緩存嗎?',
|
clearConfirm: '確認清空所有緩存嗎?',
|
||||||
|
tmdbClearConfirm: '確認清空全部 TheMovieDb 識別緩存嗎?',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
dialog: {
|
dialog: {
|
||||||
|
|||||||
+243
-86
@@ -9,13 +9,17 @@ import { useGlobalSettingsStore } from '@/stores'
|
|||||||
import { usePWA } from '@/composables/usePWA'
|
import { usePWA } from '@/composables/usePWA'
|
||||||
import { openSharedDialog } from '@/composables/useSharedDialog'
|
import { openSharedDialog } from '@/composables/useSharedDialog'
|
||||||
import { useDisplay } from 'vuetify'
|
import { useDisplay } from 'vuetify'
|
||||||
|
import TmdbRecognitionCachePanel from '@/components/cache/TmdbRecognitionCachePanel.vue'
|
||||||
|
|
||||||
const CacheReidentifyDialog = defineAsyncComponent(() => import('@/components/dialog/CacheReidentifyDialog.vue'))
|
const CacheReidentifyDialog = defineAsyncComponent(() => import('@/components/dialog/CacheReidentifyDialog.vue'))
|
||||||
|
|
||||||
type InfiniteScrollStatus = 'ok' | 'empty' | 'loading' | 'error'
|
type InfiniteScrollStatus = 'ok' | 'empty' | 'loading' | 'error'
|
||||||
|
type CacheManagerType = 'torrent' | 'tmdb'
|
||||||
|
|
||||||
const MOBILE_CACHE_PAGE_SIZE = 20
|
const MOBILE_CACHE_PAGE_SIZE = 20
|
||||||
|
|
||||||
|
const activeCacheType = ref<CacheManagerType>('torrent')
|
||||||
|
|
||||||
// 国际化
|
// 国际化
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
@@ -336,7 +340,32 @@ watch([titleFilter, siteFilter], () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section v-if="isMobile" class="cache-mobile-page">
|
<section class="cache-manager">
|
||||||
|
<header class="cache-manager__header">
|
||||||
|
<VBtnToggle
|
||||||
|
v-model="activeCacheType"
|
||||||
|
mandatory
|
||||||
|
divided
|
||||||
|
density="comfortable"
|
||||||
|
variant="text"
|
||||||
|
color="primary"
|
||||||
|
class="cache-manager__switcher"
|
||||||
|
:aria-label="t('setting.cache.cacheType')"
|
||||||
|
>
|
||||||
|
<VBtn value="torrent" prepend-icon="mdi-download-box-outline">
|
||||||
|
{{ t('setting.cache.torrentCache') }}
|
||||||
|
</VBtn>
|
||||||
|
<VBtn value="tmdb" prepend-icon="mdi-movie-search-outline">
|
||||||
|
{{ t('setting.cache.tmdbRecognitionCache') }}
|
||||||
|
</VBtn>
|
||||||
|
</VBtnToggle>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="cache-manager__content">
|
||||||
|
<TmdbRecognitionCachePanel v-if="activeCacheType === 'tmdb'" />
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<section v-if="isMobile" class="cache-mobile-page">
|
||||||
<div class="cache-mobile-stats">
|
<div class="cache-mobile-stats">
|
||||||
<div class="cache-mobile-stat cache-mobile-stat--primary">
|
<div class="cache-mobile-stat cache-mobile-stat--primary">
|
||||||
<VIcon icon="mdi-database" size="32" />
|
<VIcon icon="mdi-database" size="32" />
|
||||||
@@ -500,70 +529,55 @@ watch([titleFilter, siteFilter], () => {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div v-else>
|
<div v-else class="cache-desktop-page">
|
||||||
<!-- 工具栏统计信息和操作按钮 -->
|
<div class="cache-desktop-toolbar">
|
||||||
<VCard class="mb-4">
|
<div class="cache-desktop-stats">
|
||||||
<VCardItem>
|
<div class="cache-desktop-stat cache-desktop-stat--primary">
|
||||||
<!-- 移动端垂直布局,桌面端水平布局 -->
|
<VIcon icon="mdi-database-outline" size="22" />
|
||||||
<div class="d-flex flex-column flex-md-row align-center justify-space-between w-100 gap-4">
|
<div>
|
||||||
<!-- 左侧统计信息 -->
|
<strong>{{ cacheData.count }}</strong>
|
||||||
<div class="d-flex align-center justify-center justify-md-start gap-2 gap-md-6 w-100 w-md-auto">
|
<span>{{ t('setting.cache.totalCount') }}</span>
|
||||||
<!-- 统计信息卡片 -->
|
|
||||||
<div class="d-flex gap-2 gap-md-4 flex-wrap justify-center justify-md-start">
|
|
||||||
<VCard variant="tonal" color="primary" class="pa-2 pa-md-3 flex-grow-1 flex-md-grow-0" style="min-width: 120px;">
|
|
||||||
<div class="d-flex align-center gap-2">
|
|
||||||
<VIcon color="primary" size="small">mdi-database</VIcon>
|
|
||||||
<div>
|
|
||||||
<div class="text-h6 text-md-h6 font-weight-bold">{{ cacheData.count }}</div>
|
|
||||||
<div class="text-caption text-medium-emphasis">{{ t('setting.cache.totalCount') }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</VCard>
|
|
||||||
|
|
||||||
<VCard variant="tonal" color="success" class="pa-2 pa-md-3 flex-grow-1 flex-md-grow-0" style="min-width: 120px;">
|
|
||||||
<div class="d-flex align-center gap-2">
|
|
||||||
<VIcon color="success" size="small">mdi-web</VIcon>
|
|
||||||
<div>
|
|
||||||
<div class="text-h6 text-md-h6 font-weight-bold">{{ cacheData.sites }}</div>
|
|
||||||
<div class="text-caption text-medium-emphasis">{{ t('setting.cache.siteCount') }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</VCard>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 右侧操作按钮 -->
|
|
||||||
<div class="d-flex gap-1 gap-md-2 flex-wrap justify-center justify-md-end">
|
|
||||||
<VBtn icon color="primary" :loading="loading" @click="refreshCache" size="small">
|
|
||||||
<VIcon size="small">mdi-refresh</VIcon>
|
|
||||||
<VTooltip activator="parent" location="bottom">{{ t('setting.cache.refresh') }}</VTooltip>
|
|
||||||
</VBtn>
|
|
||||||
|
|
||||||
<VBtn
|
|
||||||
icon
|
|
||||||
color="warning"
|
|
||||||
:loading="loading"
|
|
||||||
:disabled="selectedItems.length === 0"
|
|
||||||
@click="deleteSelectedItems"
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
<VIcon size="small">mdi-delete-sweep</VIcon>
|
|
||||||
<VTooltip activator="parent" location="bottom"
|
|
||||||
>{{ t('setting.cache.deleteSelected') }} ({{ selectedItems.length }})</VTooltip
|
|
||||||
>
|
|
||||||
</VBtn>
|
|
||||||
|
|
||||||
<VBtn icon color="error" :loading="loading" @click="clearAllCache" size="small">
|
|
||||||
<VIcon size="small">mdi-delete-variant</VIcon>
|
|
||||||
<VTooltip activator="parent" location="bottom">{{ t('setting.cache.clearAll') }}</VTooltip>
|
|
||||||
</VBtn>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</VCardItem>
|
|
||||||
</VCard>
|
<div class="cache-desktop-stat cache-desktop-stat--success">
|
||||||
|
<VIcon icon="mdi-web" size="22" />
|
||||||
|
<div>
|
||||||
|
<strong>{{ cacheData.sites }}</strong>
|
||||||
|
<span>{{ t('setting.cache.siteCount') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cache-desktop-actions">
|
||||||
|
<VBtn icon variant="text" color="primary" :loading="loading" @click="refreshCache">
|
||||||
|
<VIcon icon="mdi-refresh" />
|
||||||
|
<VTooltip activator="parent" location="bottom">{{ t('setting.cache.refresh') }}</VTooltip>
|
||||||
|
</VBtn>
|
||||||
|
|
||||||
|
<VBtn
|
||||||
|
icon
|
||||||
|
variant="text"
|
||||||
|
color="warning"
|
||||||
|
:loading="loading"
|
||||||
|
:disabled="selectedItems.length === 0"
|
||||||
|
@click="deleteSelectedItems"
|
||||||
|
>
|
||||||
|
<VIcon icon="mdi-delete-sweep-outline" />
|
||||||
|
<VTooltip activator="parent" location="bottom">
|
||||||
|
{{ t('setting.cache.deleteSelected') }} ({{ selectedItems.length }})
|
||||||
|
</VTooltip>
|
||||||
|
</VBtn>
|
||||||
|
|
||||||
|
<VBtn icon variant="text" color="error" :loading="loading" @click="clearAllCache">
|
||||||
|
<VIcon icon="mdi-delete-variant" />
|
||||||
|
<VTooltip activator="parent" location="bottom">{{ t('setting.cache.clearAll') }}</VTooltip>
|
||||||
|
</VBtn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 筛选框 -->
|
<!-- 筛选框 -->
|
||||||
<VRow class="mb-4">
|
<VRow class="cache-desktop-filters">
|
||||||
<VCol cols="6">
|
<VCol cols="6">
|
||||||
<VTextField
|
<VTextField
|
||||||
v-model="titleFilter"
|
v-model="titleFilter"
|
||||||
@@ -571,6 +585,8 @@ watch([titleFilter, siteFilter], () => {
|
|||||||
prepend-inner-icon="mdi-magnify"
|
prepend-inner-icon="mdi-magnify"
|
||||||
clearable
|
clearable
|
||||||
density="compact"
|
density="compact"
|
||||||
|
variant="outlined"
|
||||||
|
hide-details
|
||||||
/>
|
/>
|
||||||
</VCol>
|
</VCol>
|
||||||
<VCol cols="6">
|
<VCol cols="6">
|
||||||
@@ -581,6 +597,8 @@ watch([titleFilter, siteFilter], () => {
|
|||||||
prepend-inner-icon="mdi-web"
|
prepend-inner-icon="mdi-web"
|
||||||
clearable
|
clearable
|
||||||
density="compact"
|
density="compact"
|
||||||
|
variant="outlined"
|
||||||
|
hide-details
|
||||||
:placeholder="t('setting.cache.selectSite')"
|
:placeholder="t('setting.cache.selectSite')"
|
||||||
/>
|
/>
|
||||||
</VCol>
|
</VCol>
|
||||||
@@ -714,15 +732,157 @@ watch([titleFilter, siteFilter], () => {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</VDataTable>
|
</VDataTable>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.cache-manager {
|
||||||
|
display: flex;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
flex-direction: column;
|
||||||
|
block-size: 100%;
|
||||||
|
inline-size: 100%;
|
||||||
|
min-block-size: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cache-manager__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
border-block-end: var(--app-surface-border);
|
||||||
|
padding: 12px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cache-manager__switcher {
|
||||||
|
overflow: hidden;
|
||||||
|
border: var(--app-surface-border);
|
||||||
|
border-radius: var(--app-control-radius);
|
||||||
|
backdrop-filter: var(--app-grouped-list-backdrop-filter);
|
||||||
|
background: var(--app-grouped-list-background);
|
||||||
|
box-shadow: var(--app-surface-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cache-manager__switcher :deep(.v-btn) {
|
||||||
|
min-inline-size: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cache-manager__switcher :deep(.v-btn__content) {
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cache-manager__content {
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
flex-direction: column;
|
||||||
|
min-block-size: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cache-desktop-page {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cache-desktop-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cache-desktop-stats {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cache-desktop-stat {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border: var(--app-surface-border);
|
||||||
|
border-radius: var(--app-surface-radius);
|
||||||
|
background: var(--app-grouped-list-background);
|
||||||
|
box-shadow: var(--app-surface-shadow);
|
||||||
|
min-block-size: 58px;
|
||||||
|
min-inline-size: 126px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cache-desktop-stat strong,
|
||||||
|
.cache-desktop-stat span {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cache-desktop-stat strong {
|
||||||
|
color: rgba(var(--v-theme-on-surface), 0.9);
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1.15;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cache-desktop-stat span {
|
||||||
|
margin-block-start: 3px;
|
||||||
|
color: rgba(var(--v-theme-on-surface), 0.58);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cache-desktop-stat--primary {
|
||||||
|
color: rgb(var(--v-theme-primary));
|
||||||
|
}
|
||||||
|
|
||||||
|
.cache-desktop-stat--success {
|
||||||
|
color: rgb(var(--v-theme-success));
|
||||||
|
}
|
||||||
|
|
||||||
|
.cache-desktop-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cache-desktop-filters {
|
||||||
|
margin-block: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cache-desktop-filters :deep(.v-field) {
|
||||||
|
border-radius: var(--app-field-radius);
|
||||||
|
background: var(--app-grouped-list-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cache-desktop-page :deep(.v-data-table) {
|
||||||
|
overflow: hidden;
|
||||||
|
border: var(--app-surface-border);
|
||||||
|
border-radius: var(--app-surface-radius);
|
||||||
|
box-shadow: var(--app-surface-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 959.98px) {
|
||||||
|
.cache-manager__header {
|
||||||
|
padding-inline: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cache-manager__switcher {
|
||||||
|
inline-size: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cache-manager__switcher :deep(.v-btn) {
|
||||||
|
flex: 1 1 0;
|
||||||
|
block-size: auto;
|
||||||
|
min-block-size: 48px;
|
||||||
|
min-inline-size: 0;
|
||||||
|
padding-block: 6px;
|
||||||
|
padding-inline: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.cache-mobile-page {
|
.cache-mobile-page {
|
||||||
--cache-mobile-control-bg: rgba(var(--v-theme-surface), 0.82);
|
--cache-mobile-control-bg: var(--app-grouped-list-background);
|
||||||
--cache-mobile-page-bg: rgb(var(--v-theme-surface));
|
--cache-mobile-page-bg: transparent;
|
||||||
--cache-mobile-surface-bg: rgba(var(--v-theme-surface), 0.94);
|
--cache-mobile-surface-bg: var(--app-grouped-list-background);
|
||||||
--cache-mobile-surface-blur: none;
|
--cache-mobile-surface-blur: var(--app-grouped-list-backdrop-filter);
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
@@ -731,7 +891,7 @@ watch([titleFilter, siteFilter], () => {
|
|||||||
block-size: 100%;
|
block-size: 100%;
|
||||||
inline-size: 100%;
|
inline-size: 100%;
|
||||||
min-block-size: 0;
|
min-block-size: 0;
|
||||||
padding: calc(8px + env(safe-area-inset-top)) 16px calc(18px + env(safe-area-inset-bottom));
|
padding: 14px 16px calc(18px + env(safe-area-inset-bottom));
|
||||||
background: var(--cache-mobile-page-bg);
|
background: var(--cache-mobile-page-bg);
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
@@ -746,7 +906,10 @@ watch([titleFilter, siteFilter], () => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
backdrop-filter: var(--cache-mobile-surface-blur);
|
backdrop-filter: var(--cache-mobile-surface-blur);
|
||||||
border-radius: 18px;
|
border: var(--app-surface-border);
|
||||||
|
border-radius: var(--app-surface-radius);
|
||||||
|
background: var(--cache-mobile-surface-bg);
|
||||||
|
box-shadow: var(--app-surface-shadow);
|
||||||
min-block-size: 92px;
|
min-block-size: 92px;
|
||||||
padding: 18px;
|
padding: 18px;
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
@@ -769,13 +932,11 @@ watch([titleFilter, siteFilter], () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.cache-mobile-stat--primary {
|
.cache-mobile-stat--primary {
|
||||||
background: linear-gradient(135deg, rgba(233, 30, 99, 0.14), rgba(233, 30, 99, 0.04));
|
color: rgb(var(--v-theme-primary));
|
||||||
color: #e91e63;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.cache-mobile-stat--success {
|
.cache-mobile-stat--success {
|
||||||
background: linear-gradient(135deg, rgba(76, 175, 80, 0.14), rgba(76, 175, 80, 0.04));
|
color: rgb(var(--v-theme-success));
|
||||||
color: #16b52b;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.cache-mobile-filters {
|
.cache-mobile-filters {
|
||||||
@@ -786,9 +947,9 @@ watch([titleFilter, siteFilter], () => {
|
|||||||
|
|
||||||
.cache-mobile-filter :deep(.v-field) {
|
.cache-mobile-filter :deep(.v-field) {
|
||||||
backdrop-filter: var(--cache-mobile-surface-blur);
|
backdrop-filter: var(--cache-mobile-surface-blur);
|
||||||
border-radius: 16px;
|
border-radius: var(--app-field-radius);
|
||||||
background: var(--cache-mobile-control-bg);
|
background: var(--cache-mobile-control-bg);
|
||||||
box-shadow: 0 6px 20px rgba(var(--v-theme-on-surface), 0.04);
|
box-shadow: var(--app-surface-shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cache-mobile-filter :deep(.v-field__outline) {
|
.cache-mobile-filter :deep(.v-field__outline) {
|
||||||
@@ -832,10 +993,10 @@ watch([titleFilter, siteFilter], () => {
|
|||||||
overflow: visible;
|
overflow: visible;
|
||||||
align-items: start;
|
align-items: start;
|
||||||
backdrop-filter: var(--cache-mobile-surface-blur);
|
backdrop-filter: var(--cache-mobile-surface-blur);
|
||||||
border: 1px solid rgba(var(--v-theme-on-surface), 0.05);
|
border: var(--app-surface-border);
|
||||||
border-radius: 16px;
|
border-radius: var(--app-surface-radius);
|
||||||
background: var(--cache-mobile-surface-bg);
|
background: var(--cache-mobile-surface-bg);
|
||||||
box-shadow: 0 10px 30px rgba(var(--v-theme-on-surface), 0.07);
|
box-shadow: var(--app-surface-shadow);
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
grid-template-columns: 72px minmax(0, 1fr);
|
grid-template-columns: 72px minmax(0, 1fr);
|
||||||
margin-block-end: 12px;
|
margin-block-end: 12px;
|
||||||
@@ -848,7 +1009,7 @@ watch([titleFilter, siteFilter], () => {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
border-radius: 9px;
|
border-radius: var(--app-control-radius);
|
||||||
background: rgba(var(--v-theme-on-surface), 0.06);
|
background: rgba(var(--v-theme-on-surface), 0.06);
|
||||||
block-size: 104px;
|
block-size: 104px;
|
||||||
color: rgba(var(--v-theme-on-surface), 0.34);
|
color: rgba(var(--v-theme-on-surface), 0.34);
|
||||||
@@ -963,15 +1124,11 @@ watch([titleFilter, siteFilter], () => {
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
html[data-theme='transparent'] .cache-mobile-page,
|
|
||||||
.v-theme--transparent .cache-mobile-page {
|
|
||||||
--cache-mobile-control-bg: rgba(var(--v-theme-surface), var(--transparent-opacity-light, 0.2));
|
|
||||||
--cache-mobile-page-bg: transparent;
|
|
||||||
--cache-mobile-surface-bg: rgba(var(--v-theme-surface), var(--transparent-opacity-light, 0.2));
|
|
||||||
--cache-mobile-surface-blur: blur(var(--transparent-blur, 10px));
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 374.98px) {
|
@media (max-width: 374.98px) {
|
||||||
|
.cache-manager__header {
|
||||||
|
padding-inline: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.cache-mobile-page {
|
.cache-mobile-page {
|
||||||
padding-inline: 12px;
|
padding-inline: 12px;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user