mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-05-30 12:49:55 +08:00
调整词表、缓存、关于功能的位置
This commit is contained in:
@@ -1,364 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { formatDateDifference } from '@/@core/utils/formatters'
|
||||
import api from '@/api'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
|
||||
// 系统环境变量
|
||||
const systemEnv = ref<any>({})
|
||||
|
||||
// 所有Release
|
||||
const allRelease = ref<any>([])
|
||||
|
||||
// 支持站点
|
||||
const supportingSites = ref<any>({})
|
||||
|
||||
// 支持站点折叠状态
|
||||
const sitesExpanded = ref(false)
|
||||
|
||||
// 去重后的支持站点
|
||||
const uniqueSupportingSites = computed(() => {
|
||||
const sitesMap = new Map()
|
||||
|
||||
Object.entries(supportingSites.value).forEach(([domain, site]: [string, any]) => {
|
||||
if (!sitesMap.has(site.name)) {
|
||||
sitesMap.set(site.name, {
|
||||
name: site.name,
|
||||
urls: [{ domain, url: site.url }],
|
||||
})
|
||||
} else {
|
||||
sitesMap.get(site.name).urls.push({ domain, url: site.url })
|
||||
}
|
||||
})
|
||||
|
||||
return Array.from(sitesMap.values())
|
||||
})
|
||||
|
||||
// 显示的支持站点(折叠时只显示前5个)
|
||||
const displayedSites = computed(() => {
|
||||
if (sitesExpanded.value) {
|
||||
return uniqueSupportingSites.value
|
||||
}
|
||||
return uniqueSupportingSites.value.slice(0, 5)
|
||||
})
|
||||
|
||||
// 变更日志对话框
|
||||
const releaseDialog = ref(false)
|
||||
|
||||
// 最新版本
|
||||
const latestRelease = ref('')
|
||||
|
||||
// 变更日志对话框标题
|
||||
const releaseDialogTitle = ref('')
|
||||
|
||||
// 变更日志对话框内容
|
||||
const releaseDialogBody = ref('')
|
||||
|
||||
// 打开日志对话框
|
||||
function showReleaseDialog(title: string, body: string) {
|
||||
releaseDialogTitle.value = title
|
||||
releaseDialogBody.value = body.replaceAll('\r\n', '<br />')
|
||||
releaseDialog.value = true
|
||||
}
|
||||
|
||||
// 查询系统环境变量
|
||||
async function querySystemEnv() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/env')
|
||||
|
||||
systemEnv.value = result.data
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 查询所有Release
|
||||
async function queryAllRelease() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/versions')
|
||||
|
||||
allRelease.value = result.data ?? []
|
||||
|
||||
// 最新版本
|
||||
if (allRelease.value.length > 0) latestRelease.value = allRelease.value[0].tag_name
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 查询支持站点
|
||||
async function querySupportingSites() {
|
||||
try {
|
||||
supportingSites.value = await api.get('site/supporting')
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 切换站点列表展开状态
|
||||
function toggleSitesExpanded() {
|
||||
sitesExpanded.value = !sitesExpanded.value
|
||||
}
|
||||
|
||||
// 计算发布时间
|
||||
function releaseTime(releaseDate: string) {
|
||||
// 上一次更新时间
|
||||
return formatDateDifference(releaseDate)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
querySystemEnv()
|
||||
queryAllRelease()
|
||||
querySupportingSites()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="px-3">
|
||||
<div class="section">
|
||||
<div>
|
||||
<h3 class="heading">{{ t('setting.about.title') }}</h3>
|
||||
</div>
|
||||
<div class="section border-t border-gray-800">
|
||||
<dl>
|
||||
<div>
|
||||
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
|
||||
<dt class="block text-sm font-bold">{{ t('setting.about.softwareVersion') }}</dt>
|
||||
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
|
||||
<span class="flex-grow flex flex-row items-center truncate">
|
||||
<code class="truncate">{{ systemEnv.VERSION }}</code>
|
||||
<a
|
||||
v-if="latestRelease === systemEnv.VERSION"
|
||||
href="https://github.com/jxxghp/MoviePilot/releases"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<span
|
||||
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full whitespace-nowrap bg-green-500 bg-opacity-80 border border-green-500 !text-green-100 ml-2 !cursor-pointer transition hover:bg-green-400"
|
||||
>
|
||||
{{ t('setting.about.latest') }}
|
||||
</span>
|
||||
</a>
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="systemEnv.FRONTEND_VERSION">
|
||||
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
|
||||
<dt class="block text-sm font-bold">{{ t('setting.about.frontendVersion') }}</dt>
|
||||
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
|
||||
<span class="flex-grow flex flex-row items-center truncate">
|
||||
<code class="truncate">{{ systemEnv.FRONTEND_VERSION }}</code>
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
|
||||
<dt class="block text-sm font-bold">{{ t('setting.about.authVersion') }}</dt>
|
||||
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
|
||||
<span class="flex-grow flex flex-row items-center truncate">
|
||||
<code class="truncate">{{ systemEnv.AUTH_VERSION }}</code>
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
|
||||
<dt class="block text-sm font-bold">{{ t('setting.about.indexerVersion') }}</dt>
|
||||
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
|
||||
<span class="flex-grow flex flex-row items-center truncate">
|
||||
<code class="truncate">{{ systemEnv.INDEXER_VERSION }}</code>
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
|
||||
<dt class="block text-sm font-bold">{{ t('setting.about.configDir') }}</dt>
|
||||
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
|
||||
<span class="flex-grow undefined">
|
||||
<code>{{ systemEnv.CONFIG_DIR }}</code>
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
|
||||
<dt class="block text-sm font-bold">{{ t('setting.about.dataDir') }}</dt>
|
||||
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
|
||||
<span class="flex-grow undefined"
|
||||
><code>{{ t('setting.about.dataDirectory') }}</code></span
|
||||
>
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
|
||||
<dt class="block text-sm font-bold">{{ t('setting.about.timezone') }}</dt>
|
||||
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
|
||||
<span class="flex-grow undefined">
|
||||
<code>{{ systemEnv.TZ }}</code>
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
|
||||
<dt class="block text-sm font-bold">{{ t('setting.about.supportingSites') }}</dt>
|
||||
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-wrap gap-2 mt-1 ms-1">
|
||||
<VChip v-for="site in displayedSites" :key="site.name" variant="outlined" size="small">
|
||||
<span class="truncate max-w-32">{{ site.name }}</span>
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="!sitesExpanded && uniqueSupportingSites.length > 5"
|
||||
variant="tonal"
|
||||
size="small"
|
||||
@click="toggleSitesExpanded"
|
||||
>
|
||||
<span> {{ uniqueSupportingSites.length }}+ ...</span>
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="sitesExpanded && uniqueSupportingSites.length > 5"
|
||||
variant="tonal"
|
||||
size="small"
|
||||
@click="toggleSitesExpanded"
|
||||
>
|
||||
<span>< {{ t('setting.about.collapse') }}</span>
|
||||
</VChip>
|
||||
</div>
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<div>
|
||||
<h3 class="heading">{{ t('setting.about.support') }}</h3>
|
||||
</div>
|
||||
<div class="section border-t border-gray-800">
|
||||
<dl>
|
||||
<div>
|
||||
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
|
||||
<dt class="block text-sm font-bold">{{ t('setting.about.documentation') }}</dt>
|
||||
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
|
||||
<span class="flex-grow undefined">
|
||||
<a
|
||||
href="https://movie-pilot.org"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
class="text-indigo-500 transition duration-300 hover:underline"
|
||||
>
|
||||
https://movie-pilot.org
|
||||
</a>
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
|
||||
<dt class="block text-sm font-bold">{{ t('setting.about.feedback') }}</dt>
|
||||
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
|
||||
<span class="flex-grow undefined">
|
||||
<a
|
||||
href="https://github.com/jxxghp/MoviePilot/issues/new/choose"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
class="text-indigo-500 transition duration-300 hover:underline"
|
||||
>
|
||||
https://github.com/jxxghp/MoviePilot/issues/new/choose
|
||||
</a>
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
|
||||
<dt class="block text-sm font-bold">{{ t('setting.about.channel') }}</dt>
|
||||
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
|
||||
<span class="flex-grow undefined">
|
||||
<a
|
||||
href="https://t.me/moviepilot_channel"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
class="text-indigo-500 transition duration-300 hover:underline"
|
||||
>
|
||||
https://t.me/moviepilot_channel
|
||||
</a>
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<div>
|
||||
<h3 class="heading">{{ t('setting.about.versions') }}</h3>
|
||||
<div class="section space-y-3">
|
||||
<div>
|
||||
<div
|
||||
v-for="release in allRelease"
|
||||
:key="release.tag_name"
|
||||
class="mb-3 flex w-full flex-col space-y-3 rounded-md px-4 py-2 ring-1 ring-gray-400 sm:flex-row sm:space-y-0 sm:space-x-3"
|
||||
>
|
||||
<div class="flex w-full flex-grow items-center justify-start space-x-2 truncate sm:justify-start">
|
||||
<span class="truncate text-lg font-bold">
|
||||
<span class="mr-2 whitespace-nowrap text-xs font-normal">{{
|
||||
releaseTime(release.published_at)
|
||||
}}</span>
|
||||
{{ release.tag_name }}
|
||||
</span>
|
||||
<span
|
||||
v-if="release.tag_name === latestRelease"
|
||||
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full whitespace-nowrap cursor-default bg-green-500 bg-opacity-80 border border-green-500 !text-green-100"
|
||||
>
|
||||
{{ t('setting.about.latestVersion') }}
|
||||
</span>
|
||||
<span
|
||||
v-if="release.tag_name === systemEnv.VERSION"
|
||||
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full whitespace-nowrap cursor-default bg-indigo-500 bg-opacity-80 border border-indigo-500 !text-indigo-100"
|
||||
>
|
||||
{{ t('setting.about.currentVersion') }}
|
||||
</span>
|
||||
</div>
|
||||
<VBtn @click.stop="showReleaseDialog(release.tag_name, release.body)">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-text-box-outline" />
|
||||
</template>
|
||||
{{ t('setting.about.viewChangelog') }}
|
||||
</VBtn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<VDialog v-if="releaseDialog" v-model="releaseDialog" width="600" scrollable>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VDialogCloseBtn @click="releaseDialog = false" />
|
||||
<VCardTitle>{{ releaseDialogTitle }} {{ t('setting.about.changelog') }}</VCardTitle>
|
||||
</VCardItem>
|
||||
<VCardText v-html="releaseDialogBody" />
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style type="scss" scoped>
|
||||
.heading {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
line-height: 2rem;
|
||||
|
||||
--tw-text-opacity: 1;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-block: 0.5rem 2.5rem;
|
||||
}
|
||||
</style>
|
||||
@@ -1,472 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { useToast } from 'vue-toastification'
|
||||
import api from '@/api'
|
||||
import type { TorrentCacheData, TorrentCacheItem } from '@/api/types'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { formatFileSize, formatDateDifference } from '@core/utils/formatters'
|
||||
import { useConfirm } from '@/composables/useConfirm'
|
||||
import { useGlobalSettingsStore } from '@/stores'
|
||||
import { usePWA } from '@/composables/usePWA'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
|
||||
// PWA模式检测
|
||||
const { appMode } = usePWA()
|
||||
|
||||
// 全局设置
|
||||
const globalSettingsStore = useGlobalSettingsStore()
|
||||
const globalSettings = globalSettingsStore.globalSettings
|
||||
|
||||
// 确认框
|
||||
const createConfirm = useConfirm()
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
// 缓存数据
|
||||
const cacheData = ref<TorrentCacheData>({
|
||||
count: 0,
|
||||
sites: 0,
|
||||
data: [],
|
||||
})
|
||||
|
||||
// 筛选条件
|
||||
const titleFilter = ref<string | null>(null)
|
||||
const siteFilter = ref<string | null>(null)
|
||||
|
||||
// 获取所有站点选项
|
||||
const siteOptions = computed(() => {
|
||||
const sites = new Set<string>()
|
||||
cacheData.value.data.forEach(item => {
|
||||
if (item.site_name) {
|
||||
sites.add(item.site_name)
|
||||
}
|
||||
})
|
||||
return Array.from(sites).sort()
|
||||
})
|
||||
|
||||
// 筛选后的数据
|
||||
const filteredData = computed(() => {
|
||||
return cacheData.value.data.filter(item => {
|
||||
const titleMatch = !titleFilter.value || item.title?.toLowerCase().includes(titleFilter.value?.toLowerCase())
|
||||
const siteMatch = !siteFilter.value || item.site_name === siteFilter.value
|
||||
return titleMatch && siteMatch
|
||||
})
|
||||
})
|
||||
|
||||
// 选中的缓存项
|
||||
const selectedItems = ref<string[]>([])
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
|
||||
// 重新识别对话框
|
||||
const reidentifyDialog = ref(false)
|
||||
const currentReidentifyItem = ref<TorrentCacheItem | null>(null)
|
||||
const tmdbId = ref<number | undefined>()
|
||||
const doubanId = ref<string | undefined>()
|
||||
|
||||
const tableStyle = computed(() => {
|
||||
return appMode ? '' : 'height: calc(100vh - 21rem - env(safe-area-inset-bottom)'
|
||||
})
|
||||
|
||||
// 调用API加载缓存数据
|
||||
async function loadCacheData() {
|
||||
try {
|
||||
loading.value = true
|
||||
const res: any = await api.get('torrent/cache')
|
||||
cacheData.value = res.data
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
$toast.error(t('setting.cache.loadFailed'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 清空所有缓存
|
||||
async function clearAllCache() {
|
||||
const isConfirmed = await createConfirm({
|
||||
type: 'warn',
|
||||
title: t('common.confirm'),
|
||||
content: t('setting.cache.clearConfirm'),
|
||||
})
|
||||
|
||||
if (!isConfirmed) return
|
||||
try {
|
||||
loading.value = true
|
||||
await api.delete('torrent/cache')
|
||||
$toast.success(t('setting.cache.clearSuccess'))
|
||||
await loadCacheData()
|
||||
selectedItems.value = []
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
$toast.error(t('setting.cache.clearFailed'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新缓存
|
||||
async function refreshCache() {
|
||||
try {
|
||||
loading.value = true
|
||||
const res: any = await api.post('torrent/cache/refresh')
|
||||
$toast.success(res.message || t('setting.cache.refreshSuccess'))
|
||||
await loadCacheData()
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
$toast.error(t('setting.cache.refreshFailed'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 删除选中的缓存项
|
||||
async function deleteSelectedItems() {
|
||||
if (selectedItems.value.length === 0) {
|
||||
$toast.warning(t('setting.cache.selectDeleteWarning'))
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
loading.value = true
|
||||
const deletePromises = selectedItems.value.map(hash => {
|
||||
const item = cacheData.value.data.find(d => d.hash === hash)
|
||||
if (item) {
|
||||
return api.delete(`torrent/cache/${item.domain}/${hash}`)
|
||||
}
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
await Promise.all(deletePromises)
|
||||
$toast.success(t('setting.cache.deleteSelectedSuccess', { count: selectedItems.value.length }))
|
||||
await loadCacheData()
|
||||
selectedItems.value = []
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
$toast.error(t('setting.cache.deleteSelectedFailed'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 删除单个缓存项
|
||||
async function deleteSingleItem(item: TorrentCacheItem) {
|
||||
try {
|
||||
loading.value = true
|
||||
await api.delete(`torrent/cache/${item.domain}/${item.hash}`)
|
||||
$toast.success(t('setting.cache.deleteSuccess'))
|
||||
await loadCacheData()
|
||||
// 从选中列表中移除
|
||||
const index = selectedItems.value.indexOf(item.hash)
|
||||
if (index > -1) {
|
||||
selectedItems.value.splice(index, 1)
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
$toast.error(t('setting.cache.deleteFailed'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 打开重新识别对话框
|
||||
function openReidentifyDialog(item: TorrentCacheItem) {
|
||||
currentReidentifyItem.value = item
|
||||
tmdbId.value = undefined
|
||||
doubanId.value = undefined
|
||||
reidentifyDialog.value = true
|
||||
}
|
||||
|
||||
// 重新识别
|
||||
async function performReidentify() {
|
||||
if (!currentReidentifyItem.value) return
|
||||
|
||||
try {
|
||||
loading.value = true
|
||||
const params: any = {}
|
||||
if (tmdbId.value) params.tmdbid = tmdbId.value
|
||||
if (doubanId.value) params.doubanid = doubanId.value
|
||||
|
||||
const res: any = await api.post(
|
||||
`torrent/cache/reidentify/${currentReidentifyItem.value.domain}/${currentReidentifyItem.value.hash}`,
|
||||
null,
|
||||
{
|
||||
params,
|
||||
},
|
||||
)
|
||||
|
||||
$toast.success(res.message || t('setting.cache.reidentifySuccess'))
|
||||
await loadCacheData()
|
||||
reidentifyDialog.value = false
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
$toast.error(t('setting.cache.reidentifyFailed'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取媒体类型颜色
|
||||
function getMediaTypeColor(type: string): string {
|
||||
switch (type) {
|
||||
case t('setting.cache.mediaType.movie'):
|
||||
return 'primary'
|
||||
case t('setting.cache.mediaType.tv'):
|
||||
return 'success'
|
||||
default:
|
||||
return 'default'
|
||||
}
|
||||
}
|
||||
|
||||
// 打开详情页面
|
||||
function openPageUrl(url: string) {
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadCacheData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>{{ t('setting.cache.title') }}</VCardTitle>
|
||||
<VCardSubtitle>{{ t('setting.cache.subtitle') }}</VCardSubtitle>
|
||||
|
||||
<template #append>
|
||||
<div class="d-flex gap-2">
|
||||
<VBtn icon color="primary" :loading="loading" @click="refreshCache">
|
||||
<VIcon>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"
|
||||
>
|
||||
<VIcon>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">
|
||||
<VIcon>mdi-delete-variant</VIcon>
|
||||
<VTooltip activator="parent" location="bottom">{{ t('setting.cache.clearAll') }}</VTooltip>
|
||||
</VBtn>
|
||||
</div>
|
||||
</template>
|
||||
</VCardItem>
|
||||
|
||||
<!-- 筛选框 -->
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol cols="6">
|
||||
<VTextField
|
||||
v-model="titleFilter"
|
||||
:label="t('setting.cache.filterByTitle')"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
clearable
|
||||
density="compact"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6">
|
||||
<VAutocomplete
|
||||
v-model="siteFilter"
|
||||
:label="t('setting.cache.filterBySite')"
|
||||
:items="siteOptions"
|
||||
prepend-inner-icon="mdi-web"
|
||||
clearable
|
||||
density="compact"
|
||||
:placeholder="t('setting.cache.selectSite')"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
|
||||
<!-- 缓存列表 -->
|
||||
<VDataTable
|
||||
v-model="selectedItems"
|
||||
:headers="[
|
||||
{ title: '', key: 'data-table-select', sortable: false, width: '48px' },
|
||||
{ title: t('setting.cache.poster'), key: 'poster', sortable: false, width: '80px' },
|
||||
{ title: t('setting.cache.torrentTitle'), key: 'title', sortable: true },
|
||||
{ title: t('setting.cache.site'), key: 'site_name', sortable: true, width: '120px' },
|
||||
{ title: t('setting.cache.size'), key: 'size', sortable: true, width: '100px' },
|
||||
{ title: t('setting.cache.publishTime'), key: 'pubdate', sortable: true, width: '150px' },
|
||||
{ title: t('setting.cache.recognitionResult'), key: 'media_info', sortable: false, width: '200px' },
|
||||
{ title: t('setting.cache.actions'), key: 'actions', sortable: false, width: '150px' },
|
||||
]"
|
||||
:items="filteredData"
|
||||
:loading="loading"
|
||||
item-value="hash"
|
||||
show-select
|
||||
hover
|
||||
fixed-header
|
||||
:items-per-page-text="t('common.itemsPerPage')"
|
||||
:no-data-text="t('common.noDataText')"
|
||||
:loading-text="t('common.loadingText')"
|
||||
:style="tableStyle"
|
||||
>
|
||||
<!-- 全选复选框 -->
|
||||
<template #header.data-table-select="{ allSelected, selectAll, someSelected }">
|
||||
<VCheckbox
|
||||
:indeterminate="someSelected && !allSelected"
|
||||
:model-value="allSelected"
|
||||
@update:model-value="(value: boolean | null) => selectAll(value as boolean)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- 海报列 -->
|
||||
<template #item.poster="{ item }">
|
||||
<div class="text-center">
|
||||
<VImg
|
||||
v-if="item.poster_path"
|
||||
:src="item.poster_path"
|
||||
:alt="item.media_name || item.title"
|
||||
cover
|
||||
rounded="md"
|
||||
class="w-12 my-1 ms-auto"
|
||||
/>
|
||||
<VIcon v-else size="x-large" color="grey-lighten-1">
|
||||
{{ item.media_type === 'movie' ? 'mdi-movie-open' : 'mdi-television-play' }}
|
||||
</VIcon>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 标题列 -->
|
||||
<template #item.title="{ item }">
|
||||
<div class="d-flex flex-column min-w-40">
|
||||
<div class="text-subtitle-2 font-weight-bold">
|
||||
{{ item.title }}
|
||||
</div>
|
||||
<div v-if="item.description" class="text-caption text-grey">
|
||||
{{ item.description }}
|
||||
</div>
|
||||
<div v-if="item.season_episode || item.resource_term" class="text-caption text-primary mt-1">
|
||||
{{ item.season_episode }} {{ item.resource_term }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 大小列 -->
|
||||
<template #item.size="{ item }">
|
||||
{{ formatFileSize(item.size) }}
|
||||
</template>
|
||||
|
||||
<!-- 发布时间列 -->
|
||||
<template #item.pubdate="{ item }">
|
||||
{{ formatDateDifference(item.pubdate || '') }}
|
||||
</template>
|
||||
|
||||
<!-- 识别结果列 -->
|
||||
<template #item.media_info="{ item }">
|
||||
<div v-if="item.media_name" class="d-flex flex-column">
|
||||
<div class="text-subtitle-2">
|
||||
{{ item.media_name }}
|
||||
<span v-if="item.media_year" class="text-caption text-grey"> ({{ item.media_year }}) </span>
|
||||
</div>
|
||||
<div>
|
||||
<VChip v-if="item.media_type" :color="getMediaTypeColor(item.media_type)" size="x-small">
|
||||
{{ item.media_type }}
|
||||
</VChip>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-caption text-grey">
|
||||
{{ t('setting.cache.unrecognized') }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 操作列 -->
|
||||
<template #item.actions="{ item }">
|
||||
<div class="d-flex gap-1">
|
||||
<VBtn icon size="small" color="primary" variant="text" @click="openReidentifyDialog(item)">
|
||||
<VIcon size="16">mdi-text-recognition</VIcon>
|
||||
</VBtn>
|
||||
|
||||
<VBtn icon size="small" color="error" variant="text" @click="deleteSingleItem(item)">
|
||||
<VIcon size="16">mdi-delete</VIcon>
|
||||
</VBtn>
|
||||
|
||||
<VBtn
|
||||
v-if="item.page_url"
|
||||
icon
|
||||
size="small"
|
||||
color="info"
|
||||
variant="text"
|
||||
@click="openPageUrl(item.page_url || '')"
|
||||
target="_blank"
|
||||
>
|
||||
<VIcon size="16">mdi-open-in-new</VIcon>
|
||||
</VBtn>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<template #no-data>
|
||||
<div class="text-center pa-4">
|
||||
<VIcon size="64" class="mb-4"> mdi-database-off </VIcon>
|
||||
<div class="text-body-2 text-grey">
|
||||
{{ t('setting.cache.noData') }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</VDataTable>
|
||||
</VCard>
|
||||
|
||||
<!-- 重新识别对话框 -->
|
||||
<VDialog v-model="reidentifyDialog" scrollable max-width="35rem">
|
||||
<VCard>
|
||||
<VCardItem class="py-2">
|
||||
<template #prepend>
|
||||
<VIcon>mdi-text-recognition</VIcon>
|
||||
</template>
|
||||
<VCardTitle>{{ t('setting.cache.reidentifyDialog.title') }}</VCardTitle>
|
||||
<VCardSubtitle>{{ currentReidentifyItem?.title }}</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VDialogCloseBtn @click="reidentifyDialog = false" />
|
||||
<VDivider />
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
v-if="globalSettings.RECOGNIZE_SOURCE === 'themoviedb'"
|
||||
v-model="tmdbId"
|
||||
:label="t('setting.cache.reidentifyDialog.tmdbId')"
|
||||
:hint="t('setting.cache.reidentifyDialog.tmdbIdHint')"
|
||||
clearable
|
||||
prepend-inner-icon="mdi-id-card"
|
||||
persistent-hint
|
||||
/>
|
||||
<VTextField
|
||||
v-else
|
||||
v-model="doubanId"
|
||||
:label="t('setting.cache.reidentifyDialog.doubanId')"
|
||||
:hint="t('setting.cache.reidentifyDialog.doubanIdHint')"
|
||||
clearable
|
||||
prepend-inner-icon="mdi-id-card"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VAlert type="info" variant="tonal" class="mt-4">
|
||||
{{ t('setting.cache.reidentifyDialog.autoHint') }}
|
||||
</VAlert>
|
||||
</VCardText>
|
||||
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
<VBtn color="primary" :loading="loading" prepend-icon="mdi-check" @click="performReidentify">
|
||||
{{ t('setting.cache.reidentifyDialog.confirm') }}
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
@@ -1,250 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { useToast } from 'vue-toastification'
|
||||
import api from '@/api'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
// 自定义识别词
|
||||
const customIdentifiers = ref('')
|
||||
|
||||
// 自定义制作组
|
||||
const customReleaseGroups = ref('')
|
||||
|
||||
// 自定义占位符
|
||||
const customization = ref('')
|
||||
|
||||
// 文件整理屏蔽词
|
||||
const transferExcludeWords = ref('')
|
||||
|
||||
// 查询已设置的识别词
|
||||
async function queryCustomIdentifiers() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/setting/CustomIdentifiers')
|
||||
if (result && result.data && result.data.value) customIdentifiers.value = result.data.value.join('\n')
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 查询已设置的制作组
|
||||
async function queryCustomReleaseGroups() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/setting/CustomReleaseGroups')
|
||||
if (result && result.data && result.data.value) customReleaseGroups.value = result.data.value.join('\n')
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 查询已设置的自定义占位符
|
||||
async function queryCustomization() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/setting/Customization')
|
||||
if (result && result.data && result.data.value) customization.value = result.data?.value.join('\n')
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 查询已设置的屏蔽词
|
||||
async function queryTransferExcludeWords() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/setting/TransferExcludeWords')
|
||||
if (result && result.data && result.data.value) transferExcludeWords.value = result.data?.value.join('\n')
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存用户设置的识别词
|
||||
async function saveCustomIdentifiers() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post(
|
||||
'system/setting/CustomIdentifiers',
|
||||
customIdentifiers.value.split('\n'),
|
||||
)
|
||||
|
||||
if (result.success) $toast.success(t('setting.words.identifierSaveSuccess'))
|
||||
else $toast.error(t('setting.words.identifierSaveFailed'))
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存自定义制作组
|
||||
async function saveCustomReleaseGroups() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post(
|
||||
'system/setting/CustomReleaseGroups',
|
||||
customReleaseGroups.value.split('\n'),
|
||||
)
|
||||
|
||||
if (result.success) $toast.success(t('setting.words.releaseGroupSaveSuccess'))
|
||||
else $toast.error(t('setting.words.releaseGroupSaveFailed'))
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存自定义占位符
|
||||
async function saveCustomization() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post(
|
||||
'system/setting/Customization',
|
||||
customization.value.split('\n'),
|
||||
)
|
||||
|
||||
if (result.success) $toast.success(t('setting.words.customizationSaveSuccess'))
|
||||
else $toast.error(t('setting.words.customizationSaveFailed'))
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存文件整理屏蔽词
|
||||
async function saveTransferExcludeWords() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post(
|
||||
'system/setting/TransferExcludeWords',
|
||||
transferExcludeWords.value.split('\n'),
|
||||
)
|
||||
|
||||
if (result.success) $toast.success(t('setting.words.excludeWordsSaveSuccess'))
|
||||
else $toast.error(t('setting.words.excludeWordsSaveFailed'))
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
queryCustomIdentifiers()
|
||||
queryCustomReleaseGroups()
|
||||
queryCustomization()
|
||||
queryTransferExcludeWords()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>{{ t('setting.words.customIdentifiers') }}</VCardTitle>
|
||||
<VCardSubtitle>{{ t('setting.words.identifiersDesc') }}</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VTextarea
|
||||
v-model="customIdentifiers"
|
||||
:placeholder="t('setting.words.identifiersPlaceholder')"
|
||||
:hint="t('setting.words.identifiersHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-tag-text"
|
||||
/>
|
||||
</VCardText>
|
||||
<VCardText>
|
||||
<VAlert type="info" variant="tonal" :title="t('setting.words.formatTitle')">
|
||||
<div style="white-space: pre-line" v-html="t('setting.words.formatContent').split('\n').join('<br>')"></div>
|
||||
</VAlert>
|
||||
</VCardText>
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<div class="d-flex flex-wrap gap-4 mt-4">
|
||||
<VBtn type="submit" @click="saveCustomIdentifiers" prepend-icon="mdi-content-save">
|
||||
{{ t('common.save') }}
|
||||
</VBtn>
|
||||
</div>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>{{ t('setting.words.customReleaseGroups') }}</VCardTitle>
|
||||
<VCardSubtitle>{{ t('setting.words.releaseGroupsDesc') }}</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VTextarea
|
||||
v-model="customReleaseGroups"
|
||||
:placeholder="t('setting.words.releaseGroupsPlaceholder')"
|
||||
:hint="t('setting.words.releaseGroupsHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-account-group"
|
||||
/>
|
||||
</VCardText>
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<div class="d-flex flex-wrap gap-4 mt-4">
|
||||
<VBtn type="submit" @click="saveCustomReleaseGroups" prepend-icon="mdi-content-save">
|
||||
{{ t('common.save') }}
|
||||
</VBtn>
|
||||
</div>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>{{ t('setting.words.customization') }}</VCardTitle>
|
||||
<VCardSubtitle>{{ t('setting.words.customizationDesc') }}</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VTextarea
|
||||
v-model="customization"
|
||||
:placeholder="t('setting.words.customizationPlaceholder')"
|
||||
:hint="t('setting.words.customizationHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-code-braces"
|
||||
/>
|
||||
</VCardText>
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<div class="d-flex flex-wrap gap-4 mt-4">
|
||||
<VBtn type="submit" @click="saveCustomization" prepend-icon="mdi-content-save">
|
||||
{{ t('common.save') }}
|
||||
</VBtn>
|
||||
</div>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>{{ t('setting.words.transferExcludeWords') }}</VCardTitle>
|
||||
<VCardSubtitle>{{ t('setting.words.excludeWordsDesc') }}</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VTextarea
|
||||
v-model="transferExcludeWords"
|
||||
:placeholder="t('setting.words.excludeWordsPlaceholder')"
|
||||
:hint="t('setting.words.excludeWordsHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-block-helper"
|
||||
/>
|
||||
</VCardText>
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<div class="d-flex flex-wrap gap-4 mt-4">
|
||||
<VBtn type="submit" @click="saveTransferExcludeWords" prepend-icon="mdi-content-save">
|
||||
{{ t('common.save') }}
|
||||
</VBtn>
|
||||
</div>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</template>
|
||||
Reference in New Issue
Block a user