Compare commits

...

23 Commits

Author SHA1 Message Date
jxxghp
633b38da01 fix dashboard 2023-09-04 11:07:56 +08:00
jxxghp
68a4818be0 build 2023-09-04 07:08:00 +08:00
jxxghp
be3e4a7b13 fix user menu 2023-09-04 07:07:38 +08:00
jxxghp
2bc616ebbb px => rem 2023-09-03 22:36:48 +08:00
jxxghp
7058472784 feat 媒体信息组件 2023-09-03 19:25:46 +08:00
jxxghp
8d9f28b3c8 fix ui 2023-09-03 19:15:58 +08:00
jxxghp
0d2bba78d9 UI 1.1.3 2023-09-03 18:39:14 +08:00
jxxghp
23a62d33eb feat 过滤规则测试 2023-09-03 18:36:43 +08:00
jxxghp
93a2a4a772 fix logging ui 2023-09-03 15:42:01 +08:00
jxxghp
38e74f0c1b fix build 2023-09-03 13:16:56 +08:00
jxxghp
d2d6ca75be feat 文件管理添加快速识别按钮 2023-09-03 13:01:18 +08:00
jxxghp
a4a9f9e7c5 更新 package.json 2023-09-03 09:53:14 +08:00
jxxghp
4d5d1094ed feat 支持设置转移屏蔽词 2023-09-03 09:22:25 +08:00
jxxghp
9f8eaa5722 feat 实时日志放快捷栏&&美化日志界面 2023-09-03 09:15:42 +08:00
jxxghp
79e07d2b3d fix color schema 2023-09-02 09:58:28 +08:00
jxxghp
822d457bff v1.1.1 2023-09-02 08:23:04 +08:00
jxxghp
8e391af0b4 Merge pull request #33 from amtoaer/main 2023-09-02 06:42:38 +08:00
amtoaer
b522b4d355 fix: 修复 tmdb 未记录发行日期的剧集全部堆叠在今天的问题 2023-09-02 00:36:06 +08:00
jxxghp
9c5ef8f5b4 fix nodatafound page 2023-09-01 17:17:07 +08:00
jxxghp
4dc1ad35d2 feat 历史记录批量操作 2023-09-01 10:54:40 +08:00
jxxghp
9b405d9e59 Merge pull request #31 from thofx/fix_28 2023-08-31 22:36:47 +08:00
thofx
2351ec7b85 fix: 筛选出现的bug #28 2023-08-31 22:34:39 +08:00
jxxghp
6fc3228334 fix color 2023-08-31 19:17:08 +08:00
30 changed files with 1057 additions and 301 deletions

View File

@@ -1,6 +1,6 @@
{ {
"name": "moviepilot", "name": "moviepilot",
"version": "1.0.10", "version": "1.1.3-3",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite --host", "dev": "vite --host",
@@ -59,6 +59,7 @@
"@iconify/vue": "4.1.1", "@iconify/vue": "4.1.1",
"@intlify/unplugin-vue-i18n": "^0.10.0", "@intlify/unplugin-vue-i18n": "^0.10.0",
"@tailwindcss/aspect-ratio": "^0.4.2", "@tailwindcss/aspect-ratio": "^0.4.2",
"@types/lodash": "^4.14.197",
"@types/node": "^20.1.4", "@types/node": "^20.1.4",
"@types/webfontloader": "^1.6.34", "@types/webfontloader": "^1.6.34",
"@typescript-eslint/eslint-plugin": "^5.59.5", "@typescript-eslint/eslint-plugin": "^5.59.5",

View File

@@ -50,7 +50,7 @@
rgba(#{variables.$vertical-nav-background-color-rgb}, 30%) 75%, rgba(#{variables.$vertical-nav-background-color-rgb}, 30%) 75%,
transparent transparent
); );
block-size: calc(env(safe-area-inset-top) + 64px); block-size: calc(env(safe-area-inset-top) + 4rem);
inline-size: 100%; inline-size: 100%;
inset-block-start: calc(#{variables.$vertical-nav-header-height} - 2px); inset-block-start: calc(#{variables.$vertical-nav-header-height} - 2px);
opacity: 0; opacity: 0;

View File

@@ -88,9 +88,9 @@ export function formatSeconds(seconds: number) {
} }
// YYYY-MM-DD 转化为Date // YYYY-MM-DD 转化为Date
export function parseDate(dateString: string): Date { export function parseDate(dateString: string): Date | null {
if (!dateString) if (!dateString)
return new Date() return null
const [year, month, day] = dateString.split('-').map(Number) const [year, month, day] = dateString.split('-').map(Number)
return new Date(year, month - 1, day) return new Date(year, month - 1, day)

View File

@@ -2,15 +2,15 @@
// 👉 Vertical nav // 👉 Vertical nav
$layout-vertical-nav-z-index: 12 !default; $layout-vertical-nav-z-index: 12 !default;
$layout-vertical-nav-width: 260px !default; $layout-vertical-nav-width: 16.25rem !default;
$layout-vertical-nav-collapsed-width: 80px !default; $layout-vertical-nav-collapsed-width: 80px !default;
// 👉 Horizontal nav // 👉 Horizontal nav
$layout-horizontal-nav-z-index: 11 !default; $layout-horizontal-nav-z-index: 11 !default;
$layout-horizontal-nav-navbar-height: 64px !default; $layout-horizontal-nav-navbar-height: 4rem !default;
// 👉 Navbar // 👉 Navbar
$layout-vertical-nav-navbar-height: 64px !default; $layout-vertical-nav-navbar-height: 4rem !default;
$layout-vertical-nav-navbar-is-contained: true !default; $layout-vertical-nav-navbar-is-contained: true !default;
$layout-vertical-nav-layout-navbar-z-index: 11 !default; $layout-vertical-nav-layout-navbar-z-index: 11 !default;
$layout-horizontal-nav-layout-navbar-z-index: 11 !default; $layout-horizontal-nav-layout-navbar-z-index: 11 !default;
@@ -19,7 +19,7 @@ $layout-horizontal-nav-layout-navbar-z-index: 11 !default;
$layout-boxed-content-width: 1440px !default; $layout-boxed-content-width: 1440px !default;
// 👉Footer // 👉Footer
$layout-vertical-nav-footer-height: 56px !default; $layout-vertical-nav-footer-height: 3.5rem !default;
// 👉 Layout overlay // 👉 Layout overlay
$layout-overlay-z-index: 11 !default; $layout-overlay-z-index: 11 !default;

View File

@@ -64,6 +64,7 @@ async function loadAccountInfo() {
// 页面加载时,加载当前用户数据 // 页面加载时,加载当前用户数据
onBeforeMount(async () => { onBeforeMount(async () => {
await loadAccountInfo() await loadAccountInfo()
console.log('accountInfo', accountInfo.value)
startSSEMessager() startSSEMessager()
}) })

View File

@@ -1,18 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { useTheme } from 'vuetify'
import miscpose from '@images/pages/pose-fs-9.png' import miscpose from '@images/pages/pose-fs-9.png'
import miscMaskDark from '@images/pages/misc-mask-dark.png'
import miscMaskLight from '@images/pages/misc-mask-light.png'
import tree from '@images/pages/tree.png'
const props = defineProps<Props>() const props = defineProps<Props>()
const vuetifyTheme = useTheme()
const authThemeMask = computed(() => {
return vuetifyTheme.global.name.value === 'light' ? miscMaskLight : miscMaskDark
})
interface Props { interface Props {
errorCode?: string errorCode?: string
errorTitle?: string errorTitle?: string
@@ -21,7 +11,7 @@ interface Props {
</script> </script>
<template> <template>
<div class="misc-wrapper"> <div class="flex flex-col">
<ErrorHeader <ErrorHeader
:error-code="props.errorCode" :error-code="props.errorCode"
:error-title="props.errorTitle" :error-title="props.errorTitle"
@@ -29,7 +19,7 @@ interface Props {
/> />
<!-- 👉 Image --> <!-- 👉 Image -->
<div class="misc-avatar text-center"> <div class="text-center">
<VImg <VImg
:src="miscpose" :src="miscpose"
class="mx-auto pt-10" class="mx-auto pt-10"
@@ -38,40 +28,8 @@ interface Props {
/> />
<slot name="button" /> <slot name="button" />
</div> </div>
<!-- 👉 Footer -->
<VImg
:src="tree"
class="misc-footer-tree d-none d-lg-block"
/>
<VImg
:src="authThemeMask"
class="misc-footer-img d-none d-md-block"
/>
</div> </div>
</template> </template>
<style lang="scss"> <style lang="scss">
@use "@configured-variables" as variables;
@use '@core/scss/pages/misc.scss';
.misc-wrapper {
position: relative;
.misc-footer-tree {
position: fixed;
z-index: 1;
inline-size: 15.625rem;
inset-block-end: 3.5rem;
inset-inline-start: 0.375rem;
left: variables.$layout-vertical-nav-width;
}
.misc-footer-img {
position: fixed;
inline-size: 100%;
inset-block-end: 0;
}
}
</style> </style>

View File

@@ -462,7 +462,7 @@ const getImgUrl: Ref<string> = computed(() => {
</VHover> </VHover>
<VDialog <VDialog
v-model="subscribeSeasonDialog" v-model="subscribeSeasonDialog"
max-width="600" max-width="50rem"
content-class="whitespace-nowrap" content-class="whitespace-nowrap"
scrollable scrollable
> >

View File

@@ -0,0 +1,147 @@
<script lang="ts" setup>
import type { PropType } from 'vue'
import type { Context } from '@/api/types'
// 输入参数
const props = defineProps({
context: Object as PropType<Context>,
})
// TMDB图片转换为w500大小
function getW500Image(url = '') {
if (!url)
return ''
return url.replace('original', 'w500')
}
// 打开TMDB详情页面
function openTmdbPage(type: string, tmdbId: number) {
if (!type || !tmdbId)
return
const url = `https://www.themoviedb.org/${type === '电影' ? 'movie' : 'tv'}/${tmdbId}`
window.open(url, '_blank')
}
</script>
<template>
<div v-show="context">
<VCol>
<div
v-if="context?.meta_info?.name"
class="d-flex justify-space-between flex-wrap flex-md-nowrap flex-column flex-md-row"
>
<div
v-if="context?.media_info?.poster_path"
class="ma-auto"
>
<VImg
width="10rem"
aspect-ratio="2/3"
class="object-cover aspect-w-2 aspect-h-3 rounded-lg ring-1 ring-gray-500"
:src="getW500Image(context?.media_info?.poster_path)"
cover
>
<template #placeholder>
<div class="w-full h-full">
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
</div>
</template>
</VImg>
</div>
<div>
<VCardItem class="pb-1">
<VCardTitle>
{{ context?.media_info?.title || context?.meta_info?.name }}
{{ context?.meta_info?.season_episode }}
</VCardTitle>
<VCardSubtitle>
{{ context?.media_info?.year || context?.meta_info?.year }}
</VCardSubtitle>
</VCardItem>
<VCardText
v-if="context?.media_info?.overview"
class="line-clamp-4 overflow-hidden text-ellipsis ..."
>
{{ context?.media_info?.overview }}
</VCardText>
<VCardItem>
<!-- 类型 -->
<VChip
v-if="context?.media_info?.type || context?.meta_info?.type"
variant="elevated"
class="me-1 mb-1 text-white bg-blue-500"
>
{{
context?.media_info?.type || context?.meta_info?.type
}}
</VChip>
<!-- 二级分类 -->
<VChip
v-if="context?.media_info?.category"
variant="elevated"
class="me-1 mb-1 text-white bg-blue-500"
>
{{ context?.media_info?.category }}
</VChip>
<!-- TMDBID -->
<VChip
v-if="context?.media_info?.tmdb_id"
variant="elevated"
color="success"
class="me-1 mb-1"
@click="openTmdbPage(context?.media_info?.type || '', context?.media_info?.tmdb_id)"
>
{{ context?.media_info?.tmdb_id }}
</VChip>
<!-- meta_info -->
<VChip
v-if="context?.meta_info?.edition"
variant="elevated"
class="me-1 mb-1 text-white bg-red-500"
>
{{ context?.meta_info?.edition }}
</VChip>
<VChip
v-if="context?.meta_info?.resource_pix"
variant="elevated"
class="me-1 mb-1 text-white bg-red-500"
>
{{ context?.meta_info?.resource_pix }}
</VChip>
<VChip
v-if="context?.meta_info?.video_encode"
variant="elevated"
class="me-1 mb-1 text-white bg-orange-500"
>
{{ context?.meta_info?.video_encode }}
</VChip>
<VChip
v-if="context?.meta_info?.audio_encode"
variant="elevated"
class="me-1 mb-1 text-white bg-orange-500"
>
{{ context?.meta_info?.audio_encode }}
</VChip>
<VChip
v-if="context?.meta_info?.resource_team"
variant="elevated"
class="me-1 mb-1 text-white bg-cyan-500"
>
{{ context?.meta_info?.resource_team }}
</VChip>
</VCardItem>
</div>
</div>
<VAlert
v-if="!context?.meta_info?.name"
icon="mdi-alert-circle-outline"
>
识别失败无法识别到有效信息
</VAlert>
</VCol>
</div>
</template>

View File

@@ -389,7 +389,7 @@ onMounted(() => {
<!-- 更新站点Cookie & UA弹窗 --> <!-- 更新站点Cookie & UA弹窗 -->
<VDialog <VDialog
v-model="siteCookieDialog" v-model="siteCookieDialog"
max-width="600" max-width="50rem"
> >
<!-- Dialog Content --> <!-- Dialog Content -->
<VCard title="更新站点Cookie & UA"> <VCard title="更新站点Cookie & UA">

View File

@@ -62,7 +62,7 @@ async function handleAddDownload(_site: any = undefined,
confirmationText: '确认', confirmationText: '确认',
cancellationText: '取消', cancellationText: '取消',
dialogProps: { dialogProps: {
maxWidth: 600, maxWidth: '50rem',
}, },
}) })

View File

@@ -6,9 +6,10 @@ import axios from 'axios'
import { useToast } from 'vue-toast-notification' import { useToast } from 'vue-toast-notification'
import { numberValidator } from '@/@validators' import { numberValidator } from '@/@validators'
import { formatBytes } from '@core/utils/formatters' import { formatBytes } from '@core/utils/formatters'
import type { EndPoints, FileItem } from '@/api/types' import type { Context, EndPoints, FileItem } from '@/api/types'
import store from '@/store' import store from '@/store'
import api from '@/api' import api from '@/api'
import MediaInfoCard from '@/components/cards/MediaInfoCard.vue'
// 输入参数 // 输入参数
const inProps = defineProps({ const inProps = defineProps({
@@ -84,6 +85,12 @@ const transferForm = reactive({
}) })
// 识别结果
const nameTestResult = ref<Context>()
// 识别结果对话框
const nameTestDialog = ref(false)
// 生成1到50季的下拉框选项 // 生成1到50季的下拉框选项
const seasonItems = ref( const seasonItems = ref(
Array.from({ length: 51 }, (_, i) => i).map(item => ({ Array.from({ length: 51 }, (_, i) => i).map(item => ({
@@ -144,7 +151,7 @@ async function deleteItem(item: FileItem) {
confirmationText: '确认', confirmationText: '确认',
cancellationText: '取消', cancellationText: '取消',
dialogProps: { dialogProps: {
maxWidth: 600, maxWidth: '50rem',
}, },
}) })
@@ -273,6 +280,8 @@ watch(
() => inProps.path, () => inProps.path,
async () => { async () => {
items.value = [] items.value = []
nameTestResult.value = undefined
nameTestDialog.value = false
await load() await load()
}, },
) )
@@ -311,11 +320,43 @@ function stopLoadingProgress() {
progressEventSource.value?.close() progressEventSource.value?.close()
} }
// 调用API识别
async function recognize(path: string) {
try {
// 显示进度条
progressDialog.value = true
progressText.value = `正在识别 ${path} ...`
progressValue.value = 0
nameTestResult.value = await api.get('media/recognize_file', {
params: {
path,
},
})
// 关闭进度条
progressDialog.value = false
if (!nameTestResult.value)
$toast.error(`${path} 识别失败!`)
nameTestDialog.value = !!nameTestResult.value?.meta_info?.name
}
catch (error) {
console.error(error)
}
}
// 弹出菜单 // 弹出菜单
const dropdownItems = ref([ const dropdownItems = ref([
{ {
title: '重命名', title: '识别',
value: 1, value: 1,
props: {
prependIcon: 'mdi-text-recognition',
click: (_item: FileItem) => {
recognize(_item.path || '')
},
},
}, {
title: '重命名',
value: 2,
props: { props: {
prependIcon: 'mdi-rename', prependIcon: 'mdi-rename',
click: showRenmae, click: showRenmae,
@@ -323,7 +364,7 @@ const dropdownItems = ref([
}, },
{ {
title: '整理', title: '整理',
value: 2, value: 3,
props: { props: {
prependIcon: 'mdi-folder-arrow-right', prependIcon: 'mdi-folder-arrow-right',
click: showTransfer, click: showTransfer,
@@ -331,7 +372,7 @@ const dropdownItems = ref([
}, },
{ {
title: '删除', title: '删除',
value: 3, value: 4,
props: { props: {
prependIcon: 'mdi-delete-outline', prependIcon: 'mdi-delete-outline',
color: 'error', color: 'error',
@@ -365,9 +406,9 @@ onMounted(() => {
</VCardText> </VCardText>
<VCardText <VCardText
v-else-if="isFile && !isImage" v-else-if="isFile && !isImage"
class="grow d-flex justify-center align-center break-all" class="text-center break-all"
> >
文件: {{ path }}<br> 文件: {{ path }}
</VCardText> </VCardText>
<VCardText <VCardText
v-else-if="isFile && isImage" v-else-if="isFile && isImage"
@@ -413,6 +454,9 @@ onMounted(() => {
</VList> </VList>
</VMenu> </VMenu>
</IconBtn> </IconBtn>
<IconBtn class="d-none d-sm-block" @click.stop="recognize(item.path)">
<VIcon icon="mdi-text-recognition" />
</IconBtn>
<IconBtn class="d-none d-sm-block" @click.stop="showRenmae(item)"> <IconBtn class="d-none d-sm-block" @click.stop="showRenmae(item)">
<VIcon icon="mdi-rename" /> <VIcon icon="mdi-rename" />
</IconBtn> </IconBtn>
@@ -466,6 +510,9 @@ onMounted(() => {
</VList> </VList>
</VMenu> </VMenu>
</IconBtn> </IconBtn>
<IconBtn class="d-none d-sm-block" @click.stop="recognize(item.path)">
<VIcon icon="mdi-text-recognition" />
</IconBtn>
<IconBtn class="d-none d-sm-block" @click.stop="showRenmae(item)"> <IconBtn class="d-none d-sm-block" @click.stop="showRenmae(item)">
<VIcon icon="mdi-rename" /> <VIcon icon="mdi-rename" />
</IconBtn> </IconBtn>
@@ -505,18 +552,27 @@ onMounted(() => {
class="me-2" class="me-2"
/> />
<VSpacer v-if="isFile" /> <VSpacer v-if="isFile" />
<VBtn v-if="isFile" @click="download(inProps.path || '')"> <IconBtn v-if="isFile" @click="recognize(inProps.path || '')">
<VIcon>mdi-download</VIcon> <VIcon color="primary">
</VBtn> mdi-text-recognition
<VBtn v-if="!isFile" @click="load"> </VIcon>
<VIcon>mdi-refresh</VIcon> </IconBtn>
</VBtn> <IconBtn v-if="isFile" @click="download(inProps.path || '')">
<VIcon color="primary">
mdi-download
</VIcon>
</IconBtn>
<IconBtn v-if="!isFile" @click="load">
<VIcon color="primary">
mdi-refresh
</VIcon>
</IconBtn>
</VToolbar> </VToolbar>
</VCard> </VCard>
<!-- 重命名弹窗 --> <!-- 重命名弹窗 -->
<VDialog <VDialog
v-model="renamePopper" v-model="renamePopper"
max-width="600" max-width="50rem"
> >
<template #activator="{ props }"> <template #activator="{ props }">
<IconBtn title="重命名" v-bind="props"> <IconBtn title="重命名" v-bind="props">
@@ -682,6 +738,7 @@ onMounted(() => {
<vCardText class="text-center"> <vCardText class="text-center">
{{ progressText }} {{ progressText }}
<vProgressLinear <vProgressLinear
v-if="progressValue"
color="white" color="white"
class="mb-0 mt-1" class="mb-0 mt-1"
:model-value="progressValue" :model-value="progressValue"
@@ -689,6 +746,19 @@ onMounted(() => {
</vCardText> </vCardText>
</vCard> </vCard>
</vDialog> </vDialog>
<!-- 识别结果对话框 -->
<vDialog
v-model="nameTestDialog"
:scrim="false"
width="800"
>
<vCard>
<DialogCloseBtn @click="nameTestDialog = false" />
<VCardItem>
<MediaInfoCard :context="nameTestResult" />
</VCardItem>
</vCard>
</vDialog>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -128,7 +128,7 @@ async function mkdir() {
</IconBtn> </IconBtn>
<VDialog <VDialog
v-model="newFolderPopper" v-model="newFolderPopper"
max-width="600" max-width="50rem"
> >
<template #activator="{ props }"> <template #activator="{ props }">
<IconBtn title="新建文件夹" v-bind="props"> <IconBtn title="新建文件夹" v-bind="props">

View File

@@ -1,4 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { User } from '@/api/types'
import VerticalNavSectionTitle from '@/@layouts/components/VerticalNavSectionTitle.vue' import VerticalNavSectionTitle from '@/@layouts/components/VerticalNavSectionTitle.vue'
import VerticalNavLayout from '@layouts/components/VerticalNavLayout.vue' import VerticalNavLayout from '@layouts/components/VerticalNavLayout.vue'
import VerticalNavLink from '@layouts/components/VerticalNavLink.vue' import VerticalNavLink from '@layouts/components/VerticalNavLink.vue'
@@ -9,6 +10,9 @@ import NavbarThemeSwitcher from '@/layouts/components/NavbarThemeSwitcher.vue'
import SearchBar from '@/layouts/components/SearchBar.vue' import SearchBar from '@/layouts/components/SearchBar.vue'
import ShortcutBar from '@/layouts/components/ShortcutBar.vue' import ShortcutBar from '@/layouts/components/ShortcutBar.vue'
import UserProfile from '@/layouts/components/UserProfile.vue' import UserProfile from '@/layouts/components/UserProfile.vue'
// 获取当前用户信息
const accountInfo: User = inject('accountInfo') as User
</script> </script>
<template> <template>
@@ -87,6 +91,7 @@ import UserProfile from '@/layouts/components/UserProfile.vue'
}" }"
/> />
<VerticalNavLink <VerticalNavLink
v-if="accountInfo.is_superuser"
:item="{ :item="{
title: '电影', title: '电影',
icon: 'mdi-movie-check-outline', icon: 'mdi-movie-check-outline',
@@ -94,6 +99,7 @@ import UserProfile from '@/layouts/components/UserProfile.vue'
}" }"
/> />
<VerticalNavLink <VerticalNavLink
v-if="accountInfo.is_superuser"
:item="{ :item="{
title: '电视剧', title: '电视剧',
icon: 'mdi-television-classic', icon: 'mdi-television-classic',
@@ -101,6 +107,7 @@ import UserProfile from '@/layouts/components/UserProfile.vue'
}" }"
/> />
<VerticalNavLink <VerticalNavLink
v-if="accountInfo.is_superuser"
:item="{ :item="{
title: '自定义', title: '自定义',
icon: 'mdi-rss', icon: 'mdi-rss',
@@ -128,6 +135,7 @@ import UserProfile from '@/layouts/components/UserProfile.vue'
}" }"
/> />
<VerticalNavLink <VerticalNavLink
v-if="accountInfo.is_superuser"
:item="{ :item="{
title: '历史记录', title: '历史记录',
icon: 'mdi-history', icon: 'mdi-history',
@@ -135,6 +143,7 @@ import UserProfile from '@/layouts/components/UserProfile.vue'
}" }"
/> />
<VerticalNavLink <VerticalNavLink
v-if="accountInfo.is_superuser"
:item="{ :item="{
title: '文件管理', title: '文件管理',
icon: 'mdi-folder-multiple-outline', icon: 'mdi-folder-multiple-outline',
@@ -144,11 +153,13 @@ import UserProfile from '@/layouts/components/UserProfile.vue'
<!-- 👉 系统 --> <!-- 👉 系统 -->
<VerticalNavSectionTitle <VerticalNavSectionTitle
v-if="accountInfo.is_superuser"
:item="{ :item="{
heading: '系统', heading: '系统',
}" }"
/> />
<VerticalNavLink <VerticalNavLink
v-if="accountInfo.is_superuser"
:item="{ :item="{
title: '插件', title: '插件',
icon: 'mdi-apps', icon: 'mdi-apps',
@@ -156,6 +167,7 @@ import UserProfile from '@/layouts/components/UserProfile.vue'
}" }"
/> />
<VerticalNavLink <VerticalNavLink
v-if="accountInfo.is_superuser"
:item="{ :item="{
title: '站点管理', title: '站点管理',
icon: 'mdi-web', icon: 'mdi-web',
@@ -163,6 +175,7 @@ import UserProfile from '@/layouts/components/UserProfile.vue'
}" }"
/> />
<VerticalNavLink <VerticalNavLink
v-if="accountInfo.is_superuser"
:item="{ :item="{
title: '设定', title: '设定',
icon: 'mdi-cog', icon: 'mdi-cog',

View File

@@ -31,7 +31,7 @@ function search() {
> >
<VDialog <VDialog
v-model="searchDialog" v-model="searchDialog"
max-width="600" max-width="50rem"
transition="dialog-top-transition" transition="dialog-top-transition"
> >
<!-- Dialog Activator --> <!-- Dialog Activator -->

View File

@@ -1,6 +1,8 @@
<script lang="ts" setup> <script lang="ts" setup>
import NameTestView from '@/views/system/NameTestView.vue' import NameTestView from '@/views/system/NameTestView.vue'
import NetTestView from '@/views/system/NetTestView.vue' import NetTestView from '@/views/system/NetTestView.vue'
import LoggingView from '@/views/system/LoggingView.vue'
import RuleTestView from '@/views/system/RuleTestView.vue'
// App捷径 // App捷径
const appsMenu = ref(false) const appsMenu = ref(false)
@@ -10,6 +12,12 @@ const nameTestDialog = ref(false)
// 网络测试弹窗 // 网络测试弹窗
const netTestDialog = ref(false) const netTestDialog = ref(false)
// 实时日志弹窗
const loggingDialog = ref(false)
// 过滤规则弹窗
const ruleTestDialog = ref(false)
</script> </script>
<template> <template>
@@ -86,13 +94,57 @@ const netTestDialog = ref(false)
</VListItem> </VListItem>
</VCol> </VCol>
</VRow> </VRow>
<VRow class="ma-0 mt-n1 border-t">
<VCol
cols="6"
class="text-center cursor-pointer pa-0 shortcut-icon border-e"
@click="() => {}"
>
<VListItem
class="pa-4"
@click="loggingDialog = true"
>
<VAvatar
size="48"
variant="tonal"
>
<VIcon icon="mdi-file-document-outline" />
</VAvatar>
<h6 class="text-base font-weight-medium mt-2 mb-0">
日志
</h6>
<span class="text-sm">系统实时日志</span>
</VListItem>
</VCol>
<VCol
cols="6"
class="text-center cursor-pointer pa-0 shortcut-icon border-e"
@click="() => {}"
>
<VListItem
class="pa-4"
@click="ruleTestDialog = true"
>
<VAvatar
size="48"
variant="tonal"
>
<VIcon icon="mdi-filter-cog-outline" />
</VAvatar>
<h6 class="text-base font-weight-medium mt-2 mb-0">
规则
</h6>
<span class="text-sm">过滤规则测试</span>
</VListItem>
</VCol>
</VRow>
</div> </div>
</VCard> </VCard>
</VMenu> </VMenu>
<!-- 名称测试弹窗 --> <!-- 名称测试弹窗 -->
<VDialog <VDialog
v-model="nameTestDialog" v-model="nameTestDialog"
max-width="800" max-width="50rem"
> >
<VCard title="名称识别测试"> <VCard title="名称识别测试">
<DialogCloseBtn @click="nameTestDialog = false" /> <DialogCloseBtn @click="nameTestDialog = false" />
@@ -104,7 +156,7 @@ const netTestDialog = ref(false)
<!-- 网络测试弹窗 --> <!-- 网络测试弹窗 -->
<VDialog <VDialog
v-model="netTestDialog" v-model="netTestDialog"
max-width="600" max-width="35rem"
> >
<VCard title="网络测试"> <VCard title="网络测试">
<DialogCloseBtn @click="netTestDialog = false" /> <DialogCloseBtn @click="netTestDialog = false" />
@@ -113,4 +165,30 @@ const netTestDialog = ref(false)
</VCardItem> </VCardItem>
</VCard> </VCard>
</VDialog> </VDialog>
<!-- 实时日志弹窗 -->
<VDialog
v-model="loggingDialog"
class="w-full lg:w-4/5"
scrollable
>
<VCard title="实时日志">
<DialogCloseBtn @click="loggingDialog = false" />
<VCardText>
<LoggingView />
</VCardText>
</VCard>
</VDialog>
<!-- 规则测试弹窗 -->
<VDialog
v-model="ruleTestDialog"
max-width="50rem"
scrollable
>
<VCard title="过滤规则测试">
<DialogCloseBtn @click="ruleTestDialog = false" />
<VCardText>
<RuleTestView />
</VCardText>
</VCard>
</VDialog>
</template> </template>

View File

@@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { useStore } from 'vuex' import { useStore } from 'vuex'
import router from '@/router' import router from '@/router'
import type { User } from '@/api/types'
// Vuex Store // Vuex Store
const store = useStore() const store = useStore()
@@ -15,7 +16,7 @@ function logout() {
} }
// 获取当前用户信息 // 获取当前用户信息
const accountInfo: any = inject('accountInfo') const accountInfo: User = inject('accountInfo') as User
</script> </script>
<template> <template>
@@ -56,6 +57,7 @@ const accountInfo: any = inject('accountInfo')
<!-- 👉 Profile --> <!-- 👉 Profile -->
<VListItem <VListItem
v-if="accountInfo.is_superuser"
link link
to="setting" to="setting"
> >

View File

@@ -1,10 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import AnalyticsMediaStatistic from '@/views/dashboard/AnalyticsMediaStatistic.vue' import AnalyticsMediaStatistic from '@/views/dashboard/AnalyticsMediaStatistic.vue'
import AnalyticsProcesses from '@/views/dashboard/AnalyticsProcesses.vue'
import AnalyticsScheduler from '@/views/dashboard/AnalyticsScheduler.vue' import AnalyticsScheduler from '@/views/dashboard/AnalyticsScheduler.vue'
import AnalyticsSpeed from '@/views/dashboard/AnalyticsSpeed.vue' import AnalyticsSpeed from '@/views/dashboard/AnalyticsSpeed.vue'
import AnalyticsStorage from '@/views/dashboard/AnalyticsStorage.vue' import AnalyticsStorage from '@/views/dashboard/AnalyticsStorage.vue'
import AnalyticsWeeklyOverview from '@/views/dashboard/AnalyticsWeeklyOverview.vue' import AnalyticsWeeklyOverview from '@/views/dashboard/AnalyticsWeeklyOverview.vue'
import AnalyticsCpu from '@/views/dashboard/AnalyticsCpu.vue'
import AnalyticsMemory from '@/views/dashboard/AnalyticsMemory.vue'
</script> </script>
<template> <template>
@@ -44,8 +45,18 @@ import AnalyticsWeeklyOverview from '@/views/dashboard/AnalyticsWeeklyOverview.v
<AnalyticsScheduler /> <AnalyticsScheduler />
</VCol> </VCol>
<VCol cols="12"> <VCol
<AnalyticsProcesses /> cols="12"
md="6"
>
<AnalyticsCpu />
</VCol>
<VCol
cols="12"
md="6"
>
<AnalyticsMemory />
</VCol> </VCol>
</VRow> </VRow>
</template> </template>

View File

@@ -5,7 +5,6 @@ import AccountSettingNotification from '@/views/setting/AccountSettingNotificati
import AccountSettingRule from '@/views/setting/AccountSettingRule.vue' import AccountSettingRule from '@/views/setting/AccountSettingRule.vue'
import AccountSettingSite from '@/views/setting/AccountSettingSite.vue' import AccountSettingSite from '@/views/setting/AccountSettingSite.vue'
import AccountSettingWords from '@/views/setting/AccountSettingWords.vue' import AccountSettingWords from '@/views/setting/AccountSettingWords.vue'
import AccountSettingLogging from '@/views/setting/AccountSettingLogging.vue'
import AccountSettingAbout from '@/views/setting/AccountSettingAbout.vue' import AccountSettingAbout from '@/views/setting/AccountSettingAbout.vue'
const route = useRoute() const route = useRoute()
@@ -39,11 +38,6 @@ const tabs = [
icon: 'mdi-file-word-box', icon: 'mdi-file-word-box',
tab: 'words', tab: 'words',
}, },
{
title: '日志',
icon: 'mdi-text-box',
tab: 'logging',
},
{ {
title: '关于', title: '关于',
icon: 'mdi-information', icon: 'mdi-information',
@@ -96,12 +90,6 @@ const tabs = [
<AccountSettingWords /> <AccountSettingWords />
</transition> </transition>
</VWindowItem> </VWindowItem>
<!-- Logging -->
<VWindowItem value="logging">
<transition name="fade-slide" appear>
<AccountSettingLogging />
</transition>
</VWindowItem>
<!-- About --> <!-- About -->
<VWindowItem value="about"> <VWindowItem value="about">
<transition name="fade-slide" appear> <transition name="fade-slide" appear>

View File

@@ -67,9 +67,9 @@ const theme: VuetifyOptions['theme'] = {
'on-primary': '#FFFFFF', 'on-primary': '#FFFFFF',
'on-success': '#FFFFFF', 'on-success': '#FFFFFF',
'on-warning': '#FFFFFF', 'on-warning': '#FFFFFF',
'background': '#28243D', 'background': '#111827',
'on-background': '#E7E3FC', 'on-background': '#E7E3FC',
'surface': '#312D4B', 'surface': '#161D2C',
'on-surface': '#E7E3FC', 'on-surface': '#E7E3FC',
'grey-50': '#2A2E42', 'grey-50': '#2A2E42',
'grey-100': '#474360', 'grey-100': '#474360',
@@ -87,7 +87,7 @@ const theme: VuetifyOptions['theme'] = {
}, },
variables: { variables: {
'code-color': '#d400ff', 'code-color': '#d400ff',
'overlay-scrim-background': '#2C2942', 'overlay-scrim-background': '#1F2937',
'overlay-scrim-opacity': 0.6, 'overlay-scrim-opacity': 0.6,
'hover-opacity': 0.04, 'hover-opacity': 0.04,
'focus-opacity': 0.1, 'focus-opacity': 0.1,
@@ -96,9 +96,8 @@ const theme: VuetifyOptions['theme'] = {
'pressed-opacity': 0.14, 'pressed-opacity': 0.14,
'dragged-opacity': 0.1, 'dragged-opacity': 0.1,
'border-color': '#E7E3FC', 'border-color': '#E7E3FC',
'table-header-background': '#3D3759', 'table-header-background': '#1F2937',
'custom-background': '#373452', 'custom-background': '#373452',
// Shadows // Shadows
'shadow-key-umbra-opacity': 'rgba(20, 18, 33, 0.08)', 'shadow-key-umbra-opacity': 'rgba(20, 18, 33, 0.08)',
'shadow-key-penumbra-opacity': 'rgba(20, 18, 33, 0.12)', 'shadow-key-penumbra-opacity': 'rgba(20, 18, 33, 0.12)',

View File

@@ -112,3 +112,14 @@
background: #a1a1a1; background: #a1a1a1;
} }
} }
.v-alert--variant-elevated, .v-alert--variant-flat {
background: rgb(var(--v-table-header-background));
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
}
.backdrop-blur {
--tw-backdrop-blur: blur(8px)!important;
-webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)!important;
backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)!important;
}

View File

@@ -0,0 +1,132 @@
<script setup lang="ts">
import VueApexCharts from 'vue3-apexcharts'
import { useTheme } from 'vuetify'
import { hexToRgb } from '@layouts/utils'
import api from '@/api'
const vuetifyTheme = useTheme()
const currentTheme = controlledComputed(() => vuetifyTheme.name.value, () => vuetifyTheme.current.value.colors)
const variableTheme = controlledComputed(() => vuetifyTheme.name.value, () => vuetifyTheme.current.value.variables)
// 定时器
let refreshTimer: NodeJS.Timer | null = null
// 时间序列
const series = ref([
{
data: [0],
},
])
// 当前值
const current = ref(0)
const chartOptions = controlledComputed(() => vuetifyTheme.name.value, () => {
return {
chart: {
parentHeightOffset: 0,
toolbar: { show: false },
},
tooltip: { enabled: false },
grid: {
borderColor: `rgba(${hexToRgb(String(variableTheme.value['border-color']))},${variableTheme.value['border-opacity']})`,
strokeDashArray: 6,
xaxis: {
lines: { show: false },
},
yaxis: {
lines: { show: true },
},
padding: {
top: -10,
left: -7,
right: 5,
bottom: 5,
},
},
stroke: {
width: 3,
lineCap: 'butt',
curve: 'smooth',
},
colors: [currentTheme.value.primary],
markers: {
size: 6,
offsetY: 4,
offsetX: -2,
strokeWidth: 3,
colors: ['transparent'],
strokeColors: 'transparent',
discrete: [
{
size: 5.5,
seriesIndex: 0,
strokeColor: currentTheme.value.primary,
fillColor: currentTheme.value.surface,
},
],
hover: { size: 7 },
},
xaxis: {
labels: { show: false },
axisTicks: { show: false },
axisBorder: { show: false },
},
yaxis: {
labels: { show: false },
},
}
})
// 调用API接口获取最新CPU使用率
async function getCpuUsage() {
try {
// 请求数据
current.value = await api.get('dashboard/cpu') ?? 0
// 添加到序列
series.value[0].data.push(current.value)
// 序列超过30条记录时清掉前面的
if (series.value[0].data.length > 30)
series.value[0].data.shift()
}
catch (e) {
console.log(e)
}
}
onMounted(() => {
getCpuUsage()// 启动定时器
refreshTimer = setInterval(() => {
getCpuUsage()
}, 2000)
})
// 组件卸载时停止定时器
onUnmounted(() => {
if (refreshTimer) {
clearInterval(refreshTimer)
refreshTimer = null
}
})
</script>
<template>
<VCard>
<VCardText>
<h6 class="text-h6">
CPU
</h6>
<VueApexCharts
type="line"
:options="chartOptions"
:series="series"
:height="150"
/>
<p class="text-center font-weight-medium mb-0">
当前{{ current }}%
</p>
</VCardText>
</VCard>
</template>

View File

@@ -0,0 +1,137 @@
<script setup lang="ts">
import VueApexCharts from 'vue3-apexcharts'
import { useTheme } from 'vuetify'
import { hexToRgb } from '@layouts/utils'
import api from '@/api'
import { formatBytes } from '@/@core/utils/formatters'
const vuetifyTheme = useTheme()
const currentTheme = controlledComputed(() => vuetifyTheme.name.value, () => vuetifyTheme.current.value.colors)
const variableTheme = controlledComputed(() => vuetifyTheme.name.value, () => vuetifyTheme.current.value.variables)
// 定时器
let refreshTimer: NodeJS.Timer | null = null
// 时间序列
const series = ref([
{
data: [0],
},
])
// 当前值
const current = ref(0)
const chartOptions = controlledComputed(() => vuetifyTheme.name.value, () => {
return {
chart: {
parentHeightOffset: 0,
toolbar: { show: false },
},
tooltip: { enabled: false },
grid: {
borderColor: `rgba(${hexToRgb(String(variableTheme.value['border-color']))},${variableTheme.value['border-opacity']})`,
strokeDashArray: 6,
xaxis: {
lines: { show: false },
},
yaxis: {
lines: { show: true },
},
padding: {
top: -10,
left: -7,
right: 5,
bottom: 5,
},
},
stroke: {
width: 3,
lineCap: 'butt',
curve: 'smooth',
},
colors: [currentTheme.value.primary],
markers: {
size: 6,
offsetY: 4,
offsetX: -2,
strokeWidth: 3,
colors: ['transparent'],
strokeColors: 'transparent',
discrete: [
{
size: 5.5,
seriesIndex: 0,
strokeColor: currentTheme.value.primary,
fillColor: currentTheme.value.surface,
},
],
hover: { size: 7 },
},
dataLabels: {
enabled: false,
},
xaxis: {
labels: { show: false },
axisTicks: { show: false },
axisBorder: { show: false },
},
yaxis: {
labels: { show: false },
},
}
})
// 调用API接口获取最新内存使用量
async function getMemorgUsage() {
try {
// 请求数据
current.value = await api.get('dashboard/memory') ?? 0
// 添加到序列
series.value[0].data.push(current.value / 1024 / 1024 / 1024)
// 序列超过30条记录时清掉前面的
if (series.value[0].data.length > 30)
series.value[0].data.shift()
}
catch (e) {
console.log(e)
}
}
onMounted(() => {
getMemorgUsage()
// 启动定时器
refreshTimer = setInterval(() => {
getMemorgUsage()
}, 3000)
})
// 组件卸载时停止定时器
onUnmounted(() => {
if (refreshTimer) {
clearInterval(refreshTimer)
refreshTimer = null
}
})
</script>
<template>
<VCard>
<VCardText>
<h6 class="text-h6">
内存
</h6>
<VueApexCharts
type="area"
:options="chartOptions"
:series="series"
:height="150"
/>
<p class="text-center font-weight-medium mb-0">
当前{{ formatBytes(current) }}
</p>
</VCardText>
</VCard>
</template>

View File

@@ -1,5 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue' import { ref } from 'vue'
import _ from 'lodash'
import api from '@/api' import api from '@/api'
import type { Context } from '@/api/types' import type { Context } from '@/api/types'
import TorrentCard from '@/components/cards/TorrentCard.vue' import TorrentCard from '@/components/cards/TorrentCard.vue'
@@ -111,7 +112,7 @@ watchEffect(() => {
) )
}) })
if (matchData.length > 0) { if (matchData.length > 0) {
const firstData = matchData[0] as SearchTorrent const firstData = _.cloneDeepWith(matchData[0]) as SearchTorrent
if (matchData.length > 1) if (matchData.length > 1)
firstData.more = matchData.slice(1) firstData.more = matchData.slice(1)
@@ -312,7 +313,7 @@ onMounted(initData)
<span>{{ progressText }}</span> <span>{{ progressText }}</span>
</div> </div>
<div v-if="dataList.length > 0" class="grid gap-3 grid-torrent-card items-start"> <div v-if="dataList.length > 0" class="grid gap-3 grid-torrent-card items-start">
<TorrentCard v-for="data in dataList" :key="`${data.torrent_info.title}_${data.torrent_info.site}`" :torrent="data" :more="data.more" /> <TorrentCard v-for="data in dataList" :key="`${data.torrent_info.title}_${data.torrent_info.site_name}_${data.torrent_info.page_url}`" :torrent="data" :more="data.more" />
</div> </div>
<NoDataFound <NoDataFound
v-if="dataList.length === 0 && isRefreshed" v-if="dataList.length === 0 && isRefreshed"

View File

@@ -30,6 +30,9 @@ const redoTypeItems = ref([
// 当前操作记录 // 当前操作记录
const currentHistory = ref<TransferHistory>() const currentHistory = ref<TransferHistory>()
// 已选中的数据
const selected = ref<TransferHistory[]>([])
// 表头 // 表头
const headers = [ const headers = [
{ title: '标题', key: 'title', sortable: false }, { title: '标题', key: 'title', sortable: false },
@@ -59,6 +62,15 @@ const itemsPerPage = ref(25)
// 当前页码 // 当前页码
const currentPage = ref(1) const currentPage = ref(1)
// 进度条
const progressDialog = ref(false)
// 进度文本
const progressText = ref('请稍候 ...')
// 进度值
const progressValue = ref(0)
// 获取订阅列表数据 // 获取订阅列表数据
async function fetchData({ async function fetchData({
page, page,
@@ -113,21 +125,30 @@ const TransferDict: { [key: string]: string } = {
// 删除历史记录 // 删除历史记录
async function removeHistory(item: TransferHistory) { async function removeHistory(item: TransferHistory) {
const isConfirmed = await createConfirm({
title: '确认',
content: `同步删除 ${item.title} 对应的媒体库文件 ?`,
confirmationText: '同步删除文件',
cancellationText: '仅删除历史记录',
dialogProps: {
maxWidth: '50rem',
},
confirmationButtonProps: {
color: 'error',
},
})
if (isConfirmed === undefined)
return
// 执行删除
remove(item, isConfirmed || false)
// 清空选中项
selected.value = []
}
// 调用API删除记录
async function remove(item: TransferHistory, deleteFile: boolean) {
try { try {
const isConfirmed = await createConfirm({
title: '确认',
content: `同步删除 ${item.title} 对应的媒体库文件 ?`,
confirmationText: '同步删除文件',
cancellationText: '仅删除历史记录',
dialogProps: {
maxWidth: 600,
},
})
let deleteFile = false
if (isConfirmed)
deleteFile = true
// 调用删除API // 调用删除API
const result: { [key: string]: any } = await api.delete(`history/transfer?delete_file=${deleteFile}`, { const result: { [key: string]: any } = await api.delete(`history/transfer?delete_file=${deleteFile}`, {
data: item, data: item,
@@ -148,6 +169,54 @@ async function removeHistory(item: TransferHistory) {
} }
} }
// 批量删除历史记录
async function removeHistoryBatch() {
if (selected.value.length === 0)
return
// 确认
const isConfirmed = await createConfirm({
title: '确认',
content: `同步删除 ${selected.value.length} 条记录对应的媒体库文件 ?`,
confirmationText: '同步删除文件',
cancellationText: '仅删除历史记录',
dialogProps: {
maxWidth: '50rem',
},
confirmationButtonProps: {
color: 'error',
},
})
if (isConfirmed === undefined)
return
console.log(selected.value)
// 总条数
const total = selected.value.length
// 已处理条数
let handled = 0
// 显示进度条
progressDialog.value = true
// 循环调用removeHistory
for (const item of selected.value) {
// 开始删除
progressText.value = `正在删除 ${item.title} ${item.seasons}${item.episodes} ...`
await remove(item, isConfirmed || false)
// 删除完成
handled++
progressValue.value = handled / total * 100
}
// 清空选中项
selected.value = []
// 隐藏进度条
progressDialog.value = false
// 重新获取数据
fetchData({
page: currentPage.value,
itemsPerPage: itemsPerPage.value,
})
}
// 重新整理 // 重新整理
async function rehandleHistory() { async function rehandleHistory() {
try { try {
@@ -238,6 +307,7 @@ const dropdownItems = ref([
</VCardTitle> </VCardTitle>
</VCardItem> </VCardItem>
<VDataTableServer <VDataTableServer
v-model="selected"
v-model:items-per-page="itemsPerPage" v-model:items-per-page="itemsPerPage"
:headers="headers" :headers="headers"
:items="dataList" :items="dataList"
@@ -248,6 +318,7 @@ const dropdownItems = ref([
item-value="id" item-value="id"
return-object return-object
fixed-header fixed-header
show-select
items-per-page-text="每页条数" items-per-page-text="每页条数"
page-text="{0}-{1} {2} " page-text="{0}-{1} {2} "
@update:options="fetchData" @update:options="fetchData"
@@ -322,7 +393,7 @@ const dropdownItems = ref([
</VCard> </VCard>
<VDialog <VDialog
v-model="redoDialog" v-model="redoDialog"
max-width="600" max-width="50rem"
> >
<!-- Dialog Content --> <!-- Dialog Content -->
<VCard title="重新整理"> <VCard title="重新整理">
@@ -357,6 +428,33 @@ const dropdownItems = ref([
</VCardActions> </VCardActions>
</VCard> </VCard>
</VDialog> </VDialog>
<span v-if="selected.length > 0" class="fixed right-5 bottom-5">
<VBtn
icon="mdi-trash-can-outline"
color="error"
size="x-large"
@click="removeHistoryBatch"
/>
</span>
<!-- 进度框 -->
<vDialog
v-model="progressDialog"
:scrim="false"
width="400"
>
<vCard
color="primary"
>
<vCardText class="text-center">
{{ progressText }}
<vProgressLinear
color="white"
class="mb-0 mt-1"
:model-value="progressValue"
/>
</vCardText>
</vCard>
</vDialog>
</template> </template>
<style lang="scss"> <style lang="scss">

View File

@@ -1,55 +0,0 @@
<script lang="ts" setup>
import store from '@/store'
// 日志列表
const logs = ref<string[]>([])
// SSE持续获取日志
function startSSELogging() {
const token = store.state.auth.token
if (token) {
const eventSource = new EventSource(
`${import.meta.env.VITE_API_BASE_URL}system/logging?token=${token}`,
)
eventSource.addEventListener('message', (event) => {
const message = event.data
if (message)
logs.value.push(message)
})
onBeforeUnmount(() => {
eventSource.close()
})
}
}
onMounted(() => {
startSSELogging()
})
</script>
<template>
<VRow>
<VCol cols="12">
<VCard title="实时日志">
<VCardText>
<div
v-if="logs.length === 0"
class="mt-5 w-full text-center text-gray-500 text-sm flex flex-col items-center"
>
<VProgressCircular
size="48"
indeterminate
color="primary"
/>
<span class="mt-3">正在刷新 ...</span>
</div>
<div v-for="(log, i) in logs" :key="i">
{{ log }}
</div>
</VCardText>
</VCard>
</VCol>
</VRow>
</template>

View File

@@ -11,6 +11,9 @@ const customIdentifiers = ref('')
// 自定义制作组 // 自定义制作组
const customReleaseGroups = ref('') const customReleaseGroups = ref('')
// 文件整理屏蔽词
const transferExcludeWords = ref('')
// 查询已设置的识别词 // 查询已设置的识别词
async function queryCustomIdentifiers() { async function queryCustomIdentifiers() {
try { try {
@@ -39,6 +42,20 @@ async function queryCustomReleaseGroups() {
} }
} }
// 查询已设置的屏蔽词
async function queryTransferExcludeWords() {
try {
const result: { [key: string]: any } = await api.get(
'system/setting/TransferExcludeWords',
)
transferExcludeWords.value = result.data?.value.join('\n')
}
catch (error) {
console.log(error)
}
}
// 保存用户设置的识别词 // 保存用户设置的识别词
async function saveCustomIdentifiers() { async function saveCustomIdentifiers() {
try { try {
@@ -77,9 +94,29 @@ async function saveCustomReleaseGroups() {
} }
} }
// 保存文件整理屏蔽词
async function saveTransferExcludeWords() {
try {
// 用户名密码
const result: { [key: string]: any } = await api.post(
'system/setting/TransferExcludeWords',
transferExcludeWords.value.split('\n'),
)
if (result.success)
$toast.success('文件整理屏蔽词保存成功')
else
$toast.error('文件整理屏蔽词保存失败!')
}
catch (error) {
console.log(error)
}
}
onMounted(() => { onMounted(() => {
queryCustomIdentifiers() queryCustomIdentifiers()
queryCustomReleaseGroups() queryCustomReleaseGroups()
queryTransferExcludeWords()
}) })
</script> </script>
@@ -128,5 +165,25 @@ onMounted(() => {
</VCardItem> </VCardItem>
</VCard> </VCard>
</VCol> </VCol>
<VCol cols="12">
<VCard title="文件整理屏蔽词">
<VCardSubtitle> 目录名或文件名中包含屏蔽词时不进行整理 </VCardSubtitle>
<VCardItem>
<VTextarea
v-model="transferExcludeWords"
auto-grow
placeholder="支持正则表达式,特殊字符需要\转义,一行代表一个屏蔽词"
/>
</VCardItem>
<VCardItem>
<VBtn
type="submit"
@click="saveTransferExcludeWords"
>
保存
</VBtn>
</VCardItem>
</VCard>
</VCol>
</VRow> </VRow>
</template> </template>

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { CalendarOptions } from '@fullcalendar/core' import type { CalendarOptions, EventSourceInput } from '@fullcalendar/core'
import dayGridPlugin from '@fullcalendar/daygrid' import dayGridPlugin from '@fullcalendar/daygrid'
import interactionPlugin from '@fullcalendar/interaction' import interactionPlugin from '@fullcalendar/interaction'
import timeGridPlugin from '@fullcalendar/timegrid' import timeGridPlugin from '@fullcalendar/timegrid'
@@ -89,7 +89,7 @@ async function getSubscribes() {
// 合并事件 // 合并事件
const events = [...subEvents, ...rssEvents] const events = [...subEvents, ...rssEvents]
calendarOptions.value.events = events.flat() calendarOptions.value.events = events.flat().filter(event => event.start) as EventSourceInput
} }
catch (error) { catch (error) {
console.error(error) console.error(error)

View File

@@ -0,0 +1,121 @@
<script lang="ts" setup>
import store from '@/store'
// 日志列表
const logs = ref<string[]>([])
// SSE持续获取日志
function startSSELogging() {
const token = store.state.auth.token
if (token) {
const eventSource = new EventSource(
`${import.meta.env.VITE_API_BASE_URL}system/logging?token=${token}`,
)
eventSource.addEventListener('message', (event) => {
const message = event.data
if (message)
logs.value.push(message)
})
onBeforeUnmount(() => {
eventSource.close()
})
}
}
// 从日志中提取日志详情
function extractLogDetailsFromLogs(logs: string[]): { level: string; time: string; program: string; content: string }[] {
const logDetails: { level: string; time: string; program: string; content: string }[] = []
const logPattern = /^【(.*?)】[0-9\-:]*\s(.*?)\s-\s(.*?)\s-\s(.*)$/
for (const log of logs) {
const matches = RegExp(logPattern).exec(log)
if (matches && matches.length === 5) {
const [_, level, time, program, content] = matches
logDetails.push({ level, time, program, content })
}
}
return logDetails
}
// 计算日志颜色
function getLogColor(level: string): string {
switch (level) {
case 'DEBUG':
return 'primary'
case 'INFO':
return 'secondary'
case 'WARNING':
return 'warning'
case 'ERROR':
return 'error'
default:
return 'secondary'
}
}
// 拆分日志数据计算属性
const extractLogDetails = computed(() => {
return extractLogDetailsFromLogs(logs.value)
})
onMounted(() => {
startSSELogging()
})
</script>
<template>
<div
v-if="logs.length === 0"
class="mt-5 w-full text-center flex flex-col items-center"
>
<VProgressCircular
size="48"
indeterminate
color="primary"
/>
<span class="mt-3">正在刷新 ...</span>
</div>
<div>
<VTable
class="table-rounded"
hide-default-footer
disable-sort
>
<tbody>
<tr v-for="(log, i) in extractLogDetails" :key="i" class="text-sm">
<td
class="text-sm"
>
<VChip
size="small"
:color="getLogColor(log.level)"
variant="elevated"
v-text="log.level"
/>
</td>
<!-- name -->
<td
class="text-sm"
>
{{ log.time }}
</td>
<td
class="text-sm"
>
<h6 class="text-sm font-weight-medium">
{{ log.program }}
</h6>
</td>
<td
class="text-sm"
v-text="log.content"
/>
</tr>
</tbody>
</VTable>
</div>
</template>

View File

@@ -3,6 +3,7 @@ import { reactive, ref } from 'vue'
import { requiredValidator } from '@/@validators' import { requiredValidator } from '@/@validators'
import api from '@/api' import api from '@/api'
import type { Context } from '@/api/types' import type { Context } from '@/api/types'
import MediaInfoCard from '@/components/cards/MediaInfoCard.vue'
// 识别结果 // 识别结果
const nameTestResult = ref<Context>() const nameTestResult = ref<Context>()
@@ -45,22 +46,6 @@ async function nameTest() {
console.error(error) console.error(error)
} }
} }
// 打开TMDB详情页面
function openTmdbPage(type: string, tmdbId: number) {
if (!type || !tmdbId)
return
const url = `https://www.themoviedb.org/${type === '电影' ? 'movie' : 'tv'}/${tmdbId}`
window.open(url, '_blank')
}
// TMDB图片转换为w500大小
function getW500Image(url = '') {
if (!url)
return ''
return url.replace('original', 'w500')
}
</script> </script>
<template> <template>
@@ -101,123 +86,7 @@ function getW500Image(url = '') {
</VForm> </VForm>
<VExpandTransition> <VExpandTransition>
<div v-show="showResult"> <div v-show="showResult">
<VCol> <MediaInfoCard :context="nameTestResult" />
<div
v-if="nameTestResult?.meta_info?.name"
class="d-flex justify-space-between flex-wrap flex-md-nowrap flex-column flex-md-row"
>
<div
v-if="nameTestResult?.media_info?.poster_path"
class="ma-auto"
>
<VImg
width="10rem"
aspect-ratio="2/3"
class="object-cover aspect-w-2 aspect-h-3 rounded-lg ring-1 ring-gray-500"
:src="getW500Image(nameTestResult?.media_info?.poster_path)"
cover
>
<template #placeholder>
<div class="w-full h-full">
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
</div>
</template>
</VImg>
</div>
<div>
<VCardItem class="pb-1">
<VCardTitle>
{{ nameTestResult?.media_info?.title || nameTestResult?.meta_info?.name }}
{{ nameTestResult?.meta_info?.season_episode }}
</VCardTitle>
<VCardSubtitle>
{{ nameTestResult?.media_info?.year || nameTestResult?.meta_info?.year }}
</VCardSubtitle>
</VCardItem>
<VCardText
v-if="nameTestResult?.media_info?.overview"
class="line-clamp-4 overflow-hidden text-ellipsis ..."
>
{{ nameTestResult?.media_info?.overview }}
</VCardText>
<VCardItem>
<!-- 类型 -->
<VChip
v-if="nameTestResult?.media_info?.type || nameTestResult?.meta_info?.type"
variant="elevated"
class="me-1 mb-1 text-white bg-blue-500"
>
{{
nameTestResult?.media_info?.type || nameTestResult?.meta_info?.type
}}
</VChip>
<!-- 二级分类 -->
<VChip
v-if="nameTestResult?.media_info?.category"
variant="elevated"
class="me-1 mb-1 text-white bg-blue-500"
>
{{ nameTestResult?.media_info?.category }}
</VChip>
<!-- TMDBID -->
<VChip
v-if="nameTestResult?.media_info?.tmdb_id"
variant="elevated"
color="success"
class="me-1 mb-1"
@click="openTmdbPage(nameTestResult?.media_info?.type || '', nameTestResult?.media_info?.tmdb_id)"
>
{{ nameTestResult?.media_info?.tmdb_id }}
</VChip>
<!-- meta_info -->
<VChip
v-if="nameTestResult?.meta_info?.edition"
variant="elevated"
class="me-1 mb-1 text-white bg-red-500"
>
{{ nameTestResult?.meta_info?.edition }}
</VChip>
<VChip
v-if="nameTestResult?.meta_info?.resource_pix"
variant="elevated"
class="me-1 mb-1 text-white bg-red-500"
>
{{ nameTestResult?.meta_info?.resource_pix }}
</VChip>
<VChip
v-if="nameTestResult?.meta_info?.video_encode"
variant="elevated"
class="me-1 mb-1 text-white bg-orange-500"
>
{{ nameTestResult?.meta_info?.video_encode }}
</VChip>
<VChip
v-if="nameTestResult?.meta_info?.audio_encode"
variant="elevated"
class="me-1 mb-1 text-white bg-orange-500"
>
{{ nameTestResult?.meta_info?.audio_encode }}
</VChip>
<VChip
v-if="nameTestResult?.meta_info?.resource_team"
variant="elevated"
class="me-1 mb-1 text-white bg-cyan-500"
>
{{ nameTestResult?.meta_info?.resource_team }}
</VChip>
</VCardItem>
</div>
</div>
<VAlert
v-if="!nameTestResult?.meta_info?.name"
icon="mdi-alert-circle-outline"
>
识别失败无法识别到有效信息
</VAlert>
</VCol>
</div> </div>
</VExpandTransition> </VExpandTransition>
</template> </template>

View File

@@ -0,0 +1,117 @@
<script setup lang="ts">
import { reactive, ref } from 'vue'
import { requiredValidator } from '@/@validators'
import api from '@/api'
// 识别结果
const ruleTestResult = ref('')
// 名称识别表单
const ruleTestForm = reactive({
title: '',
subtitle: '',
ruletype: '1',
})
// 识别按钮状态
const ruleTestLoading = ref(false)
// 识别按钮文本
const ruleTestText = ref('测试')
// 是否显示结果
const showResult = ref(false)
// 调用API识别
async function ruleTest() {
if (!ruleTestForm.title)
return
try {
ruleTestLoading.value = true
ruleTestText.value = '正在测试...'
showResult.value = false
const result: { [key: string]: any } = await api.get('system/ruletest', {
params: {
title: ruleTestForm.title,
subtitle: ruleTestForm.subtitle,
ruletype: ruleTestForm.ruletype,
},
})
if (result.success)
ruleTestResult.value = `优先级:${result.data.priority}`
else
ruleTestResult.value = '未命中任何优先级规则!'
ruleTestLoading.value = false
ruleTestText.value = '重新测试'
showResult.value = true
}
catch (error) {
console.error(error)
}
}
</script>
<template>
<VForm @submit.prevent="() => {}">
<VRow class="pt-2">
<VCol cols="12" md="8">
<VTextField
v-model="ruleTestForm.title"
label="标题"
:rules="[requiredValidator]"
/>
</VCol>
<VCol cols="12" md="4">
<VSelect
v-model="ruleTestForm.ruletype"
label="规则类型"
:items="[{
title: '默认规则',
value: '1',
}, {
title: '洗版规则',
value: '2',
}]"
/>
</VCol>
<VCol cols="12">
<VTextarea
v-model="ruleTestForm.subtitle"
label="副标题"
rows="2"
auto-grow
/>
</VCol>
</VRow>
<VRow>
<VCol
cols="12"
class="text-center"
>
<VBtn
:disabled="ruleTestLoading"
@click="ruleTest"
>
<template #prepend>
<VIcon icon="mdi-filter-check-outline" />
</template>
{{ ruleTestText }}
</VBtn>
</VCol>
</VRow>
</VForm>
<VExpandTransition>
<div v-show="showResult">
<VCol>
<VAlert
icon="mdi-alert-circle-outline"
>
{{ ruleTestResult }}
</VAlert>
</VCol>
</div>
</VExpandTransition>
</template>