Compare commits

..

55 Commits

Author SHA1 Message Date
jxxghp
7cce57496d v1.2.0-1 2023-09-14 16:12:09 +08:00
jxxghp
e54e851f61 fix ios菜单点击两次 2023-09-14 16:11:44 +08:00
jxxghp
17020cf62d fix text 2023-09-14 10:39:35 +08:00
jxxghp
0c7be28eaa fix tooltip 2023-09-14 10:11:11 +08:00
jxxghp
0d5a183f2e fix ui 2023-09-14 09:49:03 +08:00
jxxghp
c222594bea fix size 2023-09-13 19:25:46 +08:00
jxxghp
3df8bdfbf2 fix ui 2023-09-12 09:24:45 +08:00
jxxghp
5722547d93 fix 日历 2023-09-12 08:53:14 +08:00
jxxghp
dea5ebd95d feat RSS地址维护 2023-09-11 18:02:00 +08:00
jxxghp
048e41c1ca feat 移动自定义订阅 2023-09-11 17:48:09 +08:00
jxxghp
5078036c51 fix ui 2023-09-11 16:34:34 +08:00
jxxghp
e7a128bf0d fix image size 2023-09-11 12:35:29 +08:00
jxxghp
0e46936231 fix ui 2023-09-11 12:32:20 +08:00
jxxghp
d91d3ef0ef feat TMDBID搜索 2023-09-11 11:59:07 +08:00
jxxghp
1f0dd907f9 1.1.8 2023-09-10 17:34:26 +08:00
jxxghp
3c555cbfca fix site api 2023-09-09 17:40:01 +08:00
jxxghp
9a8e4d8600 fix ui text 2023-09-09 16:35:55 +08:00
jxxghp
444aaa5cdc fix plugin ui 2023-09-09 11:38:17 +08:00
jxxghp
25669d18fc v1.1.7 2023-09-09 11:03:49 +08:00
jxxghp
03d6e46eca fix 过滤规则删除问题 2023-09-09 09:40:06 +08:00
jxxghp
c44c7ed0f0 fix filemanager 2023-09-09 08:01:00 +08:00
jxxghp
47ac7437c0 feat 优化插件界面交互 2023-09-08 15:36:54 +08:00
jxxghp
37f982e0ea add icon 2023-09-08 15:08:32 +08:00
jxxghp
139646369f fix icon 2023-09-08 10:02:43 +08:00
jxxghp
3d4b84dc09 更新 package.json 2023-09-07 17:52:20 +08:00
jxxghp
02866754e0 add icon 2023-09-07 15:50:08 +08:00
jxxghp
e4e1a75d44 fix ui 2023-09-06 21:19:30 +08:00
jxxghp
4e5dd03456 fix ui 2023-09-06 20:22:32 +08:00
jxxghp
506b6eea09 fix rss ui 2023-09-06 20:18:45 +08:00
jxxghp
403ee4b925 fix 2023-09-06 17:26:20 +08:00
jxxghp
193d1f550f fix ui 2023-09-06 15:50:05 +08:00
jxxghp
d2a02a830c fix restart ui 2023-09-06 15:30:10 +08:00
jxxghp
6cc9c1ac57 feat 内建重启 2023-09-06 12:56:49 +08:00
jxxghp
fffb1c6c02 v1.1.4 2023-09-05 19:52:01 +08:00
jxxghp
985d1baff5 feat 新增紫色主题 2023-09-05 10:15:20 +08:00
jxxghp
a70e467b69 fix ui 2023-09-05 09:53:02 +08:00
jxxghp
b07010bebd Merge pull request #35 from amtoaer/memory_percent 2023-09-04 23:18:20 +08:00
amtoaer
353bdc5989 feat: 内存占用图使用百分比 2023-09-04 23:05:16 +08:00
jxxghp
f135804c4b fix ui 2023-09-04 21:30:03 +08:00
jxxghp
ae6d0ead2c v1.1.3 2023-09-04 21:13:27 +08:00
jxxghp
6db3ad4e0d fix ui 2023-09-04 20:07:59 +08:00
jxxghp
09e42d5a08 build 2023-09-04 17:56:32 +08:00
jxxghp
b9a09fd1be fix 用户权限控制 2023-09-04 17:49:17 +08:00
jxxghp
b08235b9f6 fix ui 2023-09-04 12:37:34 +08:00
jxxghp
43e67893b4 fix ui 2023-09-04 12:36:05 +08:00
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
56 changed files with 1229 additions and 1478 deletions

View File

@@ -34,13 +34,6 @@ jobs:
yarn build
zip -r dist.zip dist
- name: Delete Release
uses: dev-drprasad/delete-tag-and-release@v1.0
with:
tag_name: ${{ env.frontend_version }}
github_token: ${{ secrets.GITHUB_TOKEN }}
delete_release: true
- name: Generate Release
uses: softprops/action-gh-release@v1
with:

View File

@@ -1,6 +1,6 @@
{
"name": "moviepilot",
"version": "1.1.2",
"version": "1.2.0-1",
"private": true,
"scripts": {
"dev": "vite --host",

BIN
public/plugin/brush.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
public/plugin/delete.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -17,17 +17,18 @@
// This mixin is inspired from vuetify for adding hover styles via before pseudo element
@mixin before-pseudo() {
position: relative;
&::before {
position: absolute;
background: currentcolor;
block-size: 100%;
border-radius: inherit;
content: "";
inline-size: 100%;
inset: 0;
opacity: 0;
pointer-events: none;
@media (hover) {
&::before {
position: absolute;
background: currentcolor;
block-size: 100%;
border-radius: inherit;
content: "";
inline-size: 100%;
inset: 0;
opacity: 0;
pointer-events: none;
}
}
}

View File

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

View File

@@ -8,17 +8,18 @@
// This mixin is inspired from vuetify for adding hover styles via before pseudo element
@mixin before-pseudo() {
position: relative;
&::before {
position: absolute;
border-radius: inherit;
background: currentcolor;
block-size: 100%;
content: "";
inline-size: 100%;
inset: 0;
opacity: 0;
pointer-events: none;
@media (hover) {
&::before {
position: absolute;
border-radius: inherit;
background: currentcolor;
block-size: 100%;
content: "";
inline-size: 100%;
inset: 0;
opacity: 0;
pointer-events: none;
}
}
}

View File

@@ -85,7 +85,7 @@ export default defineComponent({
<style scoped>
* {
backface-visibility: hidden;
perspective: 1000px;
perspective: 62.5rem;
transform: translateZ(0);
will-change: block-size;
}

View File

@@ -111,7 +111,7 @@ export default defineComponent({
.layout-navbar {
position: fixed;
width: calc(100vw - variables.$layout-vertical-nav-width - 1rem);
width: calc(100vw - variables.$layout-vertical-nav-width - 0.5rem);
z-index: variables.$layout-vertical-nav-layout-navbar-z-index;
inset-block-start: 0;

View File

@@ -38,7 +38,7 @@ body,
overflow: hidden;
// TODO: Use grid gutter variable here
padding-block: 1.5rem;
padding-top: calc(env(safe-area-inset-top) + 65px);
padding-top: calc(env(safe-area-inset-top) + 4.25rem);
// display: flex;
@@ -49,7 +49,7 @@ body,
& > div:first-child {
flex: auto;
position: relative;
width: calc(100vw - variables.$layout-vertical-nav-width - 1rem);
width: calc(100vw - variables.$layout-vertical-nav-width - 0.5rem);
}
}
}

View File

@@ -2,24 +2,24 @@
// 👉 Vertical nav
$layout-vertical-nav-z-index: 12 !default;
$layout-vertical-nav-width: 260px !default;
$layout-vertical-nav-collapsed-width: 80px !default;
$layout-vertical-nav-width: 16.25rem !default;
$layout-vertical-nav-collapsed-width: 5rem !default;
// 👉 Horizontal nav
$layout-horizontal-nav-z-index: 11 !default;
$layout-horizontal-nav-navbar-height: 64px !default;
$layout-horizontal-nav-navbar-height: 4rem !default;
// 👉 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-layout-navbar-z-index: 11 !default;
$layout-horizontal-nav-layout-navbar-z-index: 11 !default;
// 👉 Main content
$layout-boxed-content-width: 1440px !default;
$layout-boxed-content-width: 90rem !default;
// 👉Footer
$layout-vertical-nav-footer-height: 56px !default;
$layout-vertical-nav-footer-height: 3.5rem !default;
// 👉 Layout overlay
$layout-overlay-z-index: 11 !default;

View File

@@ -1,10 +1,7 @@
<script lang="ts" setup>
import { useToast } from 'vue-toast-notification'
import { useTheme } from 'vuetify'
import api from './api'
import type { User } from './api/types'
import store from './store'
import avatar1 from '@images/avatars/avatar-1.png'
// 第一时间应用主题
const { global: globalTheme } = useTheme()
@@ -36,39 +33,10 @@ function startSSEMessager() {
}
}
// 当前用户信息
const accountInfo = ref<User>({
id: 0,
name: '',
password: '',
email: '',
is_active: false,
is_superuser: false,
avatar: avatar1,
})
// 调用API加载当前用户数据
async function loadAccountInfo() {
try {
const user: User = await api.get('user/current')
accountInfo.value = user
if (!accountInfo.value.avatar)
accountInfo.value.avatar = avatar1
}
catch (error) {
console.log(error)
}
}
// 页面加载时,加载当前用户数据
onBeforeMount(async () => {
await loadAccountInfo()
startSSEMessager()
})
// 提供给所有元素复用
provide('accountInfo', accountInfo)
</script>
<template>

View File

@@ -526,6 +526,9 @@ export interface Plugin {
// 运行状态
state?: boolean
// 是否有详情页面
has_page?: boolean
}
// 种子信息
@@ -837,55 +840,6 @@ export interface Setting {
DOWNLOAD_PATH: string
}
// 自定义订阅
export interface Rss {
id?: number
// 名称
name?: string
// RSS地址
url?: string
// 类型
type?: string
// 标题
title?: string
// 年份
year?: string
// TMDBID
tmdbid?: number
// 季号
season?: number
// 海报
poster?: string
// 背景图
backdrop?: string
// 评分
vote?: number
// 简介
description?: string
// 总集数
total_episode?: number
// 包含
include?: string
// 排除
exclude?: string
// 洗版
best_version?: number
// 是否使用代理服务器
proxy?: number
// 是否使用过滤规则
filter?: boolean
// 保存路径
save_path?: string
// 已处理数量
processed?: number
// 附加信息
note?: string
// 最后更新时间
last_update?: string
// 状态 0-停用1-启用
state?: number
}
// 文件浏览接口
export interface EndPoints {
list: any
@@ -905,4 +859,5 @@ export interface FileItem {
extension: string
size: number
children: FileItem[]
modify_time: number
}

View File

@@ -57,6 +57,8 @@ const loading = ref(0)
const activeStorage = ref('local')
// 刷新
const refreshPending = ref(false)
// 排序
const sort = ref('name')
// axios实例
const axiosInstance = ref<Axios>()
@@ -83,6 +85,12 @@ function pathChanged(_path: string) {
emit('pathchanged', _path)
}
// 排序变化
function sortChanged(s: string) {
sort.value = s
refreshPending.value = true
}
// 初始化
onBeforeMount(() => {
activeStorage.value = props.storage ?? 'local'
@@ -101,6 +109,7 @@ onBeforeMount(() => {
@storagechanged="storageChanged"
@pathchanged="pathChanged"
@foldercreated="refreshPending = true"
@sortchanged="sortChanged"
/>
<VRow no-gutters>
<VCol v-if="tree" sm="auto" class="d-none d-md-block">
@@ -125,6 +134,7 @@ onBeforeMount(() => {
:endpoints="endpoints"
:axios="axiosInstance"
:refreshpending="refreshPending"
:sort="sort"
@pathchanged="pathChanged"
@loading="loadingChanged"
@refreshed="refreshPending = false"

View File

@@ -50,9 +50,6 @@ const selectFilterOptions = ref<{ [key: string]: string }[]>([
{ title: '排除: 国语配音', value: ' !CNVOI ' },
{ title: '促销: 免费', value: ' FREE ' },
])
// 已选择的过滤规则
const selectedFilters = ref<string[]>(props.rules ?? [])
</script>
<template>
@@ -64,7 +61,7 @@ const selectedFilters = ref<string[]>(props.rules ?? [])
<VCol>
<VSelect
:key="props.pri"
v-model="selectedFilters"
v-model="props.rules"
variant="underlined"
:items="selectFilterOptions"
chips

View File

@@ -462,7 +462,7 @@ const getImgUrl: Ref<string> = computed(() => {
</VHover>
<VDialog
v-model="subscribeSeasonDialog"
max-width="600"
max-width="50rem"
content-class="whitespace-nowrap"
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 class="text-center text-md-left">
{{ context?.media_info?.title || context?.meta_info?.name }}
{{ context?.meta_info?.season_episode }}
</VCardTitle>
<VCardSubtitle class="text-center text-md-left">
{{ 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 text-center text-md-left ..."
>
{{ context?.media_info?.overview }}
</VCardText>
<VCardItem class="text-center text-md-left">
<!-- 类型 -->
<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

@@ -53,7 +53,7 @@ async function installPlugin() {
:style="{ background: `${props.plugin?.plugin_color}` }"
>
<VAvatar
size="128"
size="8rem"
:class="{ shadow: isImageLoaded }"
>
<VImg

View File

@@ -119,6 +119,8 @@ async function savePluginConf() {
// 显示插件详情
async function showPluginInfo() {
// 加载详情
await loadPluginPage()
pluginConfigDialog.value = false
pluginInfoDialog.value = true
}
@@ -129,17 +131,35 @@ async function showPluginConfig() {
await loadPluginForm()
// 加载配置
await loadPluginConf()
// 加载详情
await loadPluginPage()
// 显示对话框
pluginInfoDialog.value = false
pluginConfigDialog.value = true
}
// 弹出菜单
const dropdownItems = ref([
{
title: '卸载',
title: '查看详情',
value: 1,
show: props.plugin?.has_page,
props: {
prependIcon: 'mdi-information-outline',
click: showPluginInfo,
},
},
{
title: '配置',
value: 2,
show: true,
props: {
prependIcon: 'mdi-cog-outline',
click: showPluginConfig,
},
},
{
title: '卸载',
value: 3,
show: true,
props: {
prependIcon: 'mdi-trash-can-outline',
color: 'error',
@@ -155,7 +175,12 @@ const dropdownItems = ref([
v-if="isVisible"
:width="props.width"
:height="props.height"
@click="showPluginConfig"
@click="() => {
if (props.plugin?.has_page)
showPluginInfo()
else
showPluginConfig()
}"
>
<div
class="relative pa-4 text-center card-cover-blurred"
@@ -171,6 +196,7 @@ const dropdownItems = ref([
<VList>
<VListItem
v-for="(item, i) in dropdownItems"
v-show="item.show"
:key="i"
variant="plain"
:base-color="item.props.color"
@@ -186,7 +212,7 @@ const dropdownItems = ref([
</IconBtn>
</div>
<VAvatar
size="128"
size="8rem"
:class="{ shadow: isImageLoaded }"
>
<VImg
@@ -210,11 +236,11 @@ const dropdownItems = ref([
<!-- 插件配置页面 -->
<VDialog
v-model="pluginConfigDialog"
max-width="800"
max-width="50rem"
scrollable
persistent
>
<VCard :title="props.plugin?.plugin_name">
<VCard :title="`${props.plugin?.plugin_name} - 配置`">
<DialogCloseBtn @click="pluginConfigDialog = false" />
<VCardText>
<FormRender
@@ -226,7 +252,7 @@ const dropdownItems = ref([
</VCardText>
<VCardActions>
<VBtn v-if="pluginPageItems.length > 0" @click="showPluginInfo">
详情
查看详情
</VBtn>
<VSpacer />
<VBtn @click="savePluginConf">
@@ -239,11 +265,11 @@ const dropdownItems = ref([
<!-- 插件详情页面 -->
<VDialog
v-model="pluginInfoDialog"
max-width="1000"
max-width="62.5rem"
scrollable
persistent
>
<VCard :title="`${props.plugin?.plugin_name} - 详情`">
<VCard :title="`${props.plugin?.plugin_name}`">
<DialogCloseBtn @click="pluginInfoDialog = false" />
<VCardText>
<PageRender
@@ -253,6 +279,9 @@ const dropdownItems = ref([
/>
</VCardText>
<VCardActions>
<VBtn @click="showPluginConfig">
配置
</VBtn>
<VSpacer />
<VBtn @click="pluginInfoDialog = false">
关闭

View File

@@ -1,590 +0,0 @@
<script lang="ts" setup>
import { useToast } from 'vue-toast-notification'
import { calculateTimeDifference } from '@/@core/utils'
import { formatFileSize, formatSeason } from '@/@core/utils/formatters'
import api from '@/api'
import type { Rss, Site, TorrentInfo } from '@/api/types'
// 输入参数
const props = defineProps({
media: Object as PropType<Rss>,
})
// 定义触发的自定义事件
const emit = defineEmits(['remove', 'save'])
// 提示框
const $toast = useToast()
// 图片是否加载完成
const imageLoaded = ref(false)
// 订阅弹窗
const rssInfoDialog = ref(false)
// RSS预览窗口
const rssPreviewDialog = ref(false)
// 加载状态
const previewLoading = ref(false)
// 总条数
const previewTotalItems = ref(0)
// 每页条数
const previewItemsPerPage = ref(25)
// 预览表头
const previewHeaders = [
{ title: '标题', key: 'title', sortable: true },
{ title: '时间', key: 'pubdate', sortable: true },
{ title: '大小', key: 'size', sortable: true },
{ title: '', key: 'actions', sortable: false },
]
// 预览数据
const previewDataList = ref<TorrentInfo[]>([])
// 站点名称
const siteName = ref('')
// 订阅编辑表单
const rssForm = reactive<any>(props.media ?? {})
// 类型转换
rssForm.best_version = rssForm.best_version === 1
rssForm.proxy = rssForm.proxy === 1
rssForm.filter = rssForm.filter === 1
// 上一次更新时间
const lastUpdateText = ref(
`${
props.media?.last_update
? `${calculateTimeDifference(props.media?.last_update || '')}`
: ''
}`,
)
// 图片加载完成响应
function imageLoadHandler() {
imageLoaded.value = true
}
// 根据 type 返回不同的图标
function getIcon() {
if (props.media?.type === '电影')
return 'mdi-movie'
else if (props.media?.type === '电视剧')
return 'mdi-television-classic'
else
return 'mdi-help-circle'
}
// 计算文本颜色
function getTextColor() {
return imageLoaded.value ? 'white' : ''
}
// 计算文本类
function getTextClass() {
return imageLoaded.value ? 'text-white' : ''
}
// 删除订阅
async function removerRss() {
try {
const result: { [key: string]: any } = await api.delete(
`rss/${props.media?.id}`,
)
if (result.success) {
// 通知父组件刷新
emit('remove')
}
}
catch (e) {
console.log(e)
}
}
// 调用API修改订阅
async function updateRssInfo() {
rssInfoDialog.value = false
try {
const result: { [key: string]: any } = await api.put('rss', rssForm)
// 提示
if (result.success) {
$toast.success(`${props.media?.name} 更新成功!`)
// 通知父组件刷新
emit('remove')
}
else { $toast.error(`${props.media?.name} 更新失败:${result.message}`) }
}
catch (e) {
console.log(e)
}
}
// 查询站点名称
async function querySiteName() {
try {
const result: Site = await api.get(
`site/domain/${props.media?.url?.split('/')[2]}`,
)
if (result)
siteName.value = result.name
}
catch (e) {
// 截取URL中的主域名作为站点名称
siteName.value = props.media?.url?.split('/')[2] ?? '未知'
console.log(e)
}
}
// 预览按钮响应
async function handleRssPreview() {
rssPreviewDialog.value = true
previewLoading.value = true
await previewRss()
previewLoading.value = false
}
// 预览站点RSS
async function previewRss() {
try {
const result: TorrentInfo[] = await api.get(
`rss/preview/${props.media?.id}`,
)
previewDataList.value = result
}
catch (e) {
console.log(e)
}
}
// 编辑订阅响应
async function editRssDialog() {
rssInfoDialog.value = true
}
// 刷新按钮响应
async function refreshRss() {
try {
const result: { [key: string]: any } = await api.get(
`rss/refresh/${props.media?.id}`,
)
if (result.success)
$toast.success(`${props.media?.name} 已提交刷新任务!`)
}
catch (e) {
console.log(e)
}
}
// 生成1到50季的下拉框选项
const seasonItems = ref(
Array.from({ length: 50 }, (_, i) => i + 1).map(item => ({
title: `${item}`,
value: item,
})),
)
// 打开种子详情页面
function openTorrentDetail(page_url: string) {
window.open(page_url, '_blank')
}
// 下载种子文件
async function downloadTorrentFile(enclosure: string) {
window.open(enclosure, '_blank')
}
// 弹出菜单
const dropdownItems = ref([
{
title: '编辑',
value: 1,
props: {
prependIcon: 'mdi-file-edit-outline',
click: editRssDialog,
},
},
{
title: '预览',
value: 2,
props: {
prependIcon: 'mdi-eye-outline',
click: handleRssPreview,
},
},
{
title: '刷新',
value: 3,
props: {
prependIcon: 'mdi-refresh',
click: refreshRss,
},
},
{
title: '取消订阅',
value: 4,
props: {
prependIcon: 'mdi-trash-can-outline',
color: 'error',
click: removerRss,
},
},
])
onMounted(() => {
querySiteName()
})
</script>
<template>
<VCard
:key="props.media?.id"
:class="`${rssForm.best_version ? 'outline-dashed outline-1' : ''}`"
@click="editRssDialog"
>
<template #image>
<VImg
:src="props.media?.backdrop || props.media?.poster"
aspect-ratio="2/3"
cover
class="brightness-50"
@load="imageLoadHandler"
/>
</template>
<VCardItem>
<template #prepend>
<VIcon
size="1.9rem"
:color="getTextColor()"
:icon="getIcon()"
/>
</template>
<VCardTitle :class="getTextClass()">
{{ props.media?.name }}
{{ formatSeason(props.media?.season ? props.media?.season.toString() : "") }}
</VCardTitle>
<template #append>
<div class="me-n3">
<IconBtn>
<VIcon
icon="mdi-dots-vertical"
:color="getTextColor()"
/>
<VMenu
activator="parent"
close-on-content-click
>
<VList>
<VListItem
v-for="(item, i) in dropdownItems"
:key="i"
variant="plain"
:base-color="item.props.color"
@click="item.props.click"
>
<template #prepend>
<VIcon :icon="item.props.prependIcon" />
</template>
<VListItemTitle v-text="item.title" />
</VListItem>
</VList>
</VMenu>
</IconBtn>
</div>
</template>
</VCardItem>
<VCardText>
<p
class="clamp-text mb-0"
:class="getTextClass()"
>
{{ props.media?.description }}
</p>
</VCardText>
<VCardText class="d-flex justify-space-between align-center flex-wrap">
<div class="d-flex align-center">
<IconBtn
icon="mdi-star"
:color="getTextColor()"
class="me-1"
/>
<span
class="text-subtitle-2 me-4"
:class="getTextClass()"
>{{
props.media?.vote
}}</span>
<IconBtn
v-bind="props"
icon="mdi-progress-clock"
:color="getTextColor()"
class="me-1"
/>
<span
class="text-subtitle-2 me-4"
:class="getTextClass()"
>{{ props.media?.processed || 0 }}</span>
<IconBtn
v-if="siteName"
icon="mdi-web"
:color="getTextColor()"
class="me-1"
/>
<span
v-if="siteName"
class="text-subtitle-2 me-4"
:class="getTextClass()"
>
{{ siteName }}
</span>
</div>
</VCardText>
<VCardText
v-if="lastUpdateText"
class="absolute right-0 bottom-0 d-flex align-center p-2 text-gray-300"
>
<VIcon
icon="mdi-download"
class="me-1"
/> {{ lastUpdateText }}
</VCardText>
</VCard>
<!-- 订阅编辑弹窗 -->
<VDialog
v-model="rssInfoDialog"
max-width="800"
persistent
scrollable
>
<!-- Dialog Content -->
<VCard :title="`订阅 - ${props.media?.name}`">
<VCardText class="pt-2">
<VForm @submit.prevent="() => {}">
<VRow>
<VCol
cols="12"
>
<VTextField
v-model="rssForm.url"
label="RSS地址"
placeholder="https://example.com/rss"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<VSelect
v-model="rssForm.type"
label="类型"
:items="[{ title: '电影', value: '电影' }, { title: '电视剧', value: '电视剧' }]"
readonly
/>
</VCol>
<VCol
cols="12"
md="6"
>
<VTextField
v-model="rssForm.title"
label="标题"
readonly
/>
</VCol>
<VCol
cols="12"
md="6"
>
<VTextField
v-model="rssForm.year"
label="年份"
readonly
/>
</VCol>
<VCol
cols="12"
md="6"
>
<VSelect
v-show="rssForm.type === '电视剧'"
v-model="rssForm.season"
label="季"
:items="seasonItems"
readonly
/>
</VCol>
<VCol
cols="12"
>
<VTextField
v-model="rssForm.include"
label="包含"
/>
</VCol>
<VCol
cols="12"
>
<VTextField
v-model="rssForm.exclude"
label="排除"
/>
</VCol>
<VCol
cols="12"
>
<VTextField
v-model="rssForm.save_path"
label="保存路径"
placeholder="留空自动"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<VSelect
v-model="rssForm.state"
label="状态"
:items="[{
title: '启用',
value: 1,
}, {
title: '停用',
value: 0,
}]"
/>
</VCol>
</VRow>
<VRow>
<VCol
cols="12"
md="4"
>
<VSwitch
v-model="rssForm.best_version"
label="洗版"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VSwitch
v-model="rssForm.proxy"
label="代理服务器"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VSwitch
v-model="rssForm.filter"
label="过滤规则"
/>
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardActions>
<VBtn @click="rssInfoDialog = false">
取消
</VBtn>
<VSpacer />
<VBtn @click="updateRssInfo">
确定
</VBtn>
</VCardActions>
</VCard>
</VDialog>
<!-- RSS预览窗口 -->
<VDialog
v-model="rssPreviewDialog"
max-width="1280"
scrollable
>
<!-- Dialog Content -->
<VCard title="RSS预览">
<DialogCloseBtn @click="rssPreviewDialog = false" />
<VCardText class="pt-2">
<VDataTable
v-model:items-per-page="previewItemsPerPage"
:headers="previewHeaders"
:items="previewDataList"
:items-length="previewTotalItems"
:loading="previewLoading"
density="compact"
item-value="title"
return-object
fixed-header
items-per-page-text="每页条数"
page-text="{0}-{1} {2} "
>
<template #item.title="{ item }">
<div class="text-high-emphasis">
{{ item.raw.title }}
</div>
</template>
<template #item.size="{ item }">
<div class="text-nowrap whitespace-nowrap">
{{ formatFileSize(item.raw.size) }}
</div>
</template>
<template #item.pubdate="{ item }">
<div class="text-sm">
{{ item.raw.pubdate }}
</div>
</template>
<template #item.actions="{ item }">
<div class="me-n3">
<IconBtn>
<VIcon
icon="mdi-dots-vertical"
/>
<VMenu
activator="parent"
close-on-content-click
>
<VList>
<VListItem
variant="plain"
@click="openTorrentDetail(item.raw.page_url)"
>
<template #prepend>
<VIcon icon="mdi-information" />
</template>
<VListItemTitle>查看详情</VListItemTitle>
</VListItem>
<VListItem
variant="plain"
@click="downloadTorrentFile(item.raw.enclosure)"
>
<template #prepend>
<VIcon icon="mdi-download" />
</template>
<VListItemTitle>下载种子</VListItemTitle>
</VListItem>
</VList>
</VMenu>
</IconBtn>
</div>
</template>
<template #no-data>
没有数据
</template>
</VDataTable>
</VCardText>
</VCard>
</VDialog>
</template>

View File

@@ -72,9 +72,6 @@ const resourceTotalItems = ref(0)
// 每页条数
const resourceItemsPerPage = ref(25)
// 当前页码
const resourceCurrentPage = ref(0)
// 用户名密码表单
const userPwForm = ref({
username: '',
@@ -244,13 +241,7 @@ function getVolumeFactorClass(downloadVolume: number, uploadVolume: number) {
async function getResourceList() {
resourceLoading.value = true
try {
resourceDataList.value = await api.get('search/title', {
params: {
keyword: resourceSearch.value,
page: resourceCurrentPage.value,
site: cardProps.site?.id,
},
})
resourceDataList.value = await api.get(`site/resource/${cardProps.site?.id}`)
resourceLoading.value = false
}
catch (error) {
@@ -389,7 +380,7 @@ onMounted(() => {
<!-- 更新站点Cookie & UA弹窗 -->
<VDialog
v-model="siteCookieDialog"
max-width="600"
max-width="50rem"
>
<!-- Dialog Content -->
<VCard title="更新站点Cookie & UA">
@@ -437,7 +428,7 @@ onMounted(() => {
<!-- 站点编辑弹窗 -->
<VDialog
v-model="siteInfoDialog"
max-width="1000"
max-width="50rem"
persistent
scrollable
>
@@ -480,6 +471,12 @@ onMounted(() => {
</VCol>
</VRow>
<VRow>
<VCol cols="12">
<VTextField
v-model="siteForm.rss"
label="RSS地址"
/>
</VCol>
<VCol cols="12">
<VTextarea
v-model="siteForm.cookie"
@@ -562,7 +559,7 @@ onMounted(() => {
<!-- 站点资源弹窗 -->
<VDialog
v-model="resourceDialog"
max-width="1280"
max-width="80rem"
scrollable
>
<!-- Dialog Content -->

View File

@@ -325,7 +325,7 @@ const dropdownItems = ref([
<!-- 订阅编辑弹窗 -->
<VDialog
v-model="subscribeInfoDialog"
max-width="1000"
max-width="50rem"
persistent
scrollable
>

View File

@@ -0,0 +1,128 @@
<script lang="ts" setup>
import api from '@/api'
import type { MediaInfo } from '@/api/types'
interface TmdbItem {
title: string
overview: string
tmdbid: number
poster: string
}
// update:modelValue 事件
const emit = defineEmits(['update:modelValue', 'close'])
const items = ref<TmdbItem[]>([])
// 搜索词
const keyword = ref('')
// 加载中
const loading = ref(false)
// 选中条目
function selectMedia(item: TmdbItem) {
console.log(item)
emit('update:modelValue', item.tmdbid)
emit('close')
}
// TMDB图片转换为w500大小
function getW500Image(url = '') {
if (!url)
return ''
return url.replace('original', 'w500')
}
// 搜索词条
async function searchMedias() {
if (!keyword)
return
// 调用API搜索词条
try {
loading.value = true
const result: MediaInfo[] = await api.get('media/search', {
params: {
title: keyword.value,
page: 1,
count: 20,
},
})
// 清空
items.value = []
// 赋值
for (const item of result) {
items.value.push({
tmdbid: item.tmdb_id || 0,
poster: getW500Image(item.poster_path),
title: `${item.title}${item.year}`,
overview: `<span class="text-primary">${item.type}</span> ${item.overview}`,
})
}
loading.value = false
}
catch (e) {
console.error(e)
}
}
</script>
<template>
<VCard
class="mx-auto"
width="100%"
>
<VToolbar flat dense>
<VTextField
v-model="keyword"
density="compact"
label="输入名称搜索"
single-line
hide-details
flat
class="mx-3"
append-inner-icon="mdi-magnify"
:loading="loading"
@click:append-inner="searchMedias"
@keydown.enter="searchMedias"
/>
</VToolbar>
<VList
v-if="items.length > 0"
lines="three"
>
<template v-for="(item, i) in items" :key="i">
<VListItem
density="compact"
@click="selectMedia(item)"
>
<template #prepend>
<VImg
height="75"
width="50"
:src="item.poster"
aspect-ratio="2/3"
class="object-cover rounded shadow ring-gray-500 me-3"
cover
>
<template #placeholder>
<div class="w-full h-full">
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
</div>
</template>
</VImg>
</template>
<VListItemTitle>
{{ item.title }}
</VListItemTitle>
<VListItemSubtitle v-html="item.overview" />
</VListItem>
<VDivider v-if="i < items.length - 1" class="mt-1" inset />
</template>
</VList>
</VCard>
</template>

View File

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

View File

@@ -9,6 +9,8 @@ import { formatBytes } from '@core/utils/formatters'
import type { Context, EndPoints, FileItem } from '@/api/types'
import store from '@/store'
import api from '@/api'
import MediaInfoCard from '@/components/cards/MediaInfoCard.vue'
import TmdbSelectorCard from '@/components/cards/TmdbSelectorCard.vue'
// 输入参数
const inProps = defineProps({
@@ -18,6 +20,7 @@ const inProps = defineProps({
endpoints: Object as PropType<EndPoints>,
axios: Object as PropType<Axios>,
refreshpending: Boolean,
sort: String,
})
// 对外事件
@@ -87,6 +90,12 @@ const transferForm = reactive({
// 识别结果
const nameTestResult = ref<Context>()
// 识别结果对话框
const nameTestDialog = ref(false)
// TMDB选择对话框
const tmdbSelectorDialog = ref(false)
// 生成1到50季的下拉框选项
const seasonItems = ref(
Array.from({ length: 51 }, (_, i) => i).map(item => ({
@@ -121,18 +130,18 @@ const isImage = computed(() => {
async function load() {
loading.value = true
emit('loading', true)
if (isDir.value) {
const url = inProps.endpoints?.list.url
.replace(/{storage}/g, storage.value)
.replace(/{path}/g, encodeURIComponent(inProps.path || ''))
// 参数
const url = inProps.endpoints?.list.url
.replace(/{storage}/g, storage.value)
.replace(/{path}/g, encodeURIComponent(inProps.path || ''))
.replace(/{sort}/g, inProps.sort || 'name')
const config = {
url,
method: inProps.endpoints?.list.method || 'get',
}
// 加载数据
items.value = await axiosInstance.value.request(config) ?? []
const config = {
url,
method: inProps.endpoints?.list.method || 'get',
}
// 加载数据
items.value = await axiosInstance.value.request(config) ?? []
emit('loading', false)
loading.value = false
}
@@ -147,7 +156,7 @@ async function deleteItem(item: FileItem) {
confirmationText: '确认',
cancellationText: '取消',
dialogProps: {
maxWidth: 600,
maxWidth: '50rem',
},
})
@@ -257,7 +266,7 @@ async function transfer() {
stopLoadingProgress()
// 显示结果
if (res.success) {
$toast.success(`${currentItem.value?.name} 整理成`)
$toast.success(`${currentItem.value?.name} 整理成!`)
// 重新加载
load()
}
@@ -271,12 +280,18 @@ async function transfer() {
}
}
// 将文件修改时间timestape转换为本地时间
function formatTime(timestape: number) {
return new Date(timestape * 1000).toLocaleString()
}
// 监听path变化
watch(
() => inProps.path,
async () => {
items.value = []
nameTestResult.value = undefined
nameTestDialog.value = false
await load()
},
)
@@ -321,6 +336,7 @@ async function recognize(path: string) {
// 显示进度条
progressDialog.value = true
progressText.value = `正在识别 ${path} ...`
progressValue.value = 0
nameTestResult.value = await api.get('media/recognize_file', {
params: {
path,
@@ -330,24 +346,27 @@ async function recognize(path: string) {
progressDialog.value = false
if (!nameTestResult.value)
$toast.error(`${path} 识别失败!`)
nameTestDialog.value = !!nameTestResult.value?.meta_info?.name
}
catch (error) {
console.error(error)
}
}
// TMDB图片转换为w500大小
function getW500Image(url = '') {
if (!url)
return ''
return url.replace('original', 'w500')
}
// 弹出菜单
const dropdownItems = ref([
{
title: '重命名',
title: '识别',
value: 1,
props: {
prependIcon: 'mdi-text-recognition',
click: (_item: FileItem) => {
recognize(_item.path || '')
},
},
}, {
title: '重命名',
value: 2,
props: {
prependIcon: 'mdi-rename',
click: showRenmae,
@@ -355,7 +374,7 @@ const dropdownItems = ref([
},
{
title: '整理',
value: 2,
value: 3,
props: {
prependIcon: 'mdi-folder-arrow-right',
click: showTransfer,
@@ -363,7 +382,7 @@ const dropdownItems = ref([
},
{
title: '删除',
value: 3,
value: 4,
props: {
prependIcon: 'mdi-delete-outline',
color: 'error',
@@ -399,116 +418,9 @@ onMounted(() => {
v-else-if="isFile && !isImage"
class="text-center break-all"
>
文件: {{ path }}
<VDivider v-if="nameTestResult" class="my-3" />
<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 class="text-start">
<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"
>
{{ 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>
<strong>{{ items[0]?.name }}</strong><br>
大小{{ formatBytes(items[0]?.size || 0) }}<br>
修改时间{{ formatTime(items[0]?.modify_time || 0) }}
</VCardText>
<VCardText
v-else-if="isFile && isImage"
@@ -554,6 +466,9 @@ onMounted(() => {
</VList>
</VMenu>
</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)">
<VIcon icon="mdi-rename" />
</IconBtn>
@@ -607,6 +522,9 @@ onMounted(() => {
</VList>
</VMenu>
</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)">
<VIcon icon="mdi-rename" />
</IconBtn>
@@ -666,7 +584,7 @@ onMounted(() => {
<!-- 重命名弹窗 -->
<VDialog
v-model="renamePopper"
max-width="600"
max-width="50rem"
>
<template #activator="{ props }">
<IconBtn title="重命名" v-bind="props">
@@ -695,7 +613,7 @@ onMounted(() => {
<!-- 文件整理弹窗 -->
<VDialog
v-model="transferPopper"
max-width="800"
max-width="50rem"
scrollable
>
<template #activator="{ props }">
@@ -753,6 +671,8 @@ onMounted(() => {
label="TMDBID"
placeholder="留空自动识别"
:rules="[numberValidator]"
append-inner-icon="mdi-magnify"
@click:append-inner="tmdbSelectorDialog = true"
/>
</VCol>
<VCol
@@ -808,10 +728,10 @@ onMounted(() => {
</VForm>
</VCardText>
<VCardActions>
<div class="flex-grow-1" />
<VBtn depressed @click="transferPopper = false">
取消
</VBtn>
<VSpacer />
<VBtn
@click="transfer"
>
@@ -824,7 +744,7 @@ onMounted(() => {
<vDialog
v-model="progressDialog"
:scrim="false"
width="400"
width="25rem"
>
<vCard
color="primary"
@@ -832,6 +752,7 @@ onMounted(() => {
<vCardText class="text-center">
{{ progressText }}
<vProgressLinear
v-if="progressValue"
color="white"
class="mb-0 mt-1"
:model-value="progressValue"
@@ -839,6 +760,29 @@ onMounted(() => {
</vCardText>
</vCard>
</vDialog>
<!-- 识别结果对话框 -->
<vDialog
v-model="nameTestDialog"
width="50rem"
>
<vCard>
<DialogCloseBtn @click="nameTestDialog = false" />
<VCardItem>
<MediaInfoCard :context="nameTestResult" />
</VCardItem>
</vCard>
</vDialog>
<!-- TMDB ID搜索框 -->
<vDialog
v-model="tmdbSelectorDialog"
width="40rem"
scrollable
>
<TmdbSelectorCard
v-model="transferForm.tmdbid"
@close="tmdbSelectorDialog = false"
/>
</vDialog>
</template>
<style lang="scss" scoped>

View File

@@ -12,7 +12,7 @@ const inProps = defineProps({
})
// 对外事件
const emit = defineEmits(['storagechanged', 'pathchanged', 'loading', 'foldercreated'])
const emit = defineEmits(['storagechanged', 'pathchanged', 'loading', 'foldercreated', 'sortchanged'])
// 新建文件夹名称
const newFolderPopper = ref(false)
@@ -20,6 +20,19 @@ const newFolderPopper = ref(false)
// 新建文件名称
const newFolderName = ref('')
// 排序方式
const sort = ref('name')
// 调整排序方式
function changeSort() {
if (sort.value === 'name')
sort.value = 'time'
else
sort.value = 'name'
emit('sortchanged', sort.value)
}
// 计算PATH面包屑
const pathSegments = computed(() => {
let path_str = ''
@@ -81,6 +94,14 @@ async function mkdir() {
// 通知重新加载
emit('foldercreated')
}
// 计算排序图标
const sortIcon = computed(() => {
if (sort.value === 'time')
return 'mdi-sort-clock-ascending-outline'
else
return 'mdi-sort-alphabetical-ascending'
})
</script>
<template>
@@ -123,12 +144,15 @@ async function mkdir() {
</template>
</VToolbarItems>
<div class="flex-grow-1" />
<IconBtn @click="changeSort">
<VIcon :icon="sortIcon" />
</IconBtn>
<IconBtn v-if="pathSegments.length > 0" @click="goUp">
<VIcon icon="mdi-arrow-up-bold-outline" />
</IconBtn>
<VDialog
v-model="newFolderPopper"
max-width="600"
max-width="50rem"
>
<template #activator="{ props }">
<IconBtn title="新建文件夹" v-bind="props">
@@ -156,9 +180,3 @@ async function mkdir() {
</VDialog>
</VToolbar>
</template>
<style lang="scss" scoped>
.v-toolbar{
background: rgb(var(--v-table-header-background));
}
</style>

View File

@@ -35,6 +35,7 @@ function init() {
name: 'root',
children: [],
size: 0,
modify_time: 0,
}]
}

View File

@@ -9,6 +9,10 @@ import NavbarThemeSwitcher from '@/layouts/components/NavbarThemeSwitcher.vue'
import SearchBar from '@/layouts/components/SearchBar.vue'
import ShortcutBar from '@/layouts/components/ShortcutBar.vue'
import UserProfile from '@/layouts/components/UserProfile.vue'
import store from '@/store'
// 从Vuex Store中获取superuser信息
const superUser = store.state.auth.superUser
</script>
<template>
@@ -87,6 +91,7 @@ import UserProfile from '@/layouts/components/UserProfile.vue'
}"
/>
<VerticalNavLink
v-if="superUser"
:item="{
title: '电影',
icon: 'mdi-movie-check-outline',
@@ -94,19 +99,13 @@ import UserProfile from '@/layouts/components/UserProfile.vue'
}"
/>
<VerticalNavLink
v-if="superUser"
:item="{
title: '电视剧',
icon: 'mdi-television-classic',
to: '/subscribe-tv',
}"
/>
<VerticalNavLink
:item="{
title: '自定义',
icon: 'mdi-rss',
to: '/subscribe-rss',
}"
/>
<VerticalNavLink
:item="{
title: '日历',
@@ -128,6 +127,7 @@ import UserProfile from '@/layouts/components/UserProfile.vue'
}"
/>
<VerticalNavLink
v-if="superUser"
:item="{
title: '历史记录',
icon: 'mdi-history',
@@ -135,6 +135,7 @@ import UserProfile from '@/layouts/components/UserProfile.vue'
}"
/>
<VerticalNavLink
v-if="superUser"
:item="{
title: '文件管理',
icon: 'mdi-folder-multiple-outline',
@@ -144,11 +145,13 @@ import UserProfile from '@/layouts/components/UserProfile.vue'
<!-- 👉 系统 -->
<VerticalNavSectionTitle
v-if="superUser"
:item="{
heading: '系统',
}"
/>
<VerticalNavLink
v-if="superUser"
:item="{
title: '插件',
icon: 'mdi-apps',
@@ -156,6 +159,7 @@ import UserProfile from '@/layouts/components/UserProfile.vue'
}"
/>
<VerticalNavLink
v-if="superUser"
:item="{
title: '站点管理',
icon: 'mdi-web',
@@ -163,6 +167,7 @@ import UserProfile from '@/layouts/components/UserProfile.vue'
}"
/>
<VerticalNavLink
v-if="superUser"
:item="{
title: '设定',
icon: 'mdi-cog',

View File

@@ -10,6 +10,10 @@ const themes: ThemeSwitcherTheme[] = [
name: 'dark',
icon: 'mdi-weather-night',
},
{
name: 'purple',
icon: 'mdi-brightness-4',
},
]
</script>

View File

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

View File

@@ -2,6 +2,7 @@
import NameTestView from '@/views/system/NameTestView.vue'
import NetTestView from '@/views/system/NetTestView.vue'
import LoggingView from '@/views/system/LoggingView.vue'
import RuleTestView from '@/views/system/RuleTestView.vue'
// App捷径
const appsMenu = ref(false)
@@ -14,6 +15,9 @@ const netTestDialog = ref(false)
// 实时日志弹窗
const loggingDialog = ref(false)
// 过滤规则弹窗
const ruleTestDialog = ref(false)
</script>
<template>
@@ -112,6 +116,27 @@ const loggingDialog = ref(false)
<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>
</VCard>
@@ -119,7 +144,7 @@ const loggingDialog = ref(false)
<!-- 名称测试弹窗 -->
<VDialog
v-model="nameTestDialog"
max-width="800"
max-width="50rem"
>
<VCard title="名称识别测试">
<DialogCloseBtn @click="nameTestDialog = false" />
@@ -131,7 +156,7 @@ const loggingDialog = ref(false)
<!-- 网络测试弹窗 -->
<VDialog
v-model="netTestDialog"
max-width="600"
max-width="35rem"
>
<VCard title="网络测试">
<DialogCloseBtn @click="netTestDialog = false" />
@@ -143,7 +168,7 @@ const loggingDialog = ref(false)
<!-- 实时日志弹窗 -->
<VDialog
v-model="loggingDialog"
max-width="1280"
class="w-full lg:w-4/5"
scrollable
>
<VCard title="实时日志">
@@ -153,4 +178,17 @@ const loggingDialog = ref(false)
</VCardText>
</VCard>
</VDialog>
<!-- 规则测试弹窗 -->
<VDialog
v-model="ruleTestDialog"
max-width="50rem"
scrollable
>
<VCard title="过滤规则测试">
<DialogCloseBtn @click="ruleTestDialog = false" />
<VCardText>
<RuleTestView />
</VCardText>
</VCard>
</VDialog>
</template>

View File

@@ -1,10 +1,23 @@
<script setup lang="ts">
import { useStore } from 'vuex'
import { useConfirm } from 'vuetify-use-dialog'
import { useToast } from 'vue-toast-notification'
import router from '@/router'
import avatar1 from '@images/avatars/avatar-1.png'
import api from '@/api'
// Vuex Store
const store = useStore()
// 确认框
const createConfirm = useConfirm()
// 提示框
const $toast = useToast()
// 进度框
const progressDialog = ref(false)
// 执行注销操作
function logout() {
// 清除登录状态信息
@@ -14,8 +27,45 @@ function logout() {
router.push('/login')
}
// 获取当前用户信息
const accountInfo: any = inject('accountInfo')
// 执行重启操作
async function restart() {
// 弹出提示
const confirmed = await createConfirm({
title: '确认',
content: '确认重启系统吗?',
confirmationText: '确认',
cancellationText: '取消',
dialogProps: {
maxWidth: '30rem',
},
})
if (confirmed) {
// 调用API重启
try {
// 显示等待框
progressDialog.value = true
const result: { [key: string]: any } = await api.get('system/restart')
if (!result?.success) {
// 隐藏等待框
progressDialog.value = false
// 重启不成功
$toast.error(result.message)
return
}
}
catch (error) {
console.error(error)
}
// 注销
logout()
}
}
// 从Vuex Store中获取信息
const superUser = store.state.auth.superUser
const userName = store.state.auth.userName
const avatar = store.state.auth.avatar
</script>
<template>
@@ -24,7 +74,7 @@ const accountInfo: any = inject('accountInfo')
color="primary"
variant="tonal"
>
<VImg :src="accountInfo.avatar" />
<VImg :src="avatar ?? avatar1" />
<!-- SECTION Menu -->
<VMenu
@@ -42,20 +92,21 @@ const accountInfo: any = inject('accountInfo')
color="primary"
variant="tonal"
>
<VImg :src="accountInfo.avatar" />
<VImg :src="avatar ?? avatar1" />
</VAvatar>
</VListItemAction>
</template>
<VListItemTitle class="font-weight-semibold">
{{ accountInfo.is_superuser ? "管理员" : "普通用户" }}
{{ superUser ? "管理员" : "普通用户" }}
</VListItemTitle>
<VListItemSubtitle>{{ accountInfo.name }}</VListItemSubtitle>
<VListItemSubtitle>{{ userName }}</VListItemSubtitle>
</VListItem>
<VDivider class="my-2" />
<!-- 👉 Profile -->
<VListItem
v-if="superUser"
link
to="setting"
>
@@ -89,6 +140,19 @@ const accountInfo: any = inject('accountInfo')
<!-- Divider -->
<VDivider class="my-2" />
<!-- 👉 restart -->
<VListItem @click="restart">
<template #prepend>
<VIcon
class="me-2"
icon="mdi-restart"
size="22"
/>
</template>
<VListItemTitle>重启</VListItemTitle>
</VListItem>
<!-- 👉 Logout -->
<VListItem @click="logout">
<template #prepend>
@@ -105,4 +169,22 @@ const accountInfo: any = inject('accountInfo')
</VMenu>
<!-- !SECTION -->
</VAvatar>
<!-- 重启进度框 -->
<vDialog
v-model="progressDialog"
width="25rem"
>
<vCard
color="primary"
>
<vCardText class="text-center">
正在重启 ...
<vProgressLinear
indeterminate
color="white"
class="mb-0 mt-1"
/>
</vCardText>
</vCard>
</vDialog>
</template>

View File

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

View File

@@ -66,10 +66,16 @@ function login() {
.then((response: any) => {
// 获取token
const token = response.access_token
const superuser = response.super_user
const username = response.user_name
const avatar = response.avatar
// 更新token和remember状态到Vuex Store
store.dispatch('auth/updateToken', token)
store.dispatch('auth/updateRemember', form.value.remember)
store.dispatch('auth/updateSuperUser', superuser)
store.dispatch('auth/updateUserName', username)
store.dispatch('auth/updateAvatar', avatar)
// 跳转到首页
router.push('/')

View File

@@ -1,9 +0,0 @@
<script setup lang="ts">
import RssListView from '@/views/subscribe/RssListView.vue'
</script>
<template>
<div>
<RssListView />
</div>
</template>

View File

@@ -57,7 +57,7 @@ const theme: VuetifyOptions['theme'] = {
dark: {
dark: true,
colors: {
'primary': '#9155FD',
'primary': '#6E66ED',
'secondary': '#8A8D93',
'on-secondary': '#fff',
'success': '#56CA00',
@@ -104,6 +104,57 @@ const theme: VuetifyOptions['theme'] = {
'shadow-key-ambient-opacity': 'rgba(20, 18, 33, 0.04)',
},
},
purple: {
dark: true,
colors: {
'primary': '#9155FD',
'secondary': '#8A8D93',
'on-secondary': '#fff',
'success': '#56CA00',
'info': '#16B1FF',
'warning': '#FFB400',
'error': '#FF4C51',
'on-primary': '#FFFFFF',
'on-success': '#FFFFFF',
'on-warning': '#FFFFFF',
'background': '#28243D',
'on-background': '#E7E3FC',
'surface': '#312D4B',
'on-surface': '#E7E3FC',
'grey-50': '#2A2E42',
'grey-100': '#474360',
'grey-200': '#4A5072',
'grey-300': '#5E6692',
'grey-400': '#7983BB',
'grey-500': '#8692D0',
'grey-600': '#AAB3DE',
'grey-700': '#B6BEE3',
'grey-800': '#CFD3EC',
'grey-900': '#E7E9F6',
'perfect-scrollbar-thumb': '#4A5072',
'skin-bordered-background': '#312d4b',
'skin-bordered-surface': '#312d4b',
},
variables: {
'code-color': '#d400ff',
'overlay-scrim-background': '#2C2942',
'overlay-scrim-opacity': 0.6,
'hover-opacity': 0.04,
'focus-opacity': 0.1,
'selected-opacity': 0.12,
'activated-opacity': 0.1,
'pressed-opacity': 0.14,
'dragged-opacity': 0.1,
'border-color': '#E7E3FC',
'table-header-background': '#3D3759',
'custom-background': '#373452',
// Shadows
'shadow-key-umbra-opacity': 'rgba(20, 18, 33, 0.08)',
'shadow-key-penumbra-opacity': 'rgba(20, 18, 33, 0.12)',
'shadow-key-ambient-opacity': 'rgba(20, 18, 33, 0.04)',
},
},
},
}

View File

@@ -48,13 +48,6 @@ const router = createRouter({
requiresAuth: true,
},
},
{
path: 'subscribe-rss',
component: () => import('../pages/subscribe-rss.vue'),
meta: {
requiresAuth: true,
},
},
{
path: 'calendar',
component: () => import('../pages/calendar.vue'),

View File

@@ -4,6 +4,9 @@ import type { Module } from 'vuex'
interface AuthState {
token: string | null
remember: boolean
superUser: boolean
userName: string
avatar: string
}
// 定义根状态类型
@@ -17,6 +20,9 @@ const authModule: Module<AuthState, RootState> = {
state: {
token: null,
remember: false,
superUser: false,
userName: '',
avatar: '',
},
mutations: {
setToken(state, token: string) {
@@ -28,6 +34,15 @@ const authModule: Module<AuthState, RootState> = {
setRemember(state, remember: boolean) {
state.remember = remember
},
setSuperUser(state, superUser: boolean) {
state.superUser = superUser
},
setUserName(state, userName: string) {
state.userName = userName
},
setAvatar(state, avatar: string) {
state.avatar = avatar
},
},
actions: {
updateToken({ commit }, token: string) {
@@ -39,10 +54,22 @@ const authModule: Module<AuthState, RootState> = {
updateRemember({ commit }, remember: boolean) {
commit('setRemember', remember)
},
updateSuperUser({ commit }, superUser: boolean) {
commit('setSuperUser', superUser)
},
updateUserName({ commit }, userName: string) {
commit('setUserName', userName)
},
updateAvatar({ commit }, avatar: string) {
commit('setAvatar', avatar)
},
},
getters: {
getToken: state => state.token,
getRemember: state => state.remember,
getSuperUser: state => state.superUser,
getUserName: state => state.userName,
getAvatar: state => state.avatar,
},
}

View File

@@ -3,14 +3,13 @@
@tailwind components;
@tailwind utilities;
#nprogress .bar {
background: #7D34FD !important;
background: rgb(var(--v-theme-primary)) !important;
top: env(safe-area-inset-top) !important;
}
#nprogress .peg {
box-shadow: 0 0 10px #7D34FD, 0 0 5px #7D34FD !important;
box-shadow: 0 0 10px rgb(var(--v-theme-primary)), 0 0 5px rgb(var(--v-theme-primary)) !important;
-webkit-transform: rotate(0deg) translate(0px, -1px);
-ms-transform: rotate(0deg) translate(0px, -1px);
transform: rotate(0deg) translate(0px, -1px);
@@ -123,3 +122,7 @@
-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;
}
.v-toolbar{
background: rgb(var(--v-table-header-background));
}

View File

@@ -0,0 +1,134 @@
<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 },
animations: { enabled: 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 },
max: 100,
},
}
})
// 调用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,140 @@
<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 usedMemory = ref(0)
// 内存使用百分比
const memoryUsage = ref(0)
const chartOptions = controlledComputed(() => vuetifyTheme.name.value, () => {
return {
chart: {
parentHeightOffset: 0,
toolbar: { show: false },
animations: { enabled: 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 },
max: 100,
},
}
})
// 调用API接口获取最新内存使用量
async function getMemorgUsage() {
try {
// 请求数据
[usedMemory.value, memoryUsage.value] = await api.get('dashboard/memory')
series.value[0].data.push(memoryUsage.value)
// 序列超过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(usedMemory) }}
</p>
</VCardText>
</VCard>
</template>

View File

@@ -430,9 +430,13 @@ onBeforeMount(() => {
<div class="relative z-20 flex items-center false"><span>已入库</span></div>
</span>
</div>
<h1 class="flex flex-row items-baseline justify-start lg:justify-center">
<span>{{ mediaDetail.title }}</span>
<span v-if="mediaDetail.year" class="text-lg">{{ mediaDetail.year }}</span>
<h1 class="d-flex flex-column flex-lg-row align-baseline justify-center justify-lg-start">
<div class="align-self-center align-self-lg-end">
{{ mediaDetail.title }}
</div>
<div v-if="mediaDetail.year" class="text-lg align-self-center align-self-lg-end">
{{ mediaDetail.year }}
</div>
</h1>
<span class="media-attributes">
<span v-if="mediaDetail.runtime || mediaDetail.episode_run_time[0]">{{ mediaDetail.runtime || mediaDetail.episode_run_time[0] }} 分钟</span>

View File

@@ -3,7 +3,7 @@ import api from '@/api'
import FileBrowser from '@/components/FileBrowser.vue'
const endpoints = {
list: { url: '/filebrowser/list?path={path}', method: 'get' },
list: { url: '/filebrowser/list?path={path}&sort={sort}', method: 'get' },
mkdir: { url: '/filebrowser/mkdir?path={path}', method: 'get' },
delete: { url: '/filebrowser/delete?path={path}', method: 'get' },
download: { url: '/filebrowser/download?path={path}', method: 'get' },

View File

@@ -5,6 +5,7 @@ import { useConfirm } from 'vuetify-use-dialog'
import { numberValidator, requiredValidator } from '@/@validators'
import api from '@/api'
import type { TransferHistory } from '@/api/types'
import TmdbSelectorCard from '@/components/cards/TmdbSelectorCard.vue'
// 确认框
const createConfirm = useConfirm()
@@ -71,6 +72,9 @@ const progressText = ref('请稍候 ...')
// 进度值
const progressValue = ref(0)
// TMDB选择对话框
const tmdbSelectorDialog = ref(false)
// 获取订阅列表数据
async function fetchData({
page,
@@ -131,7 +135,7 @@ async function removeHistory(item: TransferHistory) {
confirmationText: '同步删除文件',
cancellationText: '仅删除历史记录',
dialogProps: {
maxWidth: 600,
maxWidth: '50rem',
},
confirmationButtonProps: {
color: 'error',
@@ -180,7 +184,7 @@ async function removeHistoryBatch() {
confirmationText: '同步删除文件',
cancellationText: '仅删除历史记录',
dialogProps: {
maxWidth: 600,
maxWidth: '50rem',
},
confirmationButtonProps: {
color: 'error',
@@ -393,7 +397,7 @@ const dropdownItems = ref([
</VCard>
<VDialog
v-model="redoDialog"
max-width="600"
max-width="50rem"
>
<!-- Dialog Content -->
<VCard title="重新整理">
@@ -412,6 +416,8 @@ const dropdownItems = ref([
v-model="redoTmdbId"
label="TMDB编号"
:rules="[requiredValidator, numberValidator]"
append-inner-icon="mdi-magnify"
@click:append-inner="tmdbSelectorDialog = true"
/>
</VCol>
</VRow>
@@ -440,7 +446,7 @@ const dropdownItems = ref([
<vDialog
v-model="progressDialog"
:scrim="false"
width="400"
width="25rem"
>
<vCard
color="primary"
@@ -455,6 +461,17 @@ const dropdownItems = ref([
</vCardText>
</vCard>
</vDialog>
<!-- TMDB ID搜索框 -->
<vDialog
v-model="tmdbSelectorDialog"
width="600"
scrollable
>
<TmdbSelectorCard
v-model="redoTmdbId"
@close="tmdbSelectorDialog = false"
/>
</vDialog>
</template>
<style lang="scss">

View File

@@ -408,7 +408,7 @@ onMounted(() => {
<!-- 站点编辑弹窗 -->
<VDialog
v-model="addUserDialog"
max-width="800"
max-width="50rem"
persistent
>
<!-- Dialog Content -->

View File

@@ -5,15 +5,10 @@ import FilterRuleCard from '@/components/cards/FilterRuleCard.vue'
// 规则卡片类型
interface FilterCard {
// 优先级
pri: string
// 已选规则
rules: string[]
// 是否可见
visible: boolean
}
// 提示框
@@ -48,7 +43,6 @@ async function queryCustomFilters(ruleType: string) {
return {
pri: (index + 1).toString(),
rules: group.split('&'),
visible: true,
}
})
}
@@ -138,23 +132,18 @@ function updateFilterCardValue2(pri: string, rules: string[]) {
// 移除卡片
function filterCardClose(ruleType: string, pri: string) {
// 将卡片从列表中删除,并更新剩余卡片的序号
const cards = ruleType === 'FilterRules' ? filterCards : filterCards2
const index = cards.value.findIndex(card => card.pri === pri)
if (index !== -1) {
// 创建新的数组,然后使用 splice 方法来删除元素
const updatedCards = [...cards.value]
updatedCards.splice(index, 1)
// 更新剩余卡片的序号
updatedCards.forEach((card, i) => {
card.pri = (i + 1).toString()
// 将pri对应的卡片从列表中删除,并更新剩余卡片的序号
const updatedCards = (ruleType === 'FilterRules' ? filterCards.value : filterCards2.value)
.filter(card => card.pri !== pri)
.map((card, index) => {
card.pri = (index + 1).toString()
return card
})
// 更新 filterCards.value
cards.value = updatedCards
}
// 更新 filterCards.value
if (ruleType === 'FilterRules')
filterCards.value = updatedCards
else
filterCards2.value = updatedCards
}
// 增加卡片
@@ -164,7 +153,7 @@ function addFilterCard(ruleType: string) {
const pri = (cards.value.length + 1).toString()
// 新卡片
const newCard: FilterCard = { pri, rules: [], visible: true }
const newCard: FilterCard = { pri, rules: [] }
// 添加到列表
cards.value.push(newCard)
@@ -189,7 +178,6 @@ onMounted(() => {
:key="index"
:pri="card.pri"
:rules="card.rules"
:visible="card.visible"
@changed="updateFilterCardValue"
@close="filterCardClose('FilterRules', card.pri)"
/>
@@ -224,7 +212,6 @@ onMounted(() => {
:key="index"
:pri="card.pri"
:rules="card.rules"
:visible="card.visible"
@changed="updateFilterCardValue2"
@close="filterCardClose('FilterRules2', card.pri)"
/>

View File

@@ -129,10 +129,11 @@ onMounted(() => {
<VTextarea
v-model="customIdentifiers"
auto-grow
placeholder="支持正则表达式,特殊字符需要\转义,一行为一组,支持种配置格式:
placeholder="支持正则表达式,特殊字符需要\转义,一行为一组,支持以下几种配置格式:
屏蔽词
被替换词 => 替换词
前定位词 <> 后定位词 >> 偏移量EP"
前定位词 <> 后定位词 >> 偏移量EP
被替换词 => 替换词 && 前定位词 <> 后定位词 >> 集偏移量EP"
/>
</VCardItem>
<VCardItem>

View File

@@ -135,7 +135,7 @@ onBeforeMount(fetchData)
<!-- Dialog Content -->
<VDialog
v-model="siteAddDialog"
max-width="800"
max-width="50rem"
persistent
scrollable
>
@@ -149,6 +149,7 @@ onBeforeMount(fetchData)
/>
</template>
<VCard title="新增站点">
<DialogCloseBtn @click="siteAddDialog = false" />
<VCardText class="pt-2">
<VForm @submit.prevent="() => {}">
<VRow>
@@ -185,6 +186,12 @@ onBeforeMount(fetchData)
</VCol>
</VRow>
<VRow>
<VCol cols="12">
<VTextField
v-model="siteForm.rss"
label="RSS地址"
/>
</VCol>
<VCol cols="12">
<VTextarea
v-model="siteForm.cookie"

View File

@@ -79,17 +79,7 @@ async function getSubscribes() {
subscribes.map(async sub => eventsHander(sub)),
)
// 自定义订阅
const rsses: Rss[] = await api.get('rss')
const rssEvents = await Promise.all(
rsses.map(async rss => eventsHander(rss)),
)
// 合并事件
const events = [...subEvents, ...rssEvents]
calendarOptions.value.events = events.flat().filter(event => event.start) as EventSourceInput
calendarOptions.value.events = subEvents.flat().filter(event => event.start) as EventSourceInput
}
catch (error) {
console.error(error)

View File

@@ -1,332 +0,0 @@
<script lang="ts" setup>
import PullRefresh from 'pull-refresh-vue3'
import { useToast } from 'vue-toast-notification'
import api from '@/api'
import type { Rss } from '@/api/types'
import NoDataFound from '@/components/NoDataFound.vue'
import RssCard from '@/components/cards/RssCard.vue'
import { numberValidator, requiredValidator } from '@/@validators'
import { doneNProgress, startNProgress } from '@/api/nprogress'
// 提示框
const $toast = useToast()
// 是否刷新过
const isRefreshed = ref(false)
// 新增按钮文本
const addBtnText = ref('新增订阅')
// 新增按钮状态
const addBtnState = ref(false)
// 新增自定义订阅对话框
const rssAddDialog = ref(false)
// 新增订阅表单
const rssForm = reactive({
// RSS地址
url: '',
// 类型
type: '电影',
// 标题
title: '',
// 年份
year: '',
// 季号
season: 1,
// 包含
include: '',
// 排除
exclude: '',
// 洗版
best_version: false,
// 是否使用代理服务器
proxy: false,
// 是否使用过滤规则
filter: true,
// 保存路径
save_path: '',
// 状态 0-停用1-启用
state: 1,
})
// 数据列表
const dataList = ref<Rss[]>([])
// 获取订阅列表数据
async function fetchData() {
try {
dataList.value = await api.get('rss')
isRefreshed.value = true
}
catch (error) {
console.error(error)
}
}
// 调用API 新增自定义订阅
async function addRss() {
if (!rssForm.url || !rssForm.title)
return
startNProgress()
addBtnText.value = '新增中...'
addBtnState.value = true
if (rssForm.type === '电影')
rssForm.season = 0
try {
const result: { [key: string]: string } = await api.post('rss', rssForm)
if (result.success) {
$toast.success('新增自定义订阅成功')
// 刷新数据
fetchData()
}
else { $toast.error(`新增自定义订阅失败:${result.message}`) }
rssAddDialog.value = false
}
catch (error) {
console.error(error)
}
doneNProgress()
addBtnText.value = '新增订阅'
addBtnState.value = false
}
// 生成1到50季的下拉框选项
const seasonItems = ref(
Array.from({ length: 50 }, (_, i) => i + 1).map(item => ({
title: `${item}`,
value: item,
})),
)
// 加载时获取数据
onBeforeMount(fetchData)
// 刷新状态
const loading = ref(false)
// 下拉刷新
function onRefresh() {
loading.value = true
fetchData()
loading.value = false
}
</script>
<template>
<div
v-if="!isRefreshed"
class="mt-12 w-full text-center text-gray-500 text-sm flex flex-col items-center"
>
<VProgressCircular
v-if="!isRefreshed"
size="48"
indeterminate
color="primary"
/>
</div>
<PullRefresh
v-model="loading"
@refresh="onRefresh"
>
<div
v-if="dataList.length > 0"
class="grid gap-3 grid-rss-card p-1"
>
<RssCard
v-for="data in dataList"
:key="data.id"
:media="data"
@remove="fetchData"
@save="fetchData"
/>
</div>
<NoDataFound
v-if="dataList.length === 0 && isRefreshed"
error-code="404"
error-title="没有自定义订阅"
error-description="点击右下角按钮新增订阅"
/>
</PullRefresh>
<!-- 新增订阅 -->
<VDialog
v-model="rssAddDialog"
max-width="800"
persistent
scrollable
>
<!-- Dialog Activator -->
<template #activator="{ props }">
<VBtn
icon="mdi-plus"
v-bind="props"
size="x-large"
class="fixed right-5 bottom-5"
/>
</template>
<!-- Dialog Content -->
<VCard title="新增自定义订阅">
<VCardText class="pt-2">
<VForm @submit.prevent="() => {}">
<VRow>
<VCol
cols="12"
>
<VTextField
v-model="rssForm.url"
label="RSS地址"
placeholder="https://example.com/rss"
:rules="[requiredValidator]"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<VSelect
v-model="rssForm.type"
label="类型"
:items="[{ title: '电影', value: '电影' }, { title: '电视剧', value: '电视剧' }]"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<VTextField
v-model="rssForm.title"
label="标题"
:rules="[requiredValidator]"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<VTextField
v-model="rssForm.year"
label="年份"
:rules="[numberValidator]"
/>
</VCol>
<VCol
v-if="rssForm.type === '电视剧'"
cols="12"
md="6"
>
<VSelect
v-model="rssForm.season"
label="季"
:items="seasonItems"
/>
</VCol>
<VCol
cols="12"
>
<VTextField
v-model="rssForm.include"
label="包含"
placeholder="支持正则表达式"
/>
</VCol>
<VCol
cols="12"
>
<VTextField
v-model="rssForm.exclude"
label="排除"
placeholder="支持正则表达式"
/>
</VCol>
<VCol
cols="12"
>
<VTextField
v-model="rssForm.save_path"
label="保存路径"
placeholder="留空自动"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<VSelect
v-model="rssForm.state"
label="状态"
:items="[{
title: '启用',
value: 1,
}, {
title: '停用',
value: 0,
}]"
/>
</VCol>
</VRow>
<VRow>
<VCol
cols="12"
md="4"
>
<VSwitch
v-model="rssForm.best_version"
label="洗版"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VSwitch
v-model="rssForm.proxy"
label="代理服务器"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VSwitch
v-model="rssForm.filter"
label="过滤规则"
/>
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardActions>
<VBtn
@click="rssAddDialog = false"
>
取消
</VBtn>
<VSpacer />
<VBtn
color="primary"
:disabled="addBtnState"
@click="addRss"
>
{{ addBtnText }}
</VBtn>
</VCardActions>
</VCard>
</VDialog>
</template>
<style lang="scss">
.grid-rss-card {
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
padding-block-end: 1rem;
}
</style>

View File

@@ -28,7 +28,7 @@ function startSSELogging() {
function extractLogDetailsFromLogs(logs: string[]): { level: string; time: string; program: string; content: string }[] {
const logDetails: { level: string; time: string; program: string; content: string }[] = []
const logPattern = /^【(.*?)】.*\s(.*?)\s-\s(.*?)\s-\s(.*)$/
const logPattern = /^【(.*?)】[0-9\-:]*\s(.*?)\s-\s(.*?)\s-\s(.*)$/
for (const log of logs) {
const matches = RegExp(logPattern).exec(log)
@@ -70,7 +70,7 @@ onMounted(() => {
<template>
<div
v-if="logs.length === 0"
class="mt-5 w-full text-centerflex flex-col items-center"
class="mt-5 w-full text-center flex flex-col items-center"
>
<VProgressCircular
size="48"
@@ -79,41 +79,43 @@ onMounted(() => {
/>
<span class="mt-3">正在刷新 ...</span>
</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"
<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"
/>
</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>
</tr>
</tbody>
</VTable>
</div>
</template>

View File

@@ -3,6 +3,7 @@ import { reactive, ref } from 'vue'
import { requiredValidator } from '@/@validators'
import api from '@/api'
import type { Context } from '@/api/types'
import MediaInfoCard from '@/components/cards/MediaInfoCard.vue'
// 识别结果
const nameTestResult = ref<Context>()
@@ -45,22 +46,6 @@ async function nameTest() {
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>
<template>
@@ -101,123 +86,7 @@ function getW500Image(url = '') {
</VForm>
<VExpandTransition>
<div v-show="showResult">
<VCol>
<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>
<MediaInfoCard :context="nameTestResult" />
</div>
</VExpandTransition>
</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>

View File

@@ -1836,6 +1836,11 @@
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
"@types/lodash@^4.14.197":
version "4.14.198"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.198.tgz#4d27465257011aedc741a809f1269941fa2c5d4c"
integrity sha512-trNJ/vtMZYMLhfN45uLq4ShQSw0/S7xCTLLVM+WM1rmFpba/VS42jVUgaO3w/NOLiWR/09lnYk0yMaA/atdIsg==
"@types/mdast@^3.0.0":
version "3.0.11"
resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.11.tgz#dc130f7e7d9306124286f6d6cee40cf4d14a3dc0"