Compare commits

..

34 Commits

Author SHA1 Message Date
jxxghp
fc585a3900 v1.2.7 2023-09-28 16:16:04 +08:00
jxxghp
973f8529c2 Merge pull request #46 from thsrite/main
feat 自动清理媒体库插件icon
2023-09-28 15:46:36 +08:00
thsrite
1ff9dc50fd feat 自动清理媒体库插件icon 2023-09-28 15:12:15 +08:00
jxxghp
065c9053da fix ui 2023-09-28 12:52:44 +08:00
jxxghp
6905be1bcd fix #681 2023-09-28 09:58:39 +08:00
jxxghp
a550f9616c feat 批量整理进度条 2023-09-27 14:26:59 +08:00
jxxghp
bcee3e5373 v1.2.6 2023-09-27 10:19:05 +08:00
jxxghp
d377ced6b6 feat 优先级规则支持动态调整 2023-09-27 09:42:28 +08:00
jxxghp
6e0ceb093c feat 批量重新整理 2023-09-27 08:59:09 +08:00
jxxghp
745f99e52e feat 历史记录批量重新整理 2023-09-27 08:45:55 +08:00
jxxghp
7197034eda fix ui 2023-09-24 19:52:55 +08:00
jxxghp
264748652f fix 2023-09-24 19:47:28 +08:00
jxxghp
48e214564a v1.2.5 2023-09-24 19:30:37 +08:00
jxxghp
5424e7e02a fix ui 2023-09-24 12:26:42 +08:00
jxxghp
0c9c70b067 feat 服务设置 2023-09-24 11:14:27 +08:00
jxxghp
0ff24f4b09 fix torrent ui 2023-09-23 11:55:49 +08:00
jxxghp
cfa75b7643 rename 2023-09-23 08:32:24 +08:00
jxxghp
b72ad1d78d Merge pull request #45 from jianxcao/feature-url-307
feat: 修改url地址,防止307成错误的url地址
2023-09-22 15:34:14 +08:00
jxxghp
5d1f293606 feat SynologyChat 2023-09-22 15:32:51 +08:00
jianxiong.cao
2dc0eca4aa feat: 修改url地址,防止307成错误的url地址 2023-09-22 15:11:52 +08:00
jxxghp
f5808c1c81 fix 光标聚焦 2023-09-22 13:49:00 +08:00
jxxghp
321037477f fix placeholder 2023-09-22 11:03:58 +08:00
jxxghp
43589c66e9 fix bug 2023-09-22 09:20:41 +08:00
jxxghp
435f299a8b fix ui 2023-09-22 07:27:27 +08:00
jxxghp
083db80251 更新 MediaCard.vue 2023-09-21 23:21:04 +08:00
jxxghp
92bf520cf4 fix 2023-09-21 23:06:51 +08:00
jxxghp
ab354f21c4 fix ui 2023-09-21 22:57:54 +08:00
jxxghp
c7a2c045c7 fix ui 2023-09-21 22:50:35 +08:00
jxxghp
d33c8942e4 fix ui 2023-09-21 22:28:18 +08:00
jxxghp
5e630097b9 更新 MediaCard.vue 2023-09-21 21:52:56 +08:00
jxxghp
3b5d03c1c8 更新 package.json 2023-09-21 21:26:34 +08:00
jxxghp
298ae2c354 fix ui 2023-09-21 21:24:10 +08:00
jxxghp
d936b68597 fix ui 2023-09-21 20:31:24 +08:00
jxxghp
41471b9fd6 feat 历史记录删除UI调整 2023-09-21 20:01:25 +08:00
27 changed files with 622 additions and 207 deletions

View File

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

BIN
public/plugin/clean.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -832,6 +832,7 @@ export interface NotificationSwitch {
wechat: boolean wechat: boolean
telegram: boolean telegram: boolean
slack: boolean slack: boolean
synologychat: boolean
} }
// 环境设置 // 环境设置

View File

@@ -2,19 +2,30 @@
// 输入参数 // 输入参数
const props = defineProps({ const props = defineProps({
pri: String, pri: String,
maxpri: String,
rules: Array as PropType<string[]>, rules: Array as PropType<string[]>,
width: String, width: String,
height: String, height: String,
}) })
// 定义触发的自定义事件 // 定义触发的自定义事件
const emit = defineEmits(['close', 'changed']) const emit = defineEmits(['close', 'changed', 'levelup', 'leveldown'])
// 按钮点击 // 按钮点击
function onClose() { function onClose() {
emit('close') emit('close')
} }
// 上升优先级
function onLevelUp() {
emit('levelup', props.pri)
}
// 下降优先级
function onLevelDown() {
emit('leveldown', props.pri)
}
// 选项变化 // 选项变化
function filtersChanged(value: string[]) { function filtersChanged(value: string[]) {
emit('changed', props.pri, value) emit('changed', props.pri, value)
@@ -54,6 +65,20 @@ const selectFilterOptions = ref<{ [key: string]: string }[]>([
<template> <template>
<VCard variant="tonal" :width="props.width" :height="props.height"> <VCard variant="tonal" :width="props.width" :height="props.height">
<span class="absolute top-3 right-14">
<IconBtn
v-if="props.pri !== '1'"
@click.stop="onLevelUp"
>
<VIcon icon="mdi-arrow-up" />
</IconBtn>
<IconBtn
v-if="props.pri !== props.maxpri"
@click.stop="onLevelDown"
>
<VIcon icon="mdi-arrow-down" />
</IconBtn>
</span>
<DialogCloseBtn @click="onClose" /> <DialogCloseBtn @click="onClose" />
<VCardItem> <VCardItem>
<VCardTitle>优先级 {{ props.pri }}</VCardTitle> <VCardTitle>优先级 {{ props.pri }}</VCardTitle>

View File

@@ -112,7 +112,7 @@ async function addSubscribe(season = 0) {
// 全部存在时洗版 // 全部存在时洗版
best_version = !seasonsNotExisted.value[season] ? 1 : 0 best_version = !seasonsNotExisted.value[season] ? 1 : 0
// 请求API // 请求API
const result: { [key: string]: any } = await api.post('subscribe', { const result: { [key: string]: any } = await api.post('subscribe/', {
name: props.media?.title, name: props.media?.title,
type: props.media?.type, type: props.media?.type,
year: props.media?.year, year: props.media?.year,
@@ -360,14 +360,6 @@ onBeforeMount(() => {
handleCheckExists() handleCheckExists()
}) })
// 订阅季表头
const seasonsHeaders = [
{ title: '季', key: 'title', sortable: false },
{ title: '集数', key: 'episodes', sortable: false },
{ title: '评分', key: 'vote', sortable: false },
{ title: '状态', key: 'status', sortable: false },
]
// 计算图片地址 // 计算图片地址
const getImgUrl: Ref<string> = computed(() => { const getImgUrl: Ref<string> = computed(() => {
if (imageLoadError.value) if (imageLoadError.value)
@@ -379,6 +371,28 @@ const getImgUrl: Ref<string> = computed(() => {
return url return url
}) })
// 拼装季图片地址
function getSeasonPoster(posterPath: string) {
if (!posterPath)
return ''
return `https://image.tmdb.org/t/p/w500${posterPath}`
}
// 将yyyy-mm-dd转换为yyyy年mm月dd日
function formatAirDate(airDate: string) {
if (!airDate)
return ''
const date = new Date(airDate)
return `${date.getFullYear()}${date.getMonth() + 1}${date.getDate()}`
}
// 从yyyy-mm-dd中提取年份
function getYear(airDate: string) {
if (!airDate)
return ''
const date = new Date(airDate)
return date.getFullYear()
}
</script> </script>
<template> <template>
@@ -460,72 +474,89 @@ const getImgUrl: Ref<string> = computed(() => {
</VCard> </VCard>
</template> </template>
</VHover> </VHover>
<VDialog <!-- 订阅季弹窗 -->
<VBottomSheet
v-model="subscribeSeasonDialog" v-model="subscribeSeasonDialog"
max-width="50rem" inset
content-class="whitespace-nowrap"
scrollable scrollable
> >
<!-- Dialog Content --> <VCard>
<VCard title="选择订阅季"> <DialogCloseBtn @click="subscribeSeasonDialog = false" />
<VCardText style="padding: 0;"> <VCardTitle class="pe-10">
<VDataTable 订阅 - {{ props.media?.title }}
v-model="seasonsSelected" </VCardTitle>
:headers="seasonsHeaders" <VCardText>
:items="seasonInfos" <VList
item-value="season_number" v-model:selected="seasonsSelected"
return-object lines="three"
fixed-header select-strategy="classic"
show-select
:items-per-page="100"
density="compact"
height="auto"
> >
<template #item.title="{ item }"> <VListItem
<span class="d-block whitespace-nowrap"> {{ item.raw.season_number }} v-for="(item, i) in seasonInfos" :key="i"
</span> :value="item"
</template> >
<template #item.episodes="{ item }"> <template #prepend>
<VChip <VImg
variant="outlined" height="90"
size="small" width="60"
> :src="getSeasonPoster(item.poster_path || '')"
{{ item.raw.episode_count }} aspect-ratio="2/3"
</VChip> class="object-cover rounded shadow ring-gray-500 me-3"
</template> cover
<template #item.vote="{ item }"> >
{{ item.raw.vote_average }} <template #placeholder>
</template> <div class="w-full h-full">
<template #item.status="{ item }"> <VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
<VChip </div>
v-if="seasonsNotExisted" </template>
:color="getExistColor(item.raw.season_number)" </VImg>
flat </template>
size="small" <VListItemTitle>
> {{ item.season_number }}
{{ getExistText(item.raw.season_number) }} </VListItemTitle>
</VChip> <VListItemSubtitle class="mt-1 me-2">
</template> <VChip
<template #no-data> v-if="item.vote_average"
没有数据 color="primary"
</template> size="small"
<template #bottom /> class="mb-1"
</VDataTable> >
<VIcon icon="mdi-star" /> {{ item.vote_average }}
</VChip>
{{ getYear(item.air_date || '') }} {{ item.episode_count }}
</VListItemSubtitle>
<VListItemSubtitle>
{{ media?.title }} {{ item.season_number }} 季于 {{ formatAirDate(item.air_date || '') }} 首播
</VListItemSubtitle>
<VListItemSubtitle>
<VChip
v-if="seasonsNotExisted"
class="mt-2"
size="small"
:color="getExistColor(item.season_number || 0)"
>
{{ getExistText(item.season_number || 0) }}
</VChip>
</VListItemSubtitle>
<template #append="{ isSelected }">
<VListItemAction start>
<VSwitch :model-value="isSelected" />
</VListItemAction>
</template>
</VListItem>
</VList>
</VCardText> </VCardText>
<VCardActions> <div class="my-2 text-center">
<VBtn @click="subscribeSeasonDialog = false">
取消
</VBtn>
<VSpacer />
<VBtn <VBtn
:disabled="seasonsSelected.length === 0"
width="30%"
@click="subscribeSeasons" @click="subscribeSeasons"
@keydown.enter="subscribeSeasons"
> >
确定 {{ seasonsSelected.length === 0 ? '请选择订阅季' : '提交订阅' }}
</VBtn> </VBtn>
</VCardActions> </div>
</VCard> </VCard>
</VDialog> </VBottomSheet>
</template> </template>
<style lang="scss"> <style lang="scss">

View File

@@ -212,7 +212,7 @@ async function updateSiteInfo() {
// 更新按钮状态 // 更新按钮状态
siteInfoDialog.value = false siteInfoDialog.value = false
const result: { [key: string]: any } = await api.put('site', siteForm) const result: { [key: string]: any } = await api.put('site/', siteForm)
if (result.success) { if (result.success) {
$toast.success(`${cardProps.site?.name} 更新成功!`) $toast.success(`${cardProps.site?.name} 更新成功!`)
emit('update') emit('update')
@@ -647,6 +647,7 @@ onMounted(() => {
<VListItemTitle>查看详情</VListItemTitle> <VListItemTitle>查看详情</VListItemTitle>
</VListItem> </VListItem>
<VListItem <VListItem
v-if="item.raw.enclosure?.startsWith('http')"
variant="plain" variant="plain"
@click="downloadTorrentFile(item.raw.enclosure)" @click="downloadTorrentFile(item.raw.enclosure)"
> >

View File

@@ -118,7 +118,7 @@ async function searchSubscribe() {
async function updateSubscribeInfo() { async function updateSubscribeInfo() {
subscribeInfoDialog.value = false subscribeInfoDialog.value = false
try { try {
const result: { [key: string]: any } = await api.put('subscribe', subscribeForm) const result: { [key: string]: any } = await api.put('subscribe/', subscribeForm)
// 提示 // 提示
if (result.success) { if (result.success) {

View File

@@ -20,6 +20,9 @@ const keyword = ref('')
// 加载中 // 加载中
const loading = ref(false) const loading = ref(false)
// ref
const tmdbKeyword = ref<HTMLElement | null>(null)
// 选中条目 // 选中条目
function selectMedia(item: TmdbItem) { function selectMedia(item: TmdbItem) {
console.log(item) console.log(item)
@@ -68,6 +71,14 @@ async function searchMedias() {
console.error(e) console.error(e)
} }
} }
// 加载时聚焦搜索框
onMounted(() => {
// 500ms后聚焦
setTimeout(() => {
tmdbKeyword.value?.focus()
}, 500)
})
</script> </script>
<template> <template>
@@ -75,16 +86,17 @@ async function searchMedias() {
class="mx-auto" class="mx-auto"
width="100%" width="100%"
> >
<VToolbar flat dense> <VToolbar flat class="p-0">
<VTextField <VTextField
ref="tmdbKeyword"
v-model="keyword" v-model="keyword"
density="compact"
label="输入名称搜索" label="输入名称搜索"
single-line single-line
hide-details placeholder="电影或电视剧名称"
flat variant="solo"
class="mx-3"
append-inner-icon="mdi-magnify" append-inner-icon="mdi-magnify"
flat
class="mx-1"
:loading="loading" :loading="loading"
@click:append-inner="searchMedias" @click:append-inner="searchMedias"
@keydown.enter="searchMedias" @keydown.enter="searchMedias"
@@ -97,7 +109,6 @@ async function searchMedias() {
> >
<template v-for="(item, i) in items" :key="i"> <template v-for="(item, i) in items" :key="i">
<VListItem <VListItem
density="compact"
@click="selectMedia(item)" @click="selectMedia(item)"
> >
<template #prepend> <template #prepend>
@@ -119,7 +130,7 @@ async function searchMedias() {
<VListItemTitle> <VListItemTitle>
{{ item.title }} {{ item.title }}
</VListItemTitle> </VListItemTitle>
<VListItemSubtitle v-html="item.overview" /> <VListItemSubtitle class="mt-2" v-html="item.overview" />
</VListItem> </VListItem>
<VDivider v-if="i < items.length - 1" class="mt-1" inset /> <VDivider v-if="i < items.length - 1" class="mt-1" inset />
</template> </template>

View File

@@ -76,7 +76,7 @@ async function handleAddDownload(_site: any = undefined,
async function addDownload(_media: any, _torrent: any) { async function addDownload(_media: any, _torrent: any) {
startNProgress() startNProgress()
try { try {
const result: { [key: string]: any } = await api.post('download', { const result: { [key: string]: any } = await api.post('download/', {
media_in: _media, media_in: _media,
torrent_in: _torrent, torrent_in: _torrent,
}) })
@@ -122,26 +122,6 @@ function getVolumeFactorClass(downloadVolume: number, uploadVolume: number) {
onMounted(() => { onMounted(() => {
getSiteIcon() getSiteIcon()
}) })
// 弹出菜单
const dropdownItems = ref([
{
title: '查看详情',
value: 1,
props: {
prependIcon: 'mdi-information',
click: openTorrentDetail,
},
},
{
title: '下载种子',
value: 2,
props: {
prependIcon: 'mdi-download',
click: downloadTorrentFile,
},
},
])
</script> </script>
<template> <template>
@@ -180,15 +160,23 @@ const dropdownItems = ref([
> >
<VList> <VList>
<VListItem <VListItem
v-for="(item, i) in dropdownItems"
:key="i"
variant="plain" variant="plain"
@click="item.props.click" @click="openTorrentDetail()"
> >
<template #prepend> <template #prepend>
<VIcon :icon="item.props.prependIcon" /> <VIcon icon="mdi-information" />
</template> </template>
<VListItemTitle v-text="item.title" /> <VListItemTitle>查看详情</VListItemTitle>
</VListItem>
<VListItem
v-if="props.torrent?.torrent_info?.enclosure?.startsWith('http')"
variant="plain"
@click="downloadTorrentFile()"
>
<template #prepend>
<VIcon icon="mdi-download" />
</template>
<VListItemTitle>下载种子</VListItemTitle>
</VListItem> </VListItem>
</VList> </VList>
</VMenu> </VMenu>

View File

@@ -632,6 +632,7 @@ onMounted(() => {
<VTextField <VTextField
v-model="transferForm.target" v-model="transferForm.target"
label="目的路径" label="目的路径"
placeholder="留空自动"
/> />
</VCol> </VCol>
<VCol <VCol

View File

@@ -5,6 +5,7 @@ import { type PropType, ref } from 'vue'
interface RenderProps { interface RenderProps {
component: string component: string
text: string text: string
html: string
content?: any content?: any
props?: any props?: any
} }
@@ -16,9 +17,10 @@ const elementProps = defineProps({
}) })
// 配置元素 // 配置元素
const formItem = ref<RenderProps>(elementProps.config || { const formItem = ref<RenderProps>(elementProps.config ?? {
component: 'div', component: 'div',
text: '', text: '',
html: '',
props: {}, props: {},
content: [], content: [],
}) })
@@ -30,6 +32,7 @@ const formData = ref<any>(elementProps.form || {})
<template> <template>
<Component <Component
:is="formItem.component" :is="formItem.component"
v-if="!formItem.html"
v-bind="formItem.props" v-bind="formItem.props"
v-model="formData[formItem.props?.model || '']" v-model="formData[formItem.props?.model || '']"
> >
@@ -42,4 +45,10 @@ const formData = ref<any>(elementProps.form || {})
:form="formData" :form="formData"
/> />
</Component> </Component>
<Component
:is="formItem.component"
v-if="formItem.html"
v-bind="formItem.props"
v-html="formItem.html"
/>
</template> </template>

View File

@@ -13,11 +13,10 @@ interface RenderProps {
// 输入参数 // 输入参数
const elementProps = defineProps({ const elementProps = defineProps({
config: Object as PropType<RenderProps>, config: Object as PropType<RenderProps>,
handler: Boolean,
}) })
// 配置元素 // 配置元素
const formItem = ref<RenderProps>(elementProps.config || { const formItem = ref<RenderProps>(elementProps.config ?? {
component: 'div', component: 'div',
text: '', text: '',
html: '', html: '',

View File

@@ -8,6 +8,7 @@ import AccountSettingWords from '@/views/setting/AccountSettingWords.vue'
import AccountSettingAbout from '@/views/setting/AccountSettingAbout.vue' import AccountSettingAbout from '@/views/setting/AccountSettingAbout.vue'
import AccountSettingSearch from '@/views/setting/AccountSettingSearch.vue' import AccountSettingSearch from '@/views/setting/AccountSettingSearch.vue'
import AccountSettingSubscribe from '@/views/setting/AccountSettingSubscribe.vue' import AccountSettingSubscribe from '@/views/setting/AccountSettingSubscribe.vue'
import AccountSettingService from '@/views/setting/AccountSettingService.vue'
const route = useRoute() const route = useRoute()
@@ -35,6 +36,11 @@ const tabs = [
icon: 'mdi-rss', icon: 'mdi-rss',
tab: 'subscribe', tab: 'subscribe',
}, },
{
title: '服务',
icon: 'mdi-list-box',
tab: 'service',
},
{ {
title: '规则', title: '规则',
icon: 'mdi-filter-cog', icon: 'mdi-filter-cog',
@@ -60,7 +66,10 @@ const tabs = [
<template> <template>
<div> <div>
<VTabs v-model="activeTab" show-arrows> <VTabs
v-model="activeTab"
show-arrows
>
<VTab v-for="item in tabs" :key="item.icon" :value="item.tab"> <VTab v-for="item in tabs" :key="item.icon" :value="item.tab">
<VIcon size="20" start :icon="item.icon" /> <VIcon size="20" start :icon="item.icon" />
{{ item.title }} {{ item.title }}
@@ -68,15 +77,19 @@ const tabs = [
</VTabs> </VTabs>
<VDivider /> <VDivider />
<VWindow v-model="activeTab" class="mt-5 disable-tab-transition"> <VWindow
<!-- Account --> v-model="activeTab"
class="mt-5 disable-tab-transition"
:touch="false"
>
<!-- 用户 -->
<VWindowItem value="account"> <VWindowItem value="account">
<transition name="fade-slide" appear> <transition name="fade-slide" appear>
<AccountSettingAccount /> <AccountSettingAccount />
</transition> </transition>
</VWindowItem> </VWindowItem>
<!-- 用户 --> <!-- 站点 -->
<VWindowItem value="site"> <VWindowItem value="site">
<transition name="fade-slide" appear> <transition name="fade-slide" appear>
<AccountSettingSite /> <AccountSettingSite />
@@ -97,7 +110,14 @@ const tabs = [
</transition> </transition>
</VWindowItem> </VWindowItem>
<!-- Notification --> <!-- 服务 -->
<VWindowItem value="service">
<transition name="fade-slide" appear>
<AccountSettingService />
</transition>
</VWindowItem>
<!-- 规则 -->
<VWindowItem value="filter"> <VWindowItem value="filter">
<transition name="fade-slide" appear> <transition name="fade-slide" appear>
<AccountSettingRule /> <AccountSettingRule />

View File

@@ -185,7 +185,7 @@ async function addSubscribe(season = 0) {
// 全部存在时洗版 // 全部存在时洗版
best_version = !seasonsNotExisted.value[season] ? 1 : 0 best_version = !seasonsNotExisted.value[season] ? 1 : 0
// 请求API // 请求API
const result: { [key: string]: any } = await api.post('subscribe', { const result: { [key: string]: any } = await api.post('subscribe/', {
name: mediaDetail.value?.title, name: mediaDetail.value?.title,
type: mediaDetail.value?.type, type: mediaDetail.value?.type,
year: mediaDetail.value?.year, year: mediaDetail.value?.year,

View File

@@ -38,7 +38,7 @@ function pluginInstalled() {
// 获取插件列表数据 // 获取插件列表数据
async function fetchData() { async function fetchData() {
try { try {
dataList.value = await api.get('plugin') dataList.value = await api.get('plugin/')
isRefreshed.value = true isRefreshed.value = true
} }
catch (error) { catch (error) {

View File

@@ -17,7 +17,7 @@ const isRefreshed = ref(false)
// 获取订阅列表数据 // 获取订阅列表数据
async function fetchData() { async function fetchData() {
try { try {
dataList.value = await api.get('download') dataList.value = await api.get('download/')
isRefreshed.value = true isRefreshed.value = true
} }
catch (error) { catch (error) {

View File

@@ -1,15 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref } from 'vue'
import { useToast } from 'vue-toast-notification' import { useToast } from 'vue-toast-notification'
import { useConfirm } from 'vuetify-use-dialog' import { numberValidator } from '@/@validators'
import { numberValidator, requiredValidator } from '@/@validators'
import api from '@/api' import api from '@/api'
import type { TransferHistory } from '@/api/types' import type { TransferHistory } from '@/api/types'
import TmdbSelectorCard from '@/components/cards/TmdbSelectorCard.vue' import TmdbSelectorCard from '@/components/cards/TmdbSelectorCard.vue'
// 确认框
const createConfirm = useConfirm()
// 提示框 // 提示框
const $toast = useToast() const $toast = useToast()
@@ -24,6 +20,7 @@ const redoType = ref('电影')
// 类型下拉框:电影、电视剧 // 类型下拉框:电影、电视剧
const redoTypeItems = ref([ const redoTypeItems = ref([
{ title: '自动', value: '' },
{ title: '电影', value: '电影' }, { title: '电影', value: '电影' },
{ title: '电视剧', value: '电视剧' }, { title: '电视剧', value: '电视剧' },
]) ])
@@ -75,6 +72,12 @@ const progressValue = ref(0)
// TMDB选择对话框 // TMDB选择对话框
const tmdbSelectorDialog = ref(false) const tmdbSelectorDialog = ref(false)
// 删除确认对话框
const deleteConfirmDialog = ref(false)
// 确认框标题
const confirmTitle = ref('')
// 获取订阅列表数据 // 获取订阅列表数据
async function fetchData({ async function fetchData({
page, page,
@@ -129,74 +132,50 @@ const TransferDict: { [key: string]: string } = {
// 删除历史记录 // 删除历史记录
async function removeHistory(item: TransferHistory) { async function removeHistory(item: TransferHistory) {
const isConfirmed = await createConfirm({ currentHistory.value = item
title: '确认', confirmTitle.value = `确认删除 ${item.title} ${item.seasons}${item.episodes} ?`
content: `同步删除 ${item.title} 对应的媒体库文件 ?`, deleteConfirmDialog.value = true
confirmationText: '同步删除文件',
cancellationText: '仅删除历史记录',
dialogProps: {
maxWidth: '50rem',
},
confirmationButtonProps: {
color: 'error',
},
})
if (isConfirmed === undefined)
return
// 执行删除
remove(item, isConfirmed || false)
// 清空选中项
selected.value = []
} }
// 调用API删除记录 // 调用API删除记录
async function remove(item: TransferHistory, deleteFile: boolean) { async function remove(item: TransferHistory, deleteSrc: boolean, deleteDest: boolean) {
try { try {
// 调用删除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?deletesrc=${deleteSrc}&deletedest=${deleteDest}`, {
data: item, data: item,
}) })
if (result.success) { if (!result.success)
fetchData({
page: currentPage.value,
itemsPerPage: itemsPerPage.value,
})
}
else {
$toast.error(`删除失败: ${result.msg}`) $toast.error(`删除失败: ${result.msg}`)
}
} }
catch (error) { catch (error) {
console.error(error) console.error(error)
} }
} }
// 批量删除历史记录 // 删除单条记录
async function removeHistoryBatch() { async function removeSingle(deleteSrc: boolean, deleteDest: boolean) {
if (selected.value.length === 0) // 关闭弹窗
deleteConfirmDialog.value = false
if (!currentHistory.value)
return return
// 确认 // 删除
const isConfirmed = await createConfirm({ await remove(currentHistory.value, deleteSrc, deleteDest)
title: '确认', // 刷新
content: `同步删除 ${selected.value.length} 条记录对应的媒体库文件 ?`, fetchData({
confirmationText: '同步删除文件', page: currentPage.value,
cancellationText: '仅删除历史记录', itemsPerPage: itemsPerPage.value,
dialogProps: {
maxWidth: '50rem',
},
confirmationButtonProps: {
color: 'error',
},
}) })
if (isConfirmed === undefined) }
return
console.log(selected.value)
// 批量删除记录
async function removeBatch(deleteSrc: boolean, deleteDest: boolean) {
// 关闭弹窗
deleteConfirmDialog.value = false
// 总条数 // 总条数
const total = selected.value.length const total = selected.value.length
if (total === 0)
return
// 已处理条数 // 已处理条数
let handled = 0 let handled = 0
// 显示进度条 // 显示进度条
@@ -205,7 +184,7 @@ async function removeHistoryBatch() {
for (const item of selected.value) { for (const item of selected.value) {
// 开始删除 // 开始删除
progressText.value = `正在删除 ${item.title} ${item.seasons}${item.episodes} ...` progressText.value = `正在删除 ${item.title} ${item.seasons}${item.episodes} ...`
await remove(item, isConfirmed || false) await remove(item, deleteSrc, deleteDest)
// 删除完成 // 删除完成
handled++ handled++
progressValue.value = handled / total * 100 progressValue.value = handled / total * 100
@@ -221,27 +200,46 @@ async function removeHistoryBatch() {
}) })
} }
// 重新整理 // 响应删除操作
async function rehandleHistory() { async function deleteConfirmHandler(deleteSrc: boolean, deleteDest: boolean) {
if (currentHistory.value)
await removeSingle(deleteSrc, deleteDest)
else
await removeBatch(deleteSrc, deleteDest)
}
// 批量删除历史记录
async function removeHistoryBatch() {
if (selected.value.length === 0)
return
// 清空当前操作记录
currentHistory.value = undefined
confirmTitle.value = `确认删除 ${selected.value.length} 条记录 ?`
// 打开确认弹窗
deleteConfirmDialog.value = true
}
// 批量重新整理
async function retransferBatch() {
if (selected.value.length === 0)
return
// 清空当前操作记录
currentHistory.value = undefined
// 打开识别弹窗
redoType.value = ''
redoDialog.value = true
}
// 调API重新整理
async function retransfer(item: TransferHistory, redoType = '', redoTmdbId = 0) {
try { try {
if (!redoTmdbId.value || !redoType.value)
return
redoDialog.value = false
$toast.info(`正在重新整理 ${currentHistory.value?.title} ...`)
// 调用API接口重新转移
const requestData = {
...currentHistory.value,
}
const result: { [key: string]: any } = await api.post( const result: { [key: string]: any } = await api.post(
'history/transfer', 'history/transfer',
requestData, item,
{ {
params: { params: {
mtype: redoType.value, mtype: redoType,
new_tmdbid: parseInt(redoTmdbId.value), new_tmdbid: redoTmdbId,
}, },
}, },
) )
@@ -261,6 +259,50 @@ async function rehandleHistory() {
} }
} }
// 重新整理
async function rehandleHistory() {
try {
// 关闭弹窗
redoDialog.value = false
let tmdbid = 0
if (redoTmdbId.value)
tmdbid = parseInt(redoTmdbId.value)
// 转移当前选中记录
if (currentHistory.value) {
$toast.info(`正在重新整理 ${currentHistory.value?.title} ...`)
await retransfer(currentHistory.value, redoType.value, tmdbid)
}
else if (selected.value.length > 0) {
// 总条数
const total = selected.value.length
if (total === 0)
return
// 已处理条数
let handled = 0
// 显示进度条
progressDialog.value = true
for (const item of selected.value) {
progressText.value = `正在重新整理 ${item.src} ...`
await retransfer(item, redoType.value, tmdbid)
handled++
progressValue.value = handled / total * 100
}
// 清空选中项
selected.value = []
// 隐藏进度条
progressDialog.value = false
}
// 批量转移
else { $toast.error('没有选中任何记录!') }
}
catch (e) {
console.log(e)
}
}
// 弹出菜单 // 弹出菜单
const dropdownItems = ref([ const dropdownItems = ref([
{ {
@@ -269,6 +311,8 @@ const dropdownItems = ref([
props: { props: {
prependIcon: 'mdi-redo-variant', prependIcon: 'mdi-redo-variant',
click: (item: TransferHistory) => { click: (item: TransferHistory) => {
redoTmdbId.value = ''
redoType.value = ''
redoDialog.value = true redoDialog.value = true
currentHistory.value = item currentHistory.value = item
}, },
@@ -280,7 +324,9 @@ const dropdownItems = ref([
props: { props: {
prependIcon: 'mdi-trash-can-outline', prependIcon: 'mdi-trash-can-outline',
color: 'error', color: 'error',
click: removeHistory, click: (item: TransferHistory) => {
removeHistory(item)
},
}, },
}, },
]) ])
@@ -407,7 +453,6 @@ const dropdownItems = ref([
<VSelect <VSelect
v-model="redoType" v-model="redoType"
label="类型" label="类型"
:rules="[requiredValidator]"
:items="redoTypeItems" :items="redoTypeItems"
/> />
</VCol> </VCol>
@@ -415,9 +460,10 @@ const dropdownItems = ref([
<VTextField <VTextField
v-model="redoTmdbId" v-model="redoTmdbId"
label="TMDB编号" label="TMDB编号"
:rules="[requiredValidator, numberValidator]" placeholder="留空自动识别"
:rules="[numberValidator]"
append-inner-icon="mdi-magnify" append-inner-icon="mdi-magnify"
@click:append-inner="tmdbSelectorDialog = true" @click:append-inner.stop="tmdbSelectorDialog = true"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -435,6 +481,13 @@ const dropdownItems = ref([
</VCard> </VCard>
</VDialog> </VDialog>
<span v-if="selected.length > 0" class="fixed right-5 bottom-5"> <span v-if="selected.length > 0" class="fixed right-5 bottom-5">
<VBtn
icon="mdi-redo-variant"
class="me-2"
color="primary"
size="x-large"
@click="retransferBatch"
/>
<VBtn <VBtn
icon="mdi-trash-can-outline" icon="mdi-trash-can-outline"
color="error" color="error"
@@ -472,6 +525,45 @@ const dropdownItems = ref([
@close="tmdbSelectorDialog = false" @close="tmdbSelectorDialog = false"
/> />
</vDialog> </vDialog>
<!-- 底部弹窗 -->
<VBottomSheet v-model="deleteConfirmDialog" inset>
<VCard class="text-center">
<DialogCloseBtn @click="deleteConfirmDialog = false" />
<VCardTitle class="pe-10">
{{ confirmTitle }}
</VCardTitle>
<div class="d-flex flex-column flex-lg-row justify-center my-3">
<VBtn
color="primary"
class="mb-2 mx-2"
@click="deleteConfirmHandler(false, false)"
>
仅删除历史记录
</VBtn>
<VBtn
color="warning"
class="mb-2 mx-2"
@click="deleteConfirmHandler(true, false)"
>
删除历史记录和源文件
</VBtn>
<VBtn
color="info"
class="mb-2 mx-2"
@click="deleteConfirmHandler(false, true)"
>
删除历史记录和媒体库文件
</VBtn>
<VBtn
color="error"
class="mb-2 mx-2"
@click="deleteConfirmHandler(true, true)"
>
删除历史记录源文件和媒体库文件
</VBtn>
</div>
</VCard>
</VBottomSheet>
</template> </template>
<style lang="scss"> <style lang="scss">

View File

@@ -86,7 +86,7 @@ async function saveAccountInfo() {
accountInfo.value.password = newPassword.value accountInfo.value.password = newPassword.value
} }
try { try {
const result: { [key: string]: any } = await api.put('user', accountInfo.value) const result: { [key: string]: any } = await api.put('user/', accountInfo.value)
if (result.success) if (result.success)
$toast.success('用户信息保存成功!') $toast.success('用户信息保存成功!')
else else
@@ -100,7 +100,7 @@ async function saveAccountInfo() {
// 调用API查询所有用户 // 调用API查询所有用户
async function loadAllUsers() { async function loadAllUsers() {
try { try {
const result: User[] = await api.get('/user') const result: User[] = await api.get('/user/')
allUsers.value = result allUsers.value = result
} }
@@ -131,7 +131,7 @@ async function deactivateUser(user: User) {
try { try {
user.is_active = !user.is_active user.is_active = !user.is_active
const result: { [key: string]: any } = await api.put('user', user) const result: { [key: string]: any } = await api.put('user/', user)
if (result.success) { if (result.success) {
$toast.success('用户冻结成功!') $toast.success('用户冻结成功!')
loadAllUsers() loadAllUsers()

View File

@@ -64,6 +64,9 @@ onMounted(() => {
<th scope="col"> <th scope="col">
Slack Slack
</th> </th>
<th scope="col">
SynologyChat
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -83,6 +86,9 @@ onMounted(() => {
<td> <td>
<VCheckbox v-model="message.slack" /> <VCheckbox v-model="message.slack" />
</td> </td>
<td>
<VCheckbox v-model="message.synologychat" />
</td>
</tr> </tr>
<tr v-if="messagemTypes.length === 0"> <tr v-if="messagemTypes.length === 0">
<td <td

View File

@@ -15,7 +15,7 @@ const TorrentPriorityItems = [
] ]
// 包含与排除规则 // 包含与排除规则
const defaultIncludeExcludeFilter = ref({ const defaultFilterRules = ref({
include: '', include: '',
exclude: '', exclude: '',
}) })
@@ -35,13 +35,13 @@ async function queryTorrentPriority() {
} }
// 查询包含与排除规则 // 查询包含与排除规则
async function queryIncludeExcludeFilter() { async function queryDefaultFilter() {
try { try {
const result: { [key: string]: any } = await api.get( const result: { [key: string]: any } = await api.get(
'system/setting/DefaultIncludeExcludeFilter', 'system/setting/DefaultFilterRules',
) )
if (result.data?.value) if (result.data?.value)
defaultIncludeExcludeFilter.value = result.data?.value defaultFilterRules.value = result.data?.value
} }
catch (error) { catch (error) {
console.log(error) console.log(error)
@@ -68,11 +68,11 @@ async function saveTorrentPriority() {
} }
// 保存包含与排除规则 // 保存包含与排除规则
async function saveIncludeExcludeFilter() { async function saveDefaultFilter() {
try { try {
const result: { [key: string]: any } = await api.post( const result: { [key: string]: any } = await api.post(
'system/setting/DefaultIncludeExcludeFilter', 'system/setting/DefaultFilterRules',
defaultIncludeExcludeFilter.value, defaultFilterRules.value,
) )
if (result.success) if (result.success)
$toast.success('默认包含/排除规则保存成功') $toast.success('默认包含/排除规则保存成功')
@@ -86,7 +86,7 @@ async function saveIncludeExcludeFilter() {
onMounted(() => { onMounted(() => {
queryTorrentPriority() queryTorrentPriority()
queryIncludeExcludeFilter() queryDefaultFilter()
}) })
</script> </script>
@@ -127,14 +127,14 @@ onMounted(() => {
<VRow> <VRow>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<VTextField <VTextField
v-model="defaultIncludeExcludeFilter.include" v-model="defaultFilterRules.include"
type="text" type="text"
label="包含(关键字、正则式)" label="包含(关键字、正则式)"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<VTextField <VTextField
v-model="defaultIncludeExcludeFilter.exclude" v-model="defaultFilterRules.exclude"
type="text" type="text"
label="排除(关键字、正则式)" label="排除(关键字、正则式)"
/> />
@@ -145,7 +145,7 @@ onMounted(() => {
<VCardItem> <VCardItem>
<VBtn <VBtn
type="submit" type="submit"
@click="saveIncludeExcludeFilter" @click="saveDefaultFilter"
> >
保存 保存
</VBtn> </VBtn>

View File

@@ -109,7 +109,7 @@ function addFilterCard() {
// 查询所有站点 // 查询所有站点
async function querySites() { async function querySites() {
try { try {
const data: Site[] = await api.get('site') const data: Site[] = await api.get('site/')
// 过滤站点,只有启用的站点才显示 // 过滤站点,只有启用的站点才显示
allSites.value = data.filter(item => item.is_active) allSites.value = data.filter(item => item.is_active)
@@ -148,6 +148,48 @@ async function saveSelectedSites() {
} }
} }
// 上调优先级
function onLevelUp(pri: string) {
// 找到当前卡片
const card = filterCards.value.find(card => card.pri === pri)
if (!card)
return
// 找到当前卡片的上一张卡片
const prevCard = filterCards.value.find(card => card.pri === (parseInt(pri) - 1).toString())
if (!prevCard)
return
// 交换两张卡片的优先级
const temp = card.pri
card.pri = prevCard.pri
prevCard.pri = temp
// 卡片重新按优先级排序
filterCards.value.sort((a, b) => parseInt(a.pri) - parseInt(b.pri))
}
// 下调优先级
function onLevelDown(pri: string) {
// 找到当前卡片
const card = filterCards.value.find(card => card.pri === pri)
if (!card)
return
// 找到当前卡片的下一张卡片
const nextCard = filterCards.value.find(card => card.pri === (parseInt(pri) + 1).toString())
if (!nextCard)
return
// 交换两张卡片的优先级
const temp = card.pri
card.pri = nextCard.pri
nextCard.pri = temp
// 卡片重新按优先级排序
filterCards.value.sort((a, b) => parseInt(a.pri) - parseInt(b.pri))
}
onMounted(() => { onMounted(() => {
queryCustomFilters() queryCustomFilters()
querySites() querySites()
@@ -191,9 +233,12 @@ onMounted(() => {
v-for="(card, index) in filterCards" v-for="(card, index) in filterCards"
:key="index" :key="index"
:pri="card.pri" :pri="card.pri"
:maxpri="filterCards.length.toString()"
:rules="card.rules" :rules="card.rules"
@changed="updateFilterCardValue" @changed="updateFilterCardValue"
@close="filterCardClose(card.pri)" @close="filterCardClose(card.pri)"
@leveldown="onLevelDown"
@levelup="onLevelUp"
/> />
</div> </div>
</VCardItem> </VCardItem>

View File

@@ -0,0 +1,138 @@
<script lang="ts" setup>
import { useToast } from 'vue-toast-notification'
import api from '@/api'
import type { ScheduleInfo } from '@/api/types'
// 提示框
const $toast = useToast()
// 定时服务列表
const schedulerList = ref<ScheduleInfo[]>([])
// 定时器
let refreshTimer: NodeJS.Timer | null = null
// 调用API加载定时服务列表
async function loadSchedulerList() {
try {
const res: ScheduleInfo[] = await api.get('dashboard/schedule')
schedulerList.value = res
}
catch (e) {
console.log(e)
}
}
// 任务状态颜色
function getSchedulerColor(status: string) {
switch (status) {
case '正在运行':
return 'success'
case '已停止':
return 'error'
case '等待':
return ''
default:
return ''
}
}
// 执行命令
function runCommand(id: string) {
try {
// 异步提交
api.get('system/runscheduler', {
params: {
jobid: id,
},
})
$toast.success('定时作业执行请求提交成功!')
// 1秒后刷新数据
setTimeout(() => {
loadSchedulerList()
}, 1000)
}
catch (e) {
console.log(e)
}
}
onMounted(() => {
loadSchedulerList()
// 启动定时器
refreshTimer = setInterval(() => {
loadSchedulerList()
}, 5000)
})
// 组件卸载时停止定时器
onUnmounted(() => {
if (refreshTimer) {
clearInterval(refreshTimer)
refreshTimer = null
}
})
</script>
<template>
<VCard title="定时作业">
<VCardText> 手动执行不会影响作业正常的时间表 </VCardText>
<VTable class="text-no-wrap">
<thead>
<tr>
<th scope="col">
任务名称
</th>
<th scope="col">
任务状态
</th>
<th scope="col">
下一次执行时间
</th>
<th scope="col" />
</tr>
</thead>
<tbody>
<tr
v-for="scheduler in schedulerList"
:key="scheduler.id"
>
<td>
{{ scheduler.name }}
</td>
<td>
<VChip :color="getSchedulerColor(scheduler.status)">
{{ scheduler.status }}
</VChip>
</td>
<td>
{{ scheduler.next_run }}
</td>
<td>
<VBtn
size="small"
:disabled="scheduler.status === '正在运行'"
@click="runCommand(scheduler.id)"
>
<template #prepend>
<VIcon>mdi-play</VIcon>
</template>
执行
</VBtn>
</td>
</tr>
<tr v-if="schedulerList.length === 0">
<td
colspan="4"
class="text-center"
>
没有后台服务
</td>
</tr>
</tbody>
</VTable>
</VCard>
</template>

View File

@@ -57,7 +57,7 @@ async function saveSelectedRssSites() {
// 查询所有站点 // 查询所有站点
async function querySites() { async function querySites() {
try { try {
const data: Site[] = await api.get('site') const data: Site[] = await api.get('site/')
// 过滤站点,只有启用的站点才显示 // 过滤站点,只有启用的站点才显示
allSites.value = data.filter(item => item.is_active) allSites.value = data.filter(item => item.is_active)
@@ -165,6 +165,48 @@ function addFilterCard(ruleType: string) {
cards.value.push(newCard) cards.value.push(newCard)
} }
// 上调优先级
function onLevelUp(filterCards: FilterCard[], pri: string) {
// 找到当前卡片
const card = filterCards.find(card => card.pri === pri)
if (!card)
return
// 找到当前卡片的上一张卡片
const prevCard = filterCards.find(card => card.pri === (parseInt(pri) - 1).toString())
if (!prevCard)
return
// 交换两张卡片的优先级
const temp = card.pri
card.pri = prevCard.pri
prevCard.pri = temp
// 卡片重新按优先级排序
filterCards.sort((a, b) => parseInt(a.pri) - parseInt(b.pri))
}
// 下调优先级
function onLevelDown(filterCards: FilterCard[], pri: string) {
// 找到当前卡片
const card = filterCards.find(card => card.pri === pri)
if (!card)
return
// 找到当前卡片的下一张卡片
const nextCard = filterCards.find(card => card.pri === (parseInt(pri) + 1).toString())
if (!nextCard)
return
// 交换两张卡片的优先级
const temp = card.pri
card.pri = nextCard.pri
nextCard.pri = temp
// 卡片重新按优先级排序
filterCards.sort((a, b) => parseInt(a.pri) - parseInt(b.pri))
}
onMounted(() => { onMounted(() => {
querySites() querySites()
queryCustomFilters('SubscribeFilterRules') queryCustomFilters('SubscribeFilterRules')
@@ -209,9 +251,12 @@ onMounted(() => {
v-for="(card, index) in subscribeFilterCards" v-for="(card, index) in subscribeFilterCards"
:key="index" :key="index"
:pri="card.pri" :pri="card.pri"
:maxpri="subscribeFilterCards.length.toString()"
:rules="card.rules" :rules="card.rules"
@changed="updateFilterCardValue" @changed="updateFilterCardValue"
@close="filterCardClose('SubscribeFilterRules', card.pri)" @close="filterCardClose('SubscribeFilterRules', card.pri)"
@leveldown="onLevelDown(subscribeFilterCards, card.pri)"
@levelup="onLevelUp(subscribeFilterCards, card.pri)"
/> />
</div> </div>
</VCardItem> </VCardItem>
@@ -242,9 +287,12 @@ onMounted(() => {
v-for="(card, index) in bestVersionFilterCards" v-for="(card, index) in bestVersionFilterCards"
:key="index" :key="index"
:pri="card.pri" :pri="card.pri"
:maxpri="bestVersionFilterCards.length.toString()"
:rules="card.rules" :rules="card.rules"
@changed="updateFilterCardValue2" @changed="updateFilterCardValue2"
@close="filterCardClose('BestVersionFilterRules', card.pri)" @close="filterCardClose('BestVersionFilterRules', card.pri)"
@leveldown="onLevelDown(bestVersionFilterCards, card.pri)"
@levelup="onLevelUp(bestVersionFilterCards, card.pri)"
/> />
</div> </div>
</VCardItem> </VCardItem>

View File

@@ -58,7 +58,7 @@ const siteForm = reactive<Site>({
// 获取站点列表数据 // 获取站点列表数据
async function fetchData() { async function fetchData() {
try { try {
dataList.value = await api.get('site') dataList.value = await api.get('site/')
isRefreshed.value = true isRefreshed.value = true
} }
catch (error) { catch (error) {
@@ -77,7 +77,7 @@ async function addSite() {
addBtnState.value = true addBtnState.value = true
try { try {
const result: { [key: string]: string } = await api.post('site', siteForm) const result: { [key: string]: string } = await api.post('site/', siteForm)
if (result.success) { if (result.success) {
$toast.success('新增站点成功') $toast.success('新增站点成功')

View File

@@ -73,7 +73,7 @@ async function eventsHander(subscribe: Subscribe | Rss) {
async function getSubscribes() { async function getSubscribes() {
try { try {
// 订阅 // 订阅
const subscribes: Subscribe[] = await api.get('subscribe') const subscribes: Subscribe[] = await api.get('subscribe/')
const subEvents = await Promise.all( const subEvents = await Promise.all(
subscribes.map(async sub => eventsHander(sub)), subscribes.map(async sub => eventsHander(sub)),

View File

@@ -19,7 +19,7 @@ const dataList = ref<Subscribe[]>([])
// 获取订阅列表数据 // 获取订阅列表数据
async function fetchData() { async function fetchData() {
try { try {
dataList.value = await api.get('subscribe') dataList.value = await api.get('subscribe/')
isRefreshed.value = true isRefreshed.value = true
} }
catch (error) { catch (error) {