Compare commits

..

12 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
22 changed files with 105 additions and 1070 deletions

View File

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

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

@@ -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

@@ -840,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

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

@@ -212,7 +212,7 @@ const dropdownItems = ref([
</IconBtn>
</div>
<VAvatar
size="128"
size="8rem"
:class="{ shadow: isImageLoaded }"
>
<VImg
@@ -236,7 +236,7 @@ const dropdownItems = ref([
<!-- 插件配置页面 -->
<VDialog
v-model="pluginConfigDialog"
max-width="800"
max-width="50rem"
scrollable
persistent
>
@@ -265,7 +265,7 @@ const dropdownItems = ref([
<!-- 插件详情页面 -->
<VDialog
v-model="pluginInfoDialog"
max-width="1000"
max-width="62.5rem"
scrollable
persistent
>

View File

@@ -1,594 +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}`">
<DialogCloseBtn @click="rssInfoDialog = false" />
<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"
md="6"
>
<VTextField
v-model="rssForm.include"
label="包含"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<VTextField
v-model="rssForm.exclude"
label="排除"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<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: '',
@@ -431,7 +428,7 @@ onMounted(() => {
<!-- 站点编辑弹窗 -->
<VDialog
v-model="siteInfoDialog"
max-width="1000"
max-width="50rem"
persistent
scrollable
>
@@ -474,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"
@@ -556,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

@@ -2,10 +2,17 @@
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<{}[]>([])
const items = ref<TmdbItem[]>([])
// 搜索词
const keyword = ref('')
@@ -14,12 +21,19 @@ const keyword = ref('')
const loading = ref(false)
// 选中条目
function selectMedia(item: MediaInfo) {
function selectMedia(item: TmdbItem) {
console.log(item)
emit('update:modelValue', item.tmdb_id)
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)
@@ -41,13 +55,11 @@ async function searchMedias() {
// 赋值
for (const item of result) {
if (items.value.length > 0)
items.value.push({ type: 'divider', inset: true })
items.value.push({
prependAvatar: item.poster_path,
tmdbid: item.tmdb_id || 0,
poster: getW500Image(item.poster_path),
title: `${item.title}${item.year}`,
subtitle: `<span class="text-primary">${item.type}</span> ${item.overview}`,
onClick: () => selectMedia(item),
overview: `<span class="text-primary">${item.type}</span> ${item.overview}`,
})
}
loading.value = false
@@ -81,12 +93,35 @@ async function searchMedias() {
<VList
v-if="items.length > 0"
:items="items"
item-props
lines="three"
>
<template #subtitle="{ subtitle }">
<div v-html="subtitle" />
<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>

View File

@@ -613,7 +613,7 @@ onMounted(() => {
<!-- 文件整理弹窗 -->
<VDialog
v-model="transferPopper"
max-width="800"
max-width="50rem"
scrollable
>
<template #activator="{ props }">
@@ -744,7 +744,7 @@ onMounted(() => {
<vDialog
v-model="progressDialog"
:scrim="false"
width="400"
width="25rem"
>
<vCard
color="primary"
@@ -763,7 +763,7 @@ onMounted(() => {
<!-- 识别结果对话框 -->
<vDialog
v-model="nameTestDialog"
width="800"
width="50rem"
>
<vCard>
<DialogCloseBtn @click="nameTestDialog = false" />
@@ -775,7 +775,7 @@ onMounted(() => {
<!-- TMDB ID搜索框 -->
<vDialog
v-model="tmdbSelectorDialog"
width="600"
width="40rem"
scrollable
>
<TmdbSelectorCard

View File

@@ -106,14 +106,6 @@ const superUser = store.state.auth.superUser
to: '/subscribe-tv',
}"
/>
<VerticalNavLink
v-if="superUser"
:item="{
title: '自定义',
icon: 'mdi-rss',
to: '/subscribe-rss',
}"
/>
<VerticalNavLink
:item="{
title: '日历',

View File

@@ -172,7 +172,7 @@ const avatar = store.state.auth.avatar
<!-- 重启进度框 -->
<vDialog
v-model="progressDialog"
width="400"
width="25rem"
>
<vCard
color="primary"

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

@@ -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

@@ -446,7 +446,7 @@ const dropdownItems = ref([
<vDialog
v-model="progressDialog"
:scrim="false"
width="400"
width="25rem"
>
<vCard
color="primary"

View File

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

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,336 +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="新增自定义订阅">
<DialogCloseBtn @click="rssAddDialog = false" />
<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"
md="6"
>
<VTextField
v-model="rssForm.include"
label="包含"
placeholder="支持正则表达式"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<VTextField
v-model="rssForm.exclude"
label="排除"
placeholder="支持正则表达式"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<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>