mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-05-11 01:50:10 +08:00
Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43b1f7e620 | ||
|
|
ba76f79d85 | ||
|
|
ce47afa698 | ||
|
|
6da110948c | ||
|
|
533c564db5 | ||
|
|
4a65056909 | ||
|
|
c52ad73101 | ||
|
|
5a3673efc6 | ||
|
|
c03ec1d741 | ||
|
|
e62d0809b3 | ||
|
|
7f13597517 | ||
|
|
c822f1fffd | ||
|
|
14ca74a29d | ||
|
|
3ee897a350 | ||
|
|
789aac60c9 | ||
|
|
2c73a8f3e1 | ||
|
|
539bc656f8 | ||
|
|
feda0cad2d | ||
|
|
c723d89739 | ||
|
|
0a0e7a059a | ||
|
|
0263fbbee6 | ||
|
|
e205296e22 | ||
|
|
261f5a9c68 | ||
|
|
fa097651f4 | ||
|
|
c94d5f7e7d | ||
|
|
e34f18799f | ||
|
|
1681a311f7 | ||
|
|
da08d8ec19 | ||
|
|
730178c838 | ||
|
|
a04450ae98 | ||
|
|
2b2fd66a29 | ||
|
|
58fe08ad3d | ||
|
|
240d6bede0 | ||
|
|
23d808f8b1 | ||
|
|
2f293706cb | ||
|
|
9aaaf0c520 | ||
|
|
6694e7e929 | ||
|
|
d3768cb994 | ||
|
|
c59d3e28b9 | ||
|
|
914239f434 |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "moviepilot",
|
||||
"version": "1.3.1",
|
||||
"version": "1.3.4-2",
|
||||
"private": true,
|
||||
"bin": "dist/service.js",
|
||||
"scripts": {
|
||||
|
||||
BIN
public/plugin_icon/world.png
Normal file
BIN
public/plugin_icon/world.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 81 KiB |
@@ -44,6 +44,15 @@ export interface Subscribe {
|
||||
// 排除
|
||||
exclude?: string
|
||||
|
||||
// 质量
|
||||
quality?: string
|
||||
|
||||
// 分辨率
|
||||
resolution?: string
|
||||
|
||||
// 特效
|
||||
effect?: string
|
||||
|
||||
// 总集数
|
||||
total_episode?: number
|
||||
|
||||
@@ -68,8 +77,8 @@ export interface Subscribe {
|
||||
// 订阅站点
|
||||
sites: number[]
|
||||
|
||||
// 是否洗版
|
||||
best_version: number
|
||||
// 是否洗版,数字或者boolean
|
||||
best_version: any
|
||||
|
||||
// 当前优先级
|
||||
current_priority: number
|
||||
@@ -407,13 +416,13 @@ export interface Site {
|
||||
ua?: string
|
||||
|
||||
// 是否使用代理
|
||||
proxy?: number
|
||||
proxy?: any
|
||||
|
||||
// 过滤规则
|
||||
filter?: string
|
||||
|
||||
// 是否演染
|
||||
render?: number
|
||||
render?: any
|
||||
|
||||
// 是否公开站点
|
||||
public?: number
|
||||
@@ -469,6 +478,9 @@ export interface DownloadingInfo {
|
||||
|
||||
// 媒体信息
|
||||
media: { [key: string]: any }
|
||||
|
||||
// 下载用户
|
||||
userid?: string
|
||||
}
|
||||
|
||||
// 缺失剧集信息
|
||||
|
||||
@@ -35,6 +35,11 @@ function filtersChanged(value: string[]) {
|
||||
const selectFilterOptions = ref<{ [key: string]: string }[]>([
|
||||
{ title: '特效字幕', value: ' SPECSUB ' },
|
||||
{ title: '中文字幕', value: ' CNSUB ' },
|
||||
{ title: '国语配音', value: ' CNVOI ' },
|
||||
{ title: '排除: 国语配音', value: ' !CNVOI ' },
|
||||
{ title: '粤语配音', value: ' HKVOI ' },
|
||||
{ title: '排除: 粤语配音', value: ' !HKVOI ' },
|
||||
{ title: '促销: 免费', value: ' FREE ' },
|
||||
{ title: '分辨率: 4K', value: ' 4K ' },
|
||||
{ title: '分辨率: 1080P', value: ' 1080P ' },
|
||||
{ title: '分辨率: 720P', value: ' 720P ' },
|
||||
@@ -49,17 +54,22 @@ const selectFilterOptions = ref<{ [key: string]: string }[]>([
|
||||
{ title: '排除: REMUX', value: ' !REMUX ' },
|
||||
{ title: '质量: WEB-DL', value: ' WEBDL ' },
|
||||
{ title: '排除: WEB-DL', value: ' !WEBDL ' },
|
||||
{ title: '质量: 60fps', value: ' 60FPS ' },
|
||||
{ title: '排除: 60fps', value: ' !60FPS ' },
|
||||
{ title: '编码: H265', value: ' H265 ' },
|
||||
{ title: '排除: H265', value: ' !H265 ' },
|
||||
{ title: '编码: H264', value: ' H264 ' },
|
||||
{ title: '排除: H264', value: ' !H264 ' },
|
||||
{ title: '效果: 杜比视界', value: ' DOLBY ' },
|
||||
{ title: '排除: 杜比视界', value: ' !DOLBY ' },
|
||||
{ title: '效果: 杜比全景声', value: ' ATMOS ' },
|
||||
{ title: '排除: 杜比全景声', value: ' !ATMOS ' },
|
||||
{ title: '效果: HDR', value: ' HDR ' },
|
||||
{ title: '排除: HDR', value: ' !HDR ' },
|
||||
{ title: '国语配音', value: ' CNVOI ' },
|
||||
{ title: '排除: 国语配音', value: ' !CNVOI ' },
|
||||
{ title: '促销: 免费', value: ' FREE ' },
|
||||
{ title: '效果: SDR', value: ' SDR ' },
|
||||
{ title: '排除: SDR', value: ' !SDR ' },
|
||||
{ title: '效果: 3D', value: ' 3D ' },
|
||||
{ title: '排除: 3D', value: ' !3D ' },
|
||||
])
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import type { PropType, Ref } from 'vue'
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import SubscribeEditForm from '../form/SubscribeEditForm.vue'
|
||||
import { formatSeason } from '@/@core/utils/formatters'
|
||||
import api from '@/api'
|
||||
import { doneNProgress, startNProgress } from '@/api/nprogress'
|
||||
@@ -39,6 +40,12 @@ const seasonsNotExisted = ref<{ [key: number]: number }>({})
|
||||
// 订阅季弹窗
|
||||
const subscribeSeasonDialog = ref(false)
|
||||
|
||||
// 订阅编辑弹窗
|
||||
const subscribeEditDialog = ref(false)
|
||||
|
||||
// 订阅ID
|
||||
const subscribeId = ref(0)
|
||||
|
||||
// 季详情
|
||||
const seasonInfos = ref<TmdbSeason[]>([])
|
||||
|
||||
@@ -86,6 +93,7 @@ async function handleAddSubscribe() {
|
||||
}
|
||||
else {
|
||||
// 弹出季选择列表,支持多选
|
||||
seasonsSelected.value = []
|
||||
subscribeSeasonDialog.value = true
|
||||
}
|
||||
}
|
||||
@@ -136,6 +144,12 @@ async function addSubscribe(season = 0) {
|
||||
result.message,
|
||||
best_version,
|
||||
)
|
||||
|
||||
// 弹出订阅编辑弹窗
|
||||
if (result.success && seasonsSelected.value.length <= 1) {
|
||||
subscribeId.value = result.data.id
|
||||
subscribeEditDialog.value = true
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
@@ -156,9 +170,9 @@ function showSubscribeAddToast(result: boolean,
|
||||
if (best_version > 0)
|
||||
subname = '洗版订阅'
|
||||
|
||||
if (result)
|
||||
if (result && seasonsSelected.value.length > 1)
|
||||
$toast.success(`${title} 添加${subname}成功!`)
|
||||
else
|
||||
else if (!result)
|
||||
$toast.error(`${title} 添加${subname}失败:${message}!`)
|
||||
}
|
||||
|
||||
@@ -480,7 +494,7 @@ function getYear(airDate: string) {
|
||||
inset
|
||||
scrollable
|
||||
>
|
||||
<VCard>
|
||||
<VCard class="rounded-t">
|
||||
<DialogCloseBtn @click="subscribeSeasonDialog = false" />
|
||||
<VCardTitle class="pe-10">
|
||||
订阅 - {{ props.media?.title }}
|
||||
@@ -557,6 +571,14 @@ function getYear(airDate: string) {
|
||||
</div>
|
||||
</VCard>
|
||||
</VBottomSheet>
|
||||
<!-- 订阅编辑弹窗 -->
|
||||
<SubscribeEditForm
|
||||
v-model="subscribeEditDialog"
|
||||
:subid="subscribeId"
|
||||
@close="subscribeEditDialog = false"
|
||||
@save="subscribeEditDialog = false"
|
||||
@remove="() => { subscribeEditDialog = false; handleCheckSubscribe(); }"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -236,11 +236,13 @@ const dropdownItems = ref([
|
||||
<!-- 插件配置页面 -->
|
||||
<VDialog
|
||||
v-model="pluginConfigDialog"
|
||||
max-width="50rem"
|
||||
scrollable
|
||||
persistent
|
||||
max-width="60rem"
|
||||
>
|
||||
<VCard :title="`${props.plugin?.plugin_name} - 配置`">
|
||||
<VCard
|
||||
:title="`${props.plugin?.plugin_name} - 配置`"
|
||||
class="rounded-t"
|
||||
>
|
||||
<DialogCloseBtn @click="pluginConfigDialog = false" />
|
||||
<VCardText>
|
||||
<FormRender
|
||||
@@ -255,7 +257,10 @@ const dropdownItems = ref([
|
||||
查看详情
|
||||
</VBtn>
|
||||
<VSpacer />
|
||||
<VBtn @click="savePluginConf">
|
||||
<VBtn
|
||||
variant="tonal"
|
||||
@click="savePluginConf"
|
||||
>
|
||||
保存
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
@@ -265,11 +270,13 @@ const dropdownItems = ref([
|
||||
<!-- 插件详情页面 -->
|
||||
<VDialog
|
||||
v-model="pluginInfoDialog"
|
||||
max-width="62.5rem"
|
||||
scrollable
|
||||
persistent
|
||||
max-width="80rem"
|
||||
>
|
||||
<VCard :title="`${props.plugin?.plugin_name}`">
|
||||
<VCard
|
||||
:title="`${props.plugin?.plugin_name}`"
|
||||
class="rounded-t"
|
||||
>
|
||||
<DialogCloseBtn @click="pluginInfoDialog = false" />
|
||||
<VCardText>
|
||||
<PageRender
|
||||
@@ -279,11 +286,16 @@ const dropdownItems = ref([
|
||||
/>
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VBtn @click="showPluginConfig">
|
||||
<VBtn
|
||||
@click="showPluginConfig"
|
||||
>
|
||||
配置
|
||||
</VBtn>
|
||||
<VSpacer />
|
||||
<VBtn @click="pluginInfoDialog = false">
|
||||
<VBtn
|
||||
variant="tonal"
|
||||
@click="pluginInfoDialog = false"
|
||||
>
|
||||
关闭
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import type { PropType } from 'vue'
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import SiteAddEditForm from '../form/SiteAddEditForm.vue'
|
||||
import { formatFileSize } from '@core/utils/formatters'
|
||||
import { numberValidator, requiredValidator } from '@/@validators'
|
||||
import { requiredValidator } from '@/@validators'
|
||||
import api from '@/api'
|
||||
import type { Site, TorrentInfo } from '@/api/types'
|
||||
import ExistIcon from '@core/components/ExistIcon.vue'
|
||||
@@ -15,7 +16,7 @@ const cardProps = defineProps({
|
||||
})
|
||||
|
||||
// 定义触发的自定义事件
|
||||
const emit = defineEmits(['remove', 'update'])
|
||||
const emit = defineEmits(['update', 'remove'])
|
||||
|
||||
// 密码输入
|
||||
const isPasswordVisible = ref(false)
|
||||
@@ -42,7 +43,7 @@ const updateButtonDisable = ref(false)
|
||||
const siteCookieDialog = ref(false)
|
||||
|
||||
// 站点编辑弹窗
|
||||
const siteInfoDialog = ref(false)
|
||||
const siteEditDialog = ref(false)
|
||||
|
||||
// 资源浏览弹窗
|
||||
const resourceDialog = ref(false)
|
||||
@@ -78,27 +79,6 @@ const userPwForm = ref({
|
||||
password: '',
|
||||
})
|
||||
|
||||
// 状态下拉项
|
||||
const statusItems = [
|
||||
{ title: '启用', value: true },
|
||||
{ title: '停用', value: false },
|
||||
]
|
||||
|
||||
// 生成1到50的优先级下拉框选项
|
||||
const priorityItems = ref(
|
||||
Array.from({ length: 50 }, (_, i) => i + 1).map(item => ({
|
||||
title: item,
|
||||
value: item,
|
||||
})),
|
||||
)
|
||||
|
||||
// 站点编辑表单数据
|
||||
const siteForm = reactive<any>(cardProps.site ?? {})
|
||||
|
||||
// 类型转换
|
||||
siteForm.proxy = siteForm.proxy === 1
|
||||
siteForm.render = siteForm.render === 1
|
||||
|
||||
// 打开种子详情页面
|
||||
function openTorrentDetail(page_url: string) {
|
||||
window.open(page_url, '_blank')
|
||||
@@ -144,11 +124,6 @@ async function handleSiteUpdate() {
|
||||
siteCookieDialog.value = true
|
||||
}
|
||||
|
||||
// 打开站点编辑弹窗
|
||||
async function handleSiteInfo() {
|
||||
siteInfoDialog.value = true
|
||||
}
|
||||
|
||||
// 打开资源浏览弹窗
|
||||
async function handleResourceBrowse() {
|
||||
resourceDialog.value = true
|
||||
@@ -189,42 +164,6 @@ async function updateSiteCookie() {
|
||||
}
|
||||
}
|
||||
|
||||
// 调用API删除站点信息
|
||||
async function deleteSiteInfo() {
|
||||
try {
|
||||
siteInfoDialog.value = false
|
||||
const result: { [key: string]: any } = await api.delete(`site/${cardProps.site?.id}`)
|
||||
if (result.success) {
|
||||
$toast.success(`${cardProps.site?.name} 删除成功!`)
|
||||
emit('remove')
|
||||
}
|
||||
else { $toast.error(`${cardProps.site?.name} 删除失败:${result.message}`) }
|
||||
}
|
||||
catch (error) {
|
||||
$toast.error(`${cardProps.site?.name} 删除失败!`)
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 调用API更新站点信息
|
||||
async function updateSiteInfo() {
|
||||
try {
|
||||
// 更新按钮状态
|
||||
siteInfoDialog.value = false
|
||||
|
||||
const result: { [key: string]: any } = await api.put('site/', siteForm)
|
||||
if (result.success) {
|
||||
$toast.success(`${cardProps.site?.name} 更新成功!`)
|
||||
emit('update')
|
||||
}
|
||||
else { $toast.error(`${cardProps.site?.name} 更新失败:${result.message}`) }
|
||||
}
|
||||
catch (error) {
|
||||
$toast.error(`${cardProps.site?.name} 更新失败!`)
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 促销Chip类
|
||||
function getVolumeFactorClass(downloadVolume: number, uploadVolume: number) {
|
||||
if (downloadVolume === 0)
|
||||
@@ -264,9 +203,9 @@ onMounted(() => {
|
||||
<VCard
|
||||
:height="cardProps.height"
|
||||
:width="cardProps.width"
|
||||
:flat="!siteForm.is_active"
|
||||
:flat="!cardProps.site?.is_active"
|
||||
class="overflow-hidden"
|
||||
@click="handleSiteInfo"
|
||||
@click="siteEditDialog = true"
|
||||
>
|
||||
<template #image>
|
||||
<VAvatar
|
||||
@@ -278,17 +217,19 @@ onMounted(() => {
|
||||
</VAvatar>
|
||||
</template>
|
||||
<VCardItem>
|
||||
<VCardTitle class="font-bold" @click.stop="openSitePage">
|
||||
{{ cardProps.site?.name }}
|
||||
<VCardTitle class="font-bold">
|
||||
<span @click.stop="openSitePage">{{ cardProps.site?.name }}</span>
|
||||
</VCardTitle>
|
||||
<VCardSubtitle>{{ cardProps.site?.url }}</VCardSubtitle>
|
||||
<VCardSubtitle>
|
||||
{{ cardProps.site?.url }}
|
||||
</VCardSubtitle>
|
||||
</VCardItem>
|
||||
|
||||
<ExistIcon v-if="siteForm.is_active" />
|
||||
<ExistIcon v-if="cardProps.site?.is_active" />
|
||||
|
||||
<VCardText class="py-2">
|
||||
<VTooltip
|
||||
v-if="siteForm.render"
|
||||
v-if="cardProps.site?.render === 1"
|
||||
text="浏览器仿真"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
@@ -302,7 +243,7 @@ onMounted(() => {
|
||||
</VTooltip>
|
||||
|
||||
<VTooltip
|
||||
v-if="siteForm.proxy"
|
||||
v-if="cardProps.site?.proxy === 1"
|
||||
text="代理"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
@@ -316,7 +257,7 @@ onMounted(() => {
|
||||
</VTooltip>
|
||||
|
||||
<VTooltip
|
||||
v-if="siteForm.limit_interval"
|
||||
v-if="cardProps.site?.limit_interval"
|
||||
text="流控"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
@@ -330,7 +271,7 @@ onMounted(() => {
|
||||
</VTooltip>
|
||||
|
||||
<VTooltip
|
||||
v-if="siteForm.filter"
|
||||
v-if="cardProps.site?.filter"
|
||||
text="过滤"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
@@ -419,143 +360,22 @@ onMounted(() => {
|
||||
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
<VBtn @click="updateSiteCookie">
|
||||
<VBtn
|
||||
variant="tonal"
|
||||
@click="updateSiteCookie"
|
||||
>
|
||||
开始更新
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
<!-- 站点编辑弹窗 -->
|
||||
<VDialog
|
||||
v-model="siteInfoDialog"
|
||||
max-width="50rem"
|
||||
persistent
|
||||
scrollable
|
||||
>
|
||||
<!-- Dialog Content -->
|
||||
<VCard :title="`编辑站点 - ${cardProps.site?.name}`">
|
||||
<VCardText class="pt-2">
|
||||
<DialogCloseBtn @click="siteInfoDialog = false" />
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VTextField
|
||||
v-model="siteForm.url"
|
||||
label="站点地址"
|
||||
:rules="[requiredValidator]"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="3"
|
||||
>
|
||||
<VSelect
|
||||
v-model="siteForm.pri"
|
||||
label="优先级"
|
||||
:items="priorityItems"
|
||||
:rules="[requiredValidator]"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="3"
|
||||
>
|
||||
<VSelect
|
||||
v-model="siteForm.is_active"
|
||||
:items="statusItems"
|
||||
label="状态"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
v-model="siteForm.rss"
|
||||
label="RSS地址"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VTextarea
|
||||
v-model="siteForm.cookie"
|
||||
label="站点Cookie"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
v-model="siteForm.ua"
|
||||
label="站点User-Agent"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<VTextField
|
||||
v-model="siteForm.limit_interval"
|
||||
label="单位周期(秒)"
|
||||
:rules="[numberValidator]"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<VTextField
|
||||
v-model="siteForm.limit_seconds"
|
||||
label="访问次数"
|
||||
:rules="[numberValidator]"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<VTextField
|
||||
v-model="siteForm.limit_seconds"
|
||||
label="访问间隔(秒)"
|
||||
:rules="[numberValidator]"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VSwitch
|
||||
v-model="siteForm.proxy"
|
||||
label="代理"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VSwitch
|
||||
v-model="siteForm.render"
|
||||
label="仿真"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
|
||||
<VCardActions>
|
||||
<VBtn color="error" @click="deleteSiteInfo">
|
||||
删除
|
||||
</VBtn>
|
||||
<VSpacer />
|
||||
<VBtn @click="updateSiteInfo">
|
||||
确定
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
<SiteAddEditForm
|
||||
v-model="siteEditDialog"
|
||||
:siteid="cardProps.site?.id"
|
||||
@save="siteEditDialog = false; emit('update')"
|
||||
@remove="emit('remove')"
|
||||
@close="siteEditDialog = false"
|
||||
/>
|
||||
<!-- 站点资源弹窗 -->
|
||||
<VDialog
|
||||
v-model="resourceDialog"
|
||||
@@ -654,7 +474,7 @@ onMounted(() => {
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-download" />
|
||||
</template>
|
||||
<VListItemTitle>下载种子</VListItemTitle>
|
||||
<VListItemTitle>下载种子文件</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script lang="ts" setup>
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import SubscribeEditForm from '../form/SubscribeEditForm.vue'
|
||||
import { calculateTimeDifference } from '@/@core/utils'
|
||||
import { formatSeason } from '@/@core/utils/formatters'
|
||||
import { numberValidator } from '@/@validators'
|
||||
import api from '@/api'
|
||||
import type { Site, Subscribe } from '@/api/types'
|
||||
import type { Subscribe } from '@/api/types'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
@@ -21,19 +21,7 @@ const $toast = useToast()
|
||||
const imageLoaded = ref(false)
|
||||
|
||||
// 订阅弹窗
|
||||
const subscribeInfoDialog = ref(false)
|
||||
|
||||
// 站点数据列表
|
||||
const siteList = ref<Site[]>([])
|
||||
|
||||
// 站点选择下载框
|
||||
const selectSitesOptions = ref<{ [key: number]: string }[]>([])
|
||||
|
||||
// 订阅编辑表单
|
||||
const subscribeForm = reactive<any>(props.media ?? {})
|
||||
|
||||
// 类型转换
|
||||
subscribeForm.best_version = subscribeForm.best_version === 1
|
||||
const subscribeEditDialog = ref(false)
|
||||
|
||||
// 上一次更新时间
|
||||
const lastUpdateText = ref(
|
||||
@@ -114,58 +102,9 @@ async function searchSubscribe() {
|
||||
}
|
||||
}
|
||||
|
||||
// 调用API修改订阅
|
||||
async function updateSubscribeInfo() {
|
||||
subscribeInfoDialog.value = false
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.put('subscribe/', subscribeForm)
|
||||
|
||||
// 提示
|
||||
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 loadSites() {
|
||||
try {
|
||||
const data: Site[] = await api.get('site/rss')
|
||||
|
||||
// 过滤站点,只有启用的站点才显示
|
||||
siteList.value = data.filter(item => item.is_active)
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取站点列表选择框数据
|
||||
async function getSiteList() {
|
||||
// 加载订阅站点列表
|
||||
if (!siteList.value.length)
|
||||
await loadSites()
|
||||
|
||||
const maps = siteList.value.map((item) => {
|
||||
return {
|
||||
title: item.name,
|
||||
value: item.id,
|
||||
}
|
||||
})
|
||||
|
||||
selectSitesOptions.value = maps.flat()
|
||||
}
|
||||
|
||||
// 编辑订阅响应
|
||||
async function editSubscribeDialog() {
|
||||
await getSiteList()
|
||||
subscribeInfoDialog.value = true
|
||||
subscribeEditDialog.value = true
|
||||
}
|
||||
|
||||
// 弹出菜单
|
||||
@@ -201,7 +140,7 @@ const dropdownItems = ref([
|
||||
<template>
|
||||
<VCard
|
||||
:key="props.media?.id"
|
||||
:class="`${subscribeForm.best_version ? 'outline-dashed outline-1' : ''}`"
|
||||
:class="`${props.media?.best_version ? 'outline-dashed outline-1' : ''}`"
|
||||
@click="editSubscribeDialog"
|
||||
>
|
||||
<template #image>
|
||||
@@ -323,100 +262,11 @@ const dropdownItems = ref([
|
||||
/>
|
||||
</VCard>
|
||||
<!-- 订阅编辑弹窗 -->
|
||||
<VDialog
|
||||
v-model="subscribeInfoDialog"
|
||||
max-width="50rem"
|
||||
persistent
|
||||
scrollable
|
||||
>
|
||||
<!-- Dialog Content -->
|
||||
<VCard :title="`订阅 - ${props.media?.name}`">
|
||||
<VCardText class="pt-2">
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VTextField
|
||||
v-model="subscribeForm.keyword"
|
||||
label="搜索关键词"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
v-if="props.media?.type === '电视剧'"
|
||||
cols="12"
|
||||
md="3"
|
||||
>
|
||||
<VTextField
|
||||
v-model="subscribeForm.total_episode"
|
||||
label="总集数"
|
||||
:rules="[numberValidator]"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
v-if="props.media?.type === '电视剧'"
|
||||
cols="12"
|
||||
md="3"
|
||||
>
|
||||
<VTextField
|
||||
v-model="subscribeForm.start_episode"
|
||||
label="开始集数"
|
||||
:rules="[numberValidator]"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VTextField
|
||||
v-model="subscribeForm.include"
|
||||
label="包含(关键字、正则式)"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VTextField
|
||||
v-model="subscribeForm.exclude"
|
||||
label="排除(关键字、正则式)"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VSelect
|
||||
v-model="subscribeForm.sites"
|
||||
:items="selectSitesOptions"
|
||||
chips
|
||||
label="订阅站点"
|
||||
multiple
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VSwitch
|
||||
v-model="subscribeForm.best_version"
|
||||
label="洗版"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
|
||||
<VCardActions>
|
||||
<VBtn @click="subscribeInfoDialog = false">
|
||||
取消
|
||||
</VBtn>
|
||||
<VSpacer />
|
||||
<VBtn @click="updateSubscribeInfo">
|
||||
确定
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
<SubscribeEditForm
|
||||
v-model="subscribeEditDialog"
|
||||
:subid="props.media?.id"
|
||||
@remove="() => { emit('remove');subscribeEditDialog = false; }"
|
||||
@save="() => { emit('save');subscribeEditDialog = false; }"
|
||||
@close="subscribeEditDialog = false"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -64,6 +64,9 @@ async function handleAddDownload(_site: any = undefined,
|
||||
dialogProps: {
|
||||
maxWidth: '50rem',
|
||||
},
|
||||
confirmationButtonProps: {
|
||||
variant: 'tonal',
|
||||
},
|
||||
})
|
||||
|
||||
if (!isConfirmed)
|
||||
@@ -176,7 +179,7 @@ onMounted(() => {
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-download" />
|
||||
</template>
|
||||
<VListItemTitle>下载种子</VListItemTitle>
|
||||
<VListItemTitle>下载种子文件</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
|
||||
251
src/components/cards/TorrentItem.vue
Normal file
251
src/components/cards/TorrentItem.vue
Normal file
@@ -0,0 +1,251 @@
|
||||
<script lang="ts" setup>
|
||||
import type { PropType } from 'vue'
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import { useConfirm } from 'vuetify-use-dialog'
|
||||
import { formatFileSize } from '@/@core/utils/formatters'
|
||||
import api from '@/api'
|
||||
import { doneNProgress, startNProgress } from '@/api/nprogress'
|
||||
import type { Context } from '@/api/types'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
torrent: Object as PropType<Context>,
|
||||
})
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
// 确认框
|
||||
const createConfirm = useConfirm()
|
||||
|
||||
// 更多来源界面
|
||||
const showMoreTorrents = ref(false)
|
||||
|
||||
// 种子信息
|
||||
const torrent = ref(props.torrent?.torrent_info)
|
||||
|
||||
// 媒体信息
|
||||
const media = ref(props.torrent?.media_info)
|
||||
|
||||
// 识别元数据
|
||||
const meta = ref(props.torrent?.meta_info)
|
||||
|
||||
// 站点图标
|
||||
const siteIcon = ref('')
|
||||
|
||||
// 查询站点图标
|
||||
async function getSiteIcon() {
|
||||
try {
|
||||
siteIcon.value = (await api.get(`site/icon/${torrent?.value?.site}`)).data.icon
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 询问并添加下载
|
||||
async function handleAddDownload(_site: any = undefined,
|
||||
_media: any = undefined,
|
||||
_torrent: any = undefined) {
|
||||
if (!_media || !_torrent || !_site) {
|
||||
_site = torrent.value?.site_name
|
||||
_media = media.value
|
||||
_torrent = torrent.value
|
||||
}
|
||||
|
||||
const isConfirmed = await createConfirm({
|
||||
title: '确认',
|
||||
content: `是否确认下载【${_site}】${_torrent?.title} ?`,
|
||||
confirmationText: '确认',
|
||||
cancellationText: '取消',
|
||||
dialogProps: {
|
||||
maxWidth: '50rem',
|
||||
},
|
||||
confirmationButtonProps: {
|
||||
variant: 'tonal',
|
||||
},
|
||||
})
|
||||
|
||||
if (!isConfirmed)
|
||||
return
|
||||
|
||||
addDownload(_media, _torrent)
|
||||
}
|
||||
|
||||
// 添加下载
|
||||
async function addDownload(_media: any, _torrent: any) {
|
||||
startNProgress()
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post('download/', {
|
||||
media_in: _media,
|
||||
torrent_in: _torrent,
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
// 添加下载成功
|
||||
$toast.success(`${_torrent?.site_name} ${_torrent?.title} 添加下载成功!`)
|
||||
}
|
||||
else {
|
||||
// 添加下载失败
|
||||
$toast.error(`${_torrent?.site_name} ${_torrent?.title} 添加下载失败!`)
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
doneNProgress()
|
||||
}
|
||||
|
||||
// 打开种子详情页面
|
||||
function openTorrentDetail() {
|
||||
window.open(torrent.value?.page_url, '_blank')
|
||||
}
|
||||
|
||||
// 下载种子文件
|
||||
async function downloadTorrentFile() {
|
||||
window.open(torrent.value?.enclosure, '_blank')
|
||||
}
|
||||
|
||||
// 促销Chip类
|
||||
function getVolumeFactorClass(downloadVolume: number, uploadVolume: number) {
|
||||
if (downloadVolume === 0)
|
||||
return 'text-white bg-lime-500'
|
||||
else if (downloadVolume < 1)
|
||||
return 'text-white bg-green-500'
|
||||
else if (uploadVolume !== 1)
|
||||
return 'text-white bg-sky-500'
|
||||
else
|
||||
return 'text-white bg-gray-500'
|
||||
}
|
||||
|
||||
// 装载时查询站点图标
|
||||
onMounted(() => {
|
||||
getSiteIcon()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VListItem @click="handleAddDownload">
|
||||
<template
|
||||
v-if="!showMoreTorrents"
|
||||
#prepend
|
||||
>
|
||||
<VAvatar
|
||||
class="rounded"
|
||||
variant="flat"
|
||||
@click.stop="openTorrentDetail"
|
||||
>
|
||||
<VImg :src="siteIcon" />
|
||||
</VAvatar>
|
||||
</template>
|
||||
<VListItemTitle class="break-words overflow-visible whitespace-break-spaces">
|
||||
{{ torrent?.title }}
|
||||
<span class="text-green-700 ms-2 text-sm">↑{{ torrent?.seeders }}</span>
|
||||
<span class="text-orange-700 ms-2 text-sm">↓{{ torrent?.peers }}</span>
|
||||
</VListItemTitle>
|
||||
<VListItemSubtitle>
|
||||
{{ torrent?.description }}
|
||||
</VListItemSubtitle>
|
||||
<div
|
||||
v-if="torrent?.labels"
|
||||
class="pt-2"
|
||||
>
|
||||
<VChip
|
||||
v-for="(label, index) in torrent?.labels"
|
||||
:key="index"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
color="primary"
|
||||
class="me-1 mb-1"
|
||||
>
|
||||
{{ label }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="meta?.edition"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
class="me-1 mb-1 text-white bg-red-500"
|
||||
>
|
||||
{{ meta?.edition }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="meta?.resource_pix"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
class="me-1 mb-1 text-white bg-red-500"
|
||||
>
|
||||
{{ meta?.resource_pix }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="meta?.video_encode"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
class="me-1 mb-1 text-white bg-orange-500"
|
||||
>
|
||||
{{ meta?.video_encode }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="torrent?.size"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
class="me-1 mb-1 text-white bg-yellow-500"
|
||||
>
|
||||
{{ formatFileSize(torrent?.size) }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="meta?.resource_team"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
class="me-1 mb-1 text-white bg-cyan-500"
|
||||
>
|
||||
{{ meta?.resource_team }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="torrent?.downloadvolumefactor !== 1 || torrent?.uploadvolumefactor !== 1"
|
||||
:class="
|
||||
getVolumeFactorClass(torrent?.downloadvolumefactor, torrent?.uploadvolumefactor)
|
||||
"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
class="me-1 mb-1"
|
||||
>
|
||||
{{ torrent?.volume_factor }}
|
||||
</VChip>
|
||||
</div>
|
||||
<template #append>
|
||||
<div class="me-n3">
|
||||
<IconBtn>
|
||||
<VIcon
|
||||
icon="mdi-dots-vertical"
|
||||
/>
|
||||
<VMenu
|
||||
activator="parent"
|
||||
close-on-content-click
|
||||
>
|
||||
<VList>
|
||||
<VListItem
|
||||
variant="plain"
|
||||
@click="openTorrentDetail()"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-information" />
|
||||
</template>
|
||||
<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>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</div>
|
||||
</template>
|
||||
</VListItem>
|
||||
</template>
|
||||
@@ -4,13 +4,12 @@ import type { PropType } from 'vue'
|
||||
import { useConfirm } from 'vuetify-use-dialog'
|
||||
import axios from 'axios'
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import { numberValidator } from '@/@validators'
|
||||
import ReorganizeForm from '../form/ReorganizeForm.vue'
|
||||
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({
|
||||
@@ -32,6 +31,15 @@ const $toast = useToast()
|
||||
// 是否正在加载
|
||||
const loading = ref(true)
|
||||
|
||||
// 识别进度条
|
||||
const progressDialog = ref(false)
|
||||
|
||||
// 识别进度文本
|
||||
const progressText = ref('请稍候 ...')
|
||||
|
||||
// 识别进度
|
||||
const progressValue = ref(0)
|
||||
|
||||
// 确认框
|
||||
const createConfirm = useConfirm()
|
||||
|
||||
@@ -53,57 +61,18 @@ const renamePopper = ref(false)
|
||||
// 整理弹窗
|
||||
const transferPopper = ref(false)
|
||||
|
||||
// 整理进度条
|
||||
const progressDialog = ref(false)
|
||||
|
||||
// 整理进度文本
|
||||
const progressText = ref('请稍候 ...')
|
||||
|
||||
// 整理进度
|
||||
const progressValue = ref(0)
|
||||
|
||||
// 加载进度SSE
|
||||
const progressEventSource = ref<EventSource>()
|
||||
|
||||
// 新名称
|
||||
const newName = ref('')
|
||||
|
||||
// 当前名称
|
||||
const currentItem = ref<FileItem>()
|
||||
|
||||
// 文件转移表单
|
||||
const transferForm = reactive({
|
||||
path: '',
|
||||
target: '',
|
||||
tmdbid: null,
|
||||
season: null,
|
||||
type_name: '',
|
||||
transfer_type: '',
|
||||
episode_format: '',
|
||||
episode_detail: '',
|
||||
episode_part: '',
|
||||
episode_offset: null,
|
||||
min_filesize: 0,
|
||||
|
||||
})
|
||||
|
||||
// 识别结果
|
||||
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 => ({
|
||||
title: `第 ${item} 季`,
|
||||
value: item,
|
||||
})),
|
||||
)
|
||||
|
||||
// 目录过滤
|
||||
const dirs = computed(() =>
|
||||
items.value.filter(item => item.type === 'dir' && item.basename.includes(filter.value)),
|
||||
@@ -158,6 +127,9 @@ async function deleteItem(item: FileItem) {
|
||||
dialogProps: {
|
||||
maxWidth: '50rem',
|
||||
},
|
||||
cancellationButtonProps: {
|
||||
variant: 'tonal',
|
||||
},
|
||||
})
|
||||
|
||||
if (confirmed) {
|
||||
@@ -245,41 +217,6 @@ function showTransfer(item: FileItem) {
|
||||
transferPopper.value = true
|
||||
}
|
||||
|
||||
// 整理文件
|
||||
async function transfer() {
|
||||
transferForm.path = currentItem.value?.path ?? ''
|
||||
// 开始整理文件
|
||||
try {
|
||||
// 关闭弹窗
|
||||
transferPopper.value = false
|
||||
// 显示进度条
|
||||
progressDialog.value = true
|
||||
// 开始监听进度
|
||||
startLoadingProgress()
|
||||
// 异步调API,结束后关闭进度条
|
||||
api.post('transfer/manual', {}, {
|
||||
params: transferForm,
|
||||
}).then((res: any) => {
|
||||
// 关闭进度条
|
||||
progressDialog.value = false
|
||||
// 停止监听进度
|
||||
stopLoadingProgress()
|
||||
// 显示结果
|
||||
if (res.success) {
|
||||
$toast.success(`${currentItem.value?.name} 整理完成!`)
|
||||
// 重新加载
|
||||
load()
|
||||
}
|
||||
else {
|
||||
$toast.error(`${currentItem.value?.name} 整理失败:${res.message}!`)
|
||||
}
|
||||
})
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
// 将文件修改时间(timestape)转换为本地时间
|
||||
function formatTime(timestape: number) {
|
||||
return new Date(timestape * 1000).toLocaleString()
|
||||
@@ -307,29 +244,6 @@ watch(
|
||||
},
|
||||
)
|
||||
|
||||
// 使用SSE监听加载进度
|
||||
function startLoadingProgress() {
|
||||
progressText.value = '请稍候 ...'
|
||||
|
||||
const token = store.state.auth.token
|
||||
|
||||
progressEventSource.value = new EventSource(
|
||||
`${import.meta.env.VITE_API_BASE_URL}system/progress/filetransfer?token=${token}`,
|
||||
)
|
||||
progressEventSource.value.onmessage = (event) => {
|
||||
const progress = JSON.parse(event.data)
|
||||
if (progress) {
|
||||
progressText.value = progress.text
|
||||
progressValue.value = progress.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 停止监听加载进度
|
||||
function stopLoadingProgress() {
|
||||
progressEventSource.value?.close()
|
||||
}
|
||||
|
||||
// 调用API识别
|
||||
async function recognize(path: string) {
|
||||
try {
|
||||
@@ -586,23 +500,19 @@ onMounted(() => {
|
||||
v-model="renamePopper"
|
||||
max-width="50rem"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<IconBtn title="重命名" v-bind="props">
|
||||
<VIcon icon="mdi-rename-outline" />
|
||||
</IconBtn>
|
||||
</template>
|
||||
<VCard title="重命名">
|
||||
<VCardText>
|
||||
<VTextField v-model="newName" label="名称" />
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<div class="flex-grow-1" />
|
||||
<VBtn depressed @click="renamePopper = false">
|
||||
取消
|
||||
</VBtn>
|
||||
<VSpacer />
|
||||
<VBtn
|
||||
:disabled="!newName"
|
||||
depressed
|
||||
variant="tonal"
|
||||
@click="rename"
|
||||
>
|
||||
重命名
|
||||
@@ -611,183 +521,44 @@ onMounted(() => {
|
||||
</VCard>
|
||||
</VDialog>
|
||||
<!-- 文件整理弹窗 -->
|
||||
<VDialog
|
||||
<ReorganizeForm
|
||||
v-model="transferPopper"
|
||||
max-width="50rem"
|
||||
scrollable
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<IconBtn title="整理" v-bind="props">
|
||||
<VIcon icon="mdi-folder-arrow-right-outline" />
|
||||
</IconBtn>
|
||||
</template>
|
||||
<VCard :title="`文件整理 - ${currentItem?.name}`">
|
||||
<DialogCloseBtn @click="transferPopper = false" />
|
||||
<VCardText class="pt-2">
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="8"
|
||||
>
|
||||
<VTextField
|
||||
v-model="transferForm.target"
|
||||
label="目的路径"
|
||||
placeholder="留空自动"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<VSelect
|
||||
v-model="transferForm.transfer_type"
|
||||
label="整理方式"
|
||||
:items="[
|
||||
{ title: '默认', value: '' },
|
||||
{ title: '移动', value: 'move' },
|
||||
{ title: '复制', value: 'copy' },
|
||||
{ title: '硬链接', value: 'link' },
|
||||
{ title: '软链接', value: 'softlink' },
|
||||
{ title: 'Rclone复制', value: 'rclone_copy' },
|
||||
{ title: 'Rclone移动', value: 'rclone_move' },
|
||||
]"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<VSelect
|
||||
v-model="transferForm.type_name"
|
||||
label="类型"
|
||||
:items="[{ title: '请选择', value: '' }, { title: '电影', value: '电影' }, { title: '电视剧', value: '电视剧' }]"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<VTextField
|
||||
v-model="transferForm.tmdbid"
|
||||
:disabled="transferForm.type_name === ''"
|
||||
label="TMDBID"
|
||||
placeholder="留空自动识别"
|
||||
:rules="[numberValidator]"
|
||||
append-inner-icon="mdi-magnify"
|
||||
@click:append-inner="tmdbSelectorDialog = true"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<VSelect
|
||||
v-show="transferForm.type_name === '电视剧'"
|
||||
v-model.number="transferForm.season"
|
||||
label="季"
|
||||
:items="seasonItems"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12" md="8">
|
||||
<VTextField
|
||||
v-model="transferForm.episode_format"
|
||||
label="集数定位"
|
||||
placeholder="使用{ep}定位集数"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="transferForm.episode_detail"
|
||||
label="指定集数"
|
||||
placeholder="起始集,终止集,如1或1,2"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="transferForm.episode_part"
|
||||
label="指定Part"
|
||||
placeholder="如part1"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model.number="transferForm.episode_offset"
|
||||
label="集数偏移"
|
||||
placeholder="如-10"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model.number="transferForm.min_filesize"
|
||||
label="最小文件大小(MB)"
|
||||
:rules="[numberValidator]"
|
||||
placeholder="0"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VBtn depressed @click="transferPopper = false">
|
||||
取消
|
||||
</VBtn>
|
||||
<VSpacer />
|
||||
<VBtn
|
||||
@click="transfer"
|
||||
>
|
||||
开始整理
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
:path="currentItem?.path"
|
||||
@done="transferPopper = false; load()"
|
||||
@close="transferPopper = false"
|
||||
/>
|
||||
<!-- 手动整理进度框 -->
|
||||
<vDialog
|
||||
<VDialog
|
||||
v-model="progressDialog"
|
||||
:scrim="false"
|
||||
width="25rem"
|
||||
>
|
||||
<vCard
|
||||
<VCard
|
||||
color="primary"
|
||||
>
|
||||
<vCardText class="text-center">
|
||||
<VCardText class="text-center">
|
||||
{{ progressText }}
|
||||
<vProgressLinear
|
||||
<VProgressLinear
|
||||
v-if="progressValue"
|
||||
color="white"
|
||||
class="mb-0 mt-1"
|
||||
:model-value="progressValue"
|
||||
/>
|
||||
</vCardText>
|
||||
</vCard>
|
||||
</vDialog>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
<!-- 识别结果对话框 -->
|
||||
<vDialog
|
||||
<VDialog
|
||||
v-model="nameTestDialog"
|
||||
width="50rem"
|
||||
>
|
||||
<vCard>
|
||||
<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>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -171,6 +171,7 @@ const sortIcon = computed(() => {
|
||||
<VBtn
|
||||
:disabled="!newFolderName"
|
||||
depressed
|
||||
variant="tonal"
|
||||
@click="mkdir"
|
||||
>
|
||||
新建
|
||||
|
||||
307
src/components/form/ReorganizeForm.vue
Normal file
307
src/components/form/ReorganizeForm.vue
Normal file
@@ -0,0 +1,307 @@
|
||||
<script lang="ts" setup>
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import TmdbSelectorCard from '../cards/TmdbSelectorCard.vue'
|
||||
import store from '@/store'
|
||||
import api from '@/api'
|
||||
import { numberValidator } from '@/@validators'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
path: String,
|
||||
target: String,
|
||||
logids: Array<number>,
|
||||
})
|
||||
|
||||
// 定义事件
|
||||
const emit = defineEmits(['done', 'close'])
|
||||
|
||||
// 生成1到50季的下拉框选项
|
||||
const seasonItems = ref(
|
||||
Array.from({ length: 51 }, (_, i) => i).map(item => ({
|
||||
title: `第 ${item} 季`,
|
||||
value: item,
|
||||
})),
|
||||
)
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
// TMDB选择对话框
|
||||
const tmdbSelectorDialog = ref(false)
|
||||
|
||||
// 加载进度SSE
|
||||
const progressEventSource = ref<EventSource>()
|
||||
|
||||
// 整理进度条
|
||||
const progressDialog = ref(false)
|
||||
|
||||
// 整理进度文本
|
||||
const progressText = ref('请稍候 ...')
|
||||
|
||||
// 整理进度
|
||||
const progressValue = ref(0)
|
||||
|
||||
// 文件转移表单
|
||||
const transferForm = reactive({
|
||||
logid: 0,
|
||||
path: '',
|
||||
target: props.target ?? '',
|
||||
tmdbid: null,
|
||||
season: null,
|
||||
type_name: '',
|
||||
transfer_type: '',
|
||||
episode_format: '',
|
||||
episode_detail: '',
|
||||
episode_part: '',
|
||||
episode_offset: null,
|
||||
min_filesize: 0,
|
||||
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
transferForm.path = props.path ?? ''
|
||||
transferForm.target = props.target ?? ''
|
||||
})
|
||||
|
||||
// 使用SSE监听加载进度
|
||||
function startLoadingProgress() {
|
||||
progressText.value = '请稍候 ...'
|
||||
|
||||
const token = store.state.auth.token
|
||||
|
||||
progressEventSource.value = new EventSource(
|
||||
`${import.meta.env.VITE_API_BASE_URL}system/progress/filetransfer?token=${token}`,
|
||||
)
|
||||
progressEventSource.value.onmessage = (event) => {
|
||||
const progress = JSON.parse(event.data)
|
||||
if (progress) {
|
||||
progressText.value = progress.text
|
||||
progressValue.value = progress.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 停止监听加载进度
|
||||
function stopLoadingProgress() {
|
||||
progressEventSource.value?.close()
|
||||
}
|
||||
|
||||
// 整理文件
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
async function transfer() {
|
||||
if (!props.logids && !props.path)
|
||||
return
|
||||
|
||||
// 显示进度条
|
||||
progressDialog.value = true
|
||||
// 开始监听进度
|
||||
startLoadingProgress()
|
||||
|
||||
if (props.path) {
|
||||
// 文件整理
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post('transfer/manual', {}, {
|
||||
params: transferForm,
|
||||
})
|
||||
// 显示结果
|
||||
if (result.success)
|
||||
$toast.success(`${props.path} 整理完成!`)
|
||||
|
||||
else
|
||||
$toast.error(`${props.path} 整理失败:${result.message}!`)
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
else if (props.logids) {
|
||||
// 日志整理
|
||||
for (const logid of props.logids) {
|
||||
transferForm.logid = logid
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post('transfer/manual', {}, {
|
||||
params: transferForm,
|
||||
})
|
||||
if (!result.success)
|
||||
$toast.error(`历史记录 ${logid} 重新整理失败:${result.message}!`)
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 停止监听进度
|
||||
stopLoadingProgress()
|
||||
// 关闭进度条
|
||||
progressDialog.value = false
|
||||
// 重新加载
|
||||
emit('done')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VDialog
|
||||
scrollable
|
||||
max-width="60rem"
|
||||
>
|
||||
<VCard
|
||||
:title="`${props.path ? `整理 - ${props.path}` : `整理 - 共 ${props.logids?.length} 条记录`}`"
|
||||
class="rounded-t"
|
||||
>
|
||||
<DialogCloseBtn @click="emit('close')" />
|
||||
<VCardText class="pt-2">
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="8"
|
||||
>
|
||||
<VTextField
|
||||
v-model="transferForm.target"
|
||||
label="目的路径"
|
||||
placeholder="留空自动"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<VSelect
|
||||
v-model="transferForm.transfer_type"
|
||||
label="整理方式"
|
||||
:items="[
|
||||
{ title: '默认', value: '' },
|
||||
{ title: '移动', value: 'move' },
|
||||
{ title: '复制', value: 'copy' },
|
||||
{ title: '硬链接', value: 'link' },
|
||||
{ title: '软链接', value: 'softlink' },
|
||||
{ title: 'Rclone复制', value: 'rclone_copy' },
|
||||
{ title: 'Rclone移动', value: 'rclone_move' },
|
||||
]"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<VSelect
|
||||
v-model="transferForm.type_name"
|
||||
label="类型"
|
||||
:items="[{ title: '自动', value: '' }, { title: '电影', value: '电影' }, { title: '电视剧', value: '电视剧' }]"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<VTextField
|
||||
v-model="transferForm.tmdbid"
|
||||
:disabled="transferForm.type_name === ''"
|
||||
label="TMDBID"
|
||||
placeholder="留空自动识别"
|
||||
:rules="[numberValidator]"
|
||||
append-inner-icon="mdi-magnify"
|
||||
@click:append-inner="tmdbSelectorDialog = true"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<VSelect
|
||||
v-show="transferForm.type_name === '电视剧'"
|
||||
v-model.number="transferForm.season"
|
||||
label="季"
|
||||
:items="seasonItems"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12" md="8">
|
||||
<VTextField
|
||||
v-model="transferForm.episode_format"
|
||||
label="集数定位"
|
||||
placeholder="使用{ep}定位集数"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="transferForm.episode_detail"
|
||||
label="指定集数"
|
||||
placeholder="起始集,终止集,如1或1,2"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="transferForm.episode_part"
|
||||
label="指定Part"
|
||||
placeholder="如part1"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model.number="transferForm.episode_offset"
|
||||
label="集数偏移"
|
||||
placeholder="如-10"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model.number="transferForm.min_filesize"
|
||||
label="最小文件大小(MB)"
|
||||
:rules="[numberValidator]"
|
||||
placeholder="0"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VBtn depressed @click="emit('close')">
|
||||
取消
|
||||
</VBtn>
|
||||
<VSpacer />
|
||||
<VBtn
|
||||
variant="tonal"
|
||||
@click="transfer"
|
||||
>
|
||||
开始整理
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
<!-- 手动整理进度框 -->
|
||||
<VDialog
|
||||
v-model="progressDialog"
|
||||
:scrim="false"
|
||||
width="25rem"
|
||||
>
|
||||
<VCard
|
||||
color="primary"
|
||||
>
|
||||
<VCardText class="text-center">
|
||||
{{ progressText }}
|
||||
<VProgressLinear
|
||||
v-if="progressValue"
|
||||
color="white"
|
||||
class="mb-0 mt-1"
|
||||
:model-value="progressValue"
|
||||
/>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
<!-- TMDB ID搜索框 -->
|
||||
<VDialog
|
||||
v-model="tmdbSelectorDialog"
|
||||
width="40rem"
|
||||
scrollable
|
||||
>
|
||||
<TmdbSelectorCard
|
||||
v-model="transferForm.tmdbid"
|
||||
@close="tmdbSelectorDialog = false"
|
||||
/>
|
||||
</VDialog>
|
||||
</VDialog>
|
||||
</template>
|
||||
274
src/components/form/SiteAddEditForm.vue
Normal file
274
src/components/form/SiteAddEditForm.vue
Normal file
@@ -0,0 +1,274 @@
|
||||
<script lang="ts" setup>
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import type { Site } from '@/api/types'
|
||||
import { doneNProgress, startNProgress } from '@/api/nprogress'
|
||||
import { numberValidator, requiredValidator } from '@/@validators'
|
||||
import api from '@/api'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
siteid: Number,
|
||||
oper: String,
|
||||
})
|
||||
|
||||
// 注册事件
|
||||
const emit = defineEmits(['save', 'remove', 'close'])
|
||||
|
||||
// 站点编辑表单数据
|
||||
const siteForm = ref<Site>({
|
||||
id: props.siteid ?? 0,
|
||||
url: '',
|
||||
rss: '',
|
||||
cookie: '',
|
||||
ua: '',
|
||||
pri: 0,
|
||||
is_active: true,
|
||||
limit_interval: 0,
|
||||
limit_seconds: 0,
|
||||
name: '',
|
||||
domain: '',
|
||||
})
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
// 状态下拉项
|
||||
const statusItems = [
|
||||
{ title: '启用', value: true },
|
||||
{ title: '停用', value: false },
|
||||
]
|
||||
|
||||
// 生成1到50的优先级下拉框选项
|
||||
const priorityItems = ref(
|
||||
Array.from({ length: 50 }, (_, i) => i + 1).map(item => ({
|
||||
title: item,
|
||||
value: item,
|
||||
})),
|
||||
)
|
||||
|
||||
// 监控输入参数
|
||||
watchEffect(async () => {
|
||||
if (props.siteid)
|
||||
fetchSiteInfo()
|
||||
})
|
||||
|
||||
// 查询站点信息
|
||||
async function fetchSiteInfo() {
|
||||
try {
|
||||
siteForm.value = await api.get(`site/${props.siteid}`)
|
||||
siteForm.value.proxy = siteForm.value.proxy === 1
|
||||
siteForm.value.render = siteForm.value.render === 1
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 调用API 新增站点
|
||||
async function addSite() {
|
||||
if (!siteForm.value?.url)
|
||||
return
|
||||
startNProgress()
|
||||
try {
|
||||
const result: { [key: string]: string } = await api.post('site/', siteForm.value)
|
||||
if (result.success) {
|
||||
$toast.success('新增站点成功')
|
||||
emit('save')
|
||||
}
|
||||
|
||||
else { $toast.error(`新增站点失败:${result.message}`) }
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
doneNProgress()
|
||||
}
|
||||
|
||||
// 调用API删除站点信息
|
||||
async function deleteSiteInfo() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.delete(`site/${siteForm.value?.id}`)
|
||||
if (result.success)
|
||||
emit('remove')
|
||||
|
||||
else $toast.error(`${siteForm.value?.name} 删除失败:${result.message}`)
|
||||
}
|
||||
catch (error) {
|
||||
$toast.error(`${siteForm.value?.name} 删除失败!`)
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 调用API更新站点信息
|
||||
async function updateSiteInfo() {
|
||||
startNProgress()
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.put('site/', siteForm.value)
|
||||
if (result.success) {
|
||||
$toast.success(`${siteForm.value?.name} 更新成功!`)
|
||||
emit('save')
|
||||
}
|
||||
else { $toast.error(`${siteForm.value?.name} 更新失败:${result.message}`) }
|
||||
}
|
||||
catch (error) {
|
||||
$toast.error(`${siteForm.value?.name} 更新失败!`)
|
||||
console.error(error)
|
||||
}
|
||||
doneNProgress()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VDialog
|
||||
scrollable
|
||||
max-width="60rem"
|
||||
>
|
||||
<VCard
|
||||
:title="`${props.oper === 'add' ? '新增' : '编辑'}站点${props.oper !== 'add' ? ` - ${siteForm.name}` : ''}`"
|
||||
class="rounded-t"
|
||||
>
|
||||
<DialogCloseBtn @click="emit('close')" />
|
||||
<VCardText class="pt-2">
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VTextField
|
||||
v-model="siteForm.url"
|
||||
label="站点地址"
|
||||
:rules="[requiredValidator]"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="3"
|
||||
>
|
||||
<VSelect
|
||||
v-model="siteForm.pri"
|
||||
label="优先级"
|
||||
:items="priorityItems"
|
||||
:rules="[requiredValidator]"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="3"
|
||||
>
|
||||
<VSelect
|
||||
v-model="siteForm.is_active"
|
||||
:items="statusItems"
|
||||
label="状态"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
v-model="siteForm.rss"
|
||||
label="RSS地址"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VTextarea
|
||||
v-model="siteForm.cookie"
|
||||
label="站点Cookie"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
v-model="siteForm.ua"
|
||||
label="站点User-Agent"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<VTextField
|
||||
v-model="siteForm.limit_interval"
|
||||
label="单位周期(秒)"
|
||||
:rules="[numberValidator]"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<VTextField
|
||||
v-model="siteForm.limit_seconds"
|
||||
label="访问次数"
|
||||
:rules="[numberValidator]"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<VTextField
|
||||
v-model="siteForm.limit_seconds"
|
||||
label="访问间隔(秒)"
|
||||
:rules="[numberValidator]"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VSwitch
|
||||
v-model="siteForm.proxy"
|
||||
label="代理"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VSwitch
|
||||
v-model="siteForm.render"
|
||||
label="仿真"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VBtn
|
||||
v-if="props.oper === 'add'"
|
||||
@click="emit('close')"
|
||||
>
|
||||
取消
|
||||
</VBtn>
|
||||
<VBtn
|
||||
v-else
|
||||
color="error"
|
||||
@click="deleteSiteInfo"
|
||||
>
|
||||
删除
|
||||
</VBtn>
|
||||
<VSpacer />
|
||||
<VBtn
|
||||
v-if="props.oper === 'add'"
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
@click="addSite"
|
||||
>
|
||||
新增
|
||||
</VBtn>
|
||||
<VBtn
|
||||
v-else
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
@click="updateSiteInfo"
|
||||
>
|
||||
保存
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
355
src/components/form/SubscribeEditForm.vue
Normal file
355
src/components/form/SubscribeEditForm.vue
Normal file
@@ -0,0 +1,355 @@
|
||||
<script lang="ts" setup>
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import { numberValidator } from '@/@validators'
|
||||
import api from '@/api'
|
||||
import type { Site, Subscribe } from '@/api/types'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
subid: Number,
|
||||
})
|
||||
|
||||
// 定义触发的自定义事件
|
||||
const emit = defineEmits(['remove', 'save', 'close'])
|
||||
|
||||
// 站点数据列表
|
||||
const siteList = ref<Site[]>([])
|
||||
|
||||
// 站点选择下载框
|
||||
const selectSitesOptions = ref<{ [key: number]: string }[]>([])
|
||||
|
||||
// 订阅编辑表单
|
||||
const subscribeForm = ref<Subscribe>({
|
||||
id: props.subid ?? 0,
|
||||
keyword: '',
|
||||
quality: '',
|
||||
resolution: '',
|
||||
effect: '',
|
||||
include: '',
|
||||
exclude: '',
|
||||
total_episode: 0,
|
||||
start_episode: 0,
|
||||
best_version: 0,
|
||||
sites: [],
|
||||
type: '',
|
||||
name: '',
|
||||
year: '',
|
||||
tmdbid: 0,
|
||||
state: '',
|
||||
last_update: '',
|
||||
username: '',
|
||||
current_priority: 0,
|
||||
})
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
// 调用API修改订阅
|
||||
async function updateSubscribeInfo() {
|
||||
try {
|
||||
subscribeForm.value.best_version = subscribeForm.value.best_version ? 1 : 0
|
||||
const result: { [key: string]: any } = await api.put('subscribe/', subscribeForm.value)
|
||||
// 提示
|
||||
if (result.success) {
|
||||
$toast.success(`${subscribeForm.value.name} 更新成功!`)
|
||||
// 通知父组件刷新
|
||||
emit('save')
|
||||
}
|
||||
else { $toast.error(`${subscribeForm.value.name} 更新失败:${result.message}!`) }
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取站点列表数据
|
||||
async function loadSites() {
|
||||
try {
|
||||
const data: Site[] = await api.get('site/rss')
|
||||
|
||||
// 过滤站点,只有启用的站点才显示
|
||||
siteList.value = data.filter(item => item.is_active)
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取站点列表选择框数据
|
||||
async function getSiteList() {
|
||||
// 加载订阅站点列表
|
||||
if (!siteList.value.length)
|
||||
await loadSites()
|
||||
|
||||
const maps = siteList.value.map((item) => {
|
||||
return {
|
||||
title: item.name,
|
||||
value: item.id,
|
||||
}
|
||||
})
|
||||
|
||||
selectSitesOptions.value = maps.flat()
|
||||
}
|
||||
|
||||
// 获取订阅信息
|
||||
async function getSubscribeInfo() {
|
||||
try {
|
||||
const result: Subscribe = await api.get(
|
||||
`subscribe/${props.subid}`,
|
||||
)
|
||||
subscribeForm.value = result
|
||||
subscribeForm.value.best_version = subscribeForm.value.best_version === 1
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
// 删除订阅
|
||||
async function removeSubscribe() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.delete(
|
||||
`subscribe/${props.subid}`,
|
||||
)
|
||||
|
||||
if (result.success) {
|
||||
// 通知父组件刷新
|
||||
emit('remove')
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
watchEffect(() => {
|
||||
if (props.subid)
|
||||
getSubscribeInfo()
|
||||
})
|
||||
|
||||
// 质量选择框数据
|
||||
const qualityOptions = ref([
|
||||
{
|
||||
title: '全部',
|
||||
value: '',
|
||||
},
|
||||
{
|
||||
title: '蓝光原盘',
|
||||
value: 'Blu-?Ray.+VC-?1|Blu-?Ray.+AVC|UHD.+blu-?ray.+HEVC|MiniBD',
|
||||
},
|
||||
{
|
||||
title: 'Remux',
|
||||
value: 'Remux',
|
||||
},
|
||||
{
|
||||
title: 'BluRay',
|
||||
value: 'Blu-?Ray',
|
||||
},
|
||||
{
|
||||
title: 'UHD',
|
||||
value: 'UHD|UltraHD',
|
||||
},
|
||||
{
|
||||
title: 'WEB-DL',
|
||||
value: 'WEB-?DL|WEB-?RIP',
|
||||
},
|
||||
{
|
||||
title: 'HDTV',
|
||||
value: 'HDTV',
|
||||
},
|
||||
{
|
||||
title: 'H265',
|
||||
value: '[Hx].?265|HEVC',
|
||||
},
|
||||
{
|
||||
title: 'H264',
|
||||
value: '[Hx].?264|AVC',
|
||||
},
|
||||
])
|
||||
|
||||
// 分辨率选择框数据
|
||||
const resolutionOptions = ref([
|
||||
{
|
||||
title: '全部',
|
||||
value: '',
|
||||
},
|
||||
{
|
||||
title: '4k',
|
||||
value: '4K|2160p|x2160',
|
||||
},
|
||||
{
|
||||
title: '1080p',
|
||||
value: '1080[pi]|x1080',
|
||||
},
|
||||
{
|
||||
title: '720p',
|
||||
value: '720[pi]|x720',
|
||||
},
|
||||
])
|
||||
|
||||
// 特效选择框数据
|
||||
const effectOptions = ref([
|
||||
{
|
||||
title: '全部',
|
||||
value: '',
|
||||
},
|
||||
{
|
||||
title: '杜比视界',
|
||||
value: 'Dolby[\\s.]+Vision|DOVI|[\\s.]+DV[\\s.]+',
|
||||
},
|
||||
{
|
||||
title: '杜比全景声',
|
||||
value: 'Dolby[\\s.]*\\+?Atmos|Atmos',
|
||||
},
|
||||
{
|
||||
title: 'HDR',
|
||||
value: '[\\s.]+HDR[\\s.]+|HDR10|HDR10\\+',
|
||||
},
|
||||
{
|
||||
title: 'SDR',
|
||||
value: '[\\s.]+SDR[\\s.]+',
|
||||
},
|
||||
])
|
||||
|
||||
// 初始化
|
||||
onMounted(async () => {
|
||||
// 加载订阅站点列表
|
||||
getSiteList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VDialog
|
||||
scrollable
|
||||
max-width="60rem"
|
||||
>
|
||||
<VCard
|
||||
:title="`编辑订阅 - ${subscribeForm.name} ${subscribeForm.season ? `第 ${subscribeForm.season} 季` : ''}`"
|
||||
class="rounded-t"
|
||||
>
|
||||
<VCardText class="pt-2">
|
||||
<DialogCloseBtn @click="emit('close')" />
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="8"
|
||||
>
|
||||
<VTextField
|
||||
v-model="subscribeForm.keyword"
|
||||
label="搜索关键词"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
v-if="subscribeForm.type === '电视剧'"
|
||||
cols="12"
|
||||
md="2"
|
||||
>
|
||||
<VTextField
|
||||
v-model="subscribeForm.total_episode"
|
||||
label="总集数"
|
||||
:rules="[numberValidator]"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
v-if="subscribeForm.type === '电视剧'"
|
||||
cols="12"
|
||||
md="2"
|
||||
>
|
||||
<VTextField
|
||||
v-model="subscribeForm.start_episode"
|
||||
label="开始集数"
|
||||
:rules="[numberValidator]"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<VSelect
|
||||
v-model="subscribeForm.quality"
|
||||
label="质量"
|
||||
:items="qualityOptions"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<VSelect
|
||||
v-model="subscribeForm.resolution"
|
||||
label="分辨率"
|
||||
:items="resolutionOptions"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<VSelect
|
||||
v-model="subscribeForm.effect"
|
||||
label="特效"
|
||||
:items="effectOptions"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<VTextField
|
||||
v-model="subscribeForm.include"
|
||||
label="包含(关键字、正则式)"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<VTextField
|
||||
v-model="subscribeForm.exclude"
|
||||
label="排除(关键字、正则式)"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<VSelect
|
||||
v-model="subscribeForm.sites"
|
||||
:items="selectSitesOptions"
|
||||
chips
|
||||
label="订阅站点"
|
||||
multiple
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VSwitch
|
||||
v-model="subscribeForm.best_version"
|
||||
label="洗版"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
|
||||
<VCardActions>
|
||||
<VBtn color="error" @click="removeSubscribe">
|
||||
取消订阅
|
||||
</VBtn>
|
||||
<VSpacer />
|
||||
<VBtn
|
||||
variant="tonal"
|
||||
@click="updateSubscribeInfo"
|
||||
>
|
||||
保存
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
@@ -91,7 +91,6 @@ const superUser = store.state.auth.superUser
|
||||
}"
|
||||
/>
|
||||
<VerticalNavLink
|
||||
v-if="superUser"
|
||||
:item="{
|
||||
title: '电影',
|
||||
icon: 'mdi-movie-check-outline',
|
||||
@@ -99,7 +98,6 @@ const superUser = store.state.auth.superUser
|
||||
}"
|
||||
/>
|
||||
<VerticalNavLink
|
||||
v-if="superUser"
|
||||
:item="{
|
||||
title: '电视剧',
|
||||
icon: 'mdi-television-classic',
|
||||
|
||||
@@ -8,6 +8,9 @@ const searchWord = ref<string>('')
|
||||
// 搜索弹窗
|
||||
const searchDialog = ref(false)
|
||||
|
||||
// ref
|
||||
const searchWordInput = ref<HTMLElement | null>(null)
|
||||
|
||||
// Search
|
||||
function search() {
|
||||
if (!searchWord.value)
|
||||
@@ -21,6 +24,14 @@ function search() {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 打开搜索弹窗
|
||||
function openSearchDialog() {
|
||||
searchDialog.value = true
|
||||
nextTick(() => {
|
||||
searchWordInput.value?.focus()
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -34,23 +45,16 @@ function search() {
|
||||
max-width="50rem"
|
||||
transition="dialog-top-transition"
|
||||
>
|
||||
<!-- Dialog Activator -->
|
||||
<template #activator="{ props }">
|
||||
<IconBtn
|
||||
class="d-lg-none"
|
||||
v-bind="props"
|
||||
>
|
||||
<VIcon icon="mdi-magnify" />
|
||||
</IconBtn>
|
||||
</template>
|
||||
<!-- Dialog Content -->
|
||||
<VCard title="搜索">
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
ref="searchWordInput"
|
||||
v-model="searchWord"
|
||||
label="电影、电视剧名称"
|
||||
@keydown.enter="search"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -59,8 +63,8 @@ function search() {
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
<VBtn
|
||||
variant="tonal"
|
||||
@click="search"
|
||||
@keydown.enter="search"
|
||||
>
|
||||
搜索
|
||||
</VBtn>
|
||||
@@ -68,7 +72,13 @@ function search() {
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</div>
|
||||
|
||||
<!-- 👉 Search Icon -->
|
||||
<IconBtn
|
||||
class="d-lg-none"
|
||||
@click="openSearchDialog"
|
||||
>
|
||||
<VIcon icon="mdi-magnify" />
|
||||
</IconBtn>
|
||||
<!-- 👉 Search Textfield -->
|
||||
<span class="w-1/5">
|
||||
<VTextField
|
||||
|
||||
@@ -38,6 +38,9 @@ async function restart() {
|
||||
dialogProps: {
|
||||
maxWidth: '30rem',
|
||||
},
|
||||
cancellationButtonProps: {
|
||||
variant: 'tonal',
|
||||
},
|
||||
})
|
||||
|
||||
if (confirmed) {
|
||||
@@ -121,6 +124,25 @@ const avatar = store.state.auth.avatar
|
||||
<VListItemTitle>设定</VListItemTitle>
|
||||
</VListItem>
|
||||
|
||||
<!-- Divider -->
|
||||
<VDivider class="my-2" />
|
||||
|
||||
<!-- 👉 restart -->
|
||||
<VListItem
|
||||
v-if="superUser"
|
||||
@click="restart"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon
|
||||
class="me-2"
|
||||
icon="mdi-restart"
|
||||
size="22"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<VListItemTitle>重启</VListItemTitle>
|
||||
</VListItem>
|
||||
|
||||
<!-- 👉 FAQ -->
|
||||
<VListItem
|
||||
href="https://github.com/jxxghp/MoviePilot/blob/main/README.md"
|
||||
@@ -137,22 +159,6 @@ const avatar = store.state.auth.avatar
|
||||
<VListItemTitle>帮助</VListItemTitle>
|
||||
</VListItem>
|
||||
|
||||
<!-- 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>
|
||||
@@ -170,21 +176,21 @@ const avatar = store.state.auth.avatar
|
||||
<!-- !SECTION -->
|
||||
</VAvatar>
|
||||
<!-- 重启进度框 -->
|
||||
<vDialog
|
||||
<VDialog
|
||||
v-model="progressDialog"
|
||||
width="25rem"
|
||||
>
|
||||
<vCard
|
||||
<VCard
|
||||
color="primary"
|
||||
>
|
||||
<vCardText class="text-center">
|
||||
<VCardText class="text-center">
|
||||
正在重启 ...
|
||||
<vProgressLinear
|
||||
<VProgressLinear
|
||||
indeterminate
|
||||
color="white"
|
||||
class="mb-0 mt-1"
|
||||
/>
|
||||
</vCardText>
|
||||
</vCard>
|
||||
</vDialog>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
@@ -28,6 +28,12 @@ import MediaCardSlideView from '@/views/discover/MediaCardSlideView.vue'
|
||||
title="热门电视剧"
|
||||
/>
|
||||
|
||||
<MediaCardSlideView
|
||||
apipath="douban/tv_animation"
|
||||
linkurl="/browse/douban/tv_animation?title=热门动漫"
|
||||
title="热门动漫"
|
||||
/>
|
||||
|
||||
<MediaCardSlideView
|
||||
apipath="douban/movies"
|
||||
linkurl="/browse/douban/movies?title=最新电影"
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import api from '@/api'
|
||||
import type { Context } from '@/api/types'
|
||||
import store from '@/store'
|
||||
import TorrentCardListView from '@/views/discover/TorrentCardListView.vue'
|
||||
import TorrentRowListView from '@/views/discover/TorrentRowListView.vue'
|
||||
|
||||
// 路由参数
|
||||
const route = useRoute()
|
||||
@@ -12,14 +16,130 @@ const type = route.query?.type?.toString() ?? ''
|
||||
|
||||
// 搜索字段
|
||||
const area = route.query?.area?.toString() ?? ''
|
||||
|
||||
// 视图类型,从localStorage中读取
|
||||
const viewType = ref<string>(localStorage.getItem('MPTorrentsViewType') ?? 'card')
|
||||
|
||||
// 数据列表
|
||||
const dataList = ref<Array<Context>>([])
|
||||
|
||||
// 是否刷新过
|
||||
const isRefreshed = ref(false)
|
||||
|
||||
// 加载进度文本
|
||||
const progressText = ref('')
|
||||
|
||||
// 加载进度
|
||||
const progressValue = ref(0)
|
||||
|
||||
// 加载进度SSE
|
||||
const progressEventSource = ref<EventSource>()
|
||||
|
||||
// 使用SSE监听加载进度
|
||||
function startLoadingProgress() {
|
||||
progressText.value = '正在搜索,请稍候...'
|
||||
|
||||
const token = store.state.auth.token
|
||||
|
||||
progressEventSource.value = new EventSource(
|
||||
`${import.meta.env.VITE_API_BASE_URL}system/progress/search?token=${token}`,
|
||||
)
|
||||
progressEventSource.value.onmessage = (event) => {
|
||||
const progress = JSON.parse(event.data)
|
||||
if (progress) {
|
||||
progressText.value = progress.text
|
||||
progressValue.value = progress.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 停止监听加载进度
|
||||
function stopLoadingProgress() {
|
||||
progressEventSource.value?.close()
|
||||
}
|
||||
|
||||
// 设置视图类型
|
||||
function setViewType(type: string) {
|
||||
localStorage.setItem('MPTorrentsViewType', type)
|
||||
viewType.value = type
|
||||
}
|
||||
|
||||
// 获取搜索列表数据
|
||||
async function fetchData() {
|
||||
try {
|
||||
if (!keyword) {
|
||||
// 查询上次搜索结果
|
||||
dataList.value = await api.get('search/last')
|
||||
}
|
||||
else {
|
||||
startLoadingProgress()
|
||||
// 优先按TMDBID精确查询
|
||||
if (keyword?.startsWith('tmdb:') || keyword?.startsWith('douban:')) {
|
||||
dataList.value = await api.get(`search/media/${keyword}`, {
|
||||
params: {
|
||||
mtype: type,
|
||||
area,
|
||||
},
|
||||
})
|
||||
}
|
||||
else {
|
||||
// 按标题模糊查询
|
||||
dataList.value = await api.get(`search/title/${keyword}`)
|
||||
}
|
||||
stopLoadingProgress()
|
||||
}
|
||||
// 标记已刷新
|
||||
isRefreshed.value = true
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
return Promise.reject(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="!isRefreshed" class="mt-12 w-full text-center text-gray-500 text-sm flex flex-col items-center">
|
||||
<VProgressCircular v-if="!keyword" size="48" indeterminate color="primary" />
|
||||
<VProgressCircular v-if="keyword" class="mb-3" color="primary" :model-value="progressValue" size="64" />
|
||||
<span>{{ progressText }}</span>
|
||||
</div>
|
||||
<NoDataFound
|
||||
v-if="dataList.length === 0 && isRefreshed"
|
||||
error-code="404"
|
||||
error-title="没有资源"
|
||||
error-description="没有搜索到符合条件的资源。"
|
||||
/>
|
||||
<div v-if="dataList.length > 0">
|
||||
<TorrentRowListView
|
||||
v-if="viewType === 'list'"
|
||||
:items="dataList"
|
||||
/>
|
||||
<TorrentCardListView
|
||||
:keyword="keyword"
|
||||
:type="type"
|
||||
:area="area"
|
||||
v-else
|
||||
:items="dataList"
|
||||
/>
|
||||
</div>
|
||||
<!-- 视图切换 -->
|
||||
<span v-if="dataList.length > 0" class="fixed right-5 bottom-5">
|
||||
<VBtn
|
||||
v-if="viewType === 'list'"
|
||||
size="x-large"
|
||||
icon="mdi-view-grid"
|
||||
color="primary"
|
||||
@click="setViewType('card')"
|
||||
/>
|
||||
<VBtn
|
||||
v-else
|
||||
size="x-large"
|
||||
icon="mdi-view-list"
|
||||
color="primary"
|
||||
@click="setViewType('list')"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import { useRoute } from 'vue-router'
|
||||
import AccountSettingAccount from '@/views/setting/AccountSettingAccount.vue'
|
||||
import AccountSettingNotification from '@/views/setting/AccountSettingNotification.vue'
|
||||
import AccountSettingRule from '@/views/setting/AccountSettingRule.vue'
|
||||
import AccountSettingSite from '@/views/setting/AccountSettingSite.vue'
|
||||
import AccountSettingWords from '@/views/setting/AccountSettingWords.vue'
|
||||
import AccountSettingAbout from '@/views/setting/AccountSettingAbout.vue'
|
||||
@@ -41,11 +40,6 @@ const tabs = [
|
||||
icon: 'mdi-list-box',
|
||||
tab: 'service',
|
||||
},
|
||||
{
|
||||
title: '规则',
|
||||
icon: 'mdi-filter-cog',
|
||||
tab: 'filter',
|
||||
},
|
||||
{
|
||||
title: '通知',
|
||||
icon: 'mdi-bell',
|
||||
@@ -117,13 +111,6 @@ const tabs = [
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
|
||||
<!-- 规则 -->
|
||||
<VWindowItem value="filter">
|
||||
<transition name="fade-slide" appear>
|
||||
<AccountSettingRule />
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
|
||||
<!-- 通知 -->
|
||||
<VWindowItem value="notification">
|
||||
<transition name="fade-slide" appear>
|
||||
|
||||
@@ -103,6 +103,7 @@ const router = createRouter({
|
||||
component: () => import('../pages/browse.vue'),
|
||||
props: true,
|
||||
meta: {
|
||||
keepAlive: true,
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
@@ -111,6 +112,7 @@ const router = createRouter({
|
||||
component: () => import('../pages/credits.vue'),
|
||||
props: true,
|
||||
meta: {
|
||||
keepAlive: true,
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -8,6 +8,7 @@ import NoDataFound from '@/components/NoDataFound.vue'
|
||||
import { doneNProgress, startNProgress } from '@/api/nprogress'
|
||||
import { formatSeason } from '@/@core/utils/formatters'
|
||||
import router from '@/router'
|
||||
import SubscribeEditForm from '@/components/form/SubscribeEditForm.vue'
|
||||
|
||||
// 输入参数
|
||||
const mediaProps = defineProps({
|
||||
@@ -21,6 +22,9 @@ const $toast = useToast()
|
||||
// 媒体详情
|
||||
const mediaDetail = ref<MediaInfo>({} as MediaInfo)
|
||||
|
||||
// 订阅编辑弹窗
|
||||
const subscribeEditDialog = ref(false)
|
||||
|
||||
// 本地是否存在
|
||||
const isExists = ref(false)
|
||||
|
||||
@@ -39,6 +43,9 @@ const seasonsNotExisted = ref<{ [key: number]: number }>({})
|
||||
// 各季的订阅状态
|
||||
const seasonsSubscribed = ref<{ [key: number]: boolean }>({})
|
||||
|
||||
// 订阅编号
|
||||
const subscribeId = ref(0)
|
||||
|
||||
// 调用API查询详情
|
||||
async function getMediaDetail() {
|
||||
if (mediaProps.mediaid && mediaProps.type) {
|
||||
@@ -211,6 +218,12 @@ async function addSubscribe(season = 0) {
|
||||
result.message,
|
||||
best_version,
|
||||
)
|
||||
|
||||
// 显示编辑弹窗
|
||||
if (result.success) {
|
||||
subscribeId.value = result.data.id
|
||||
subscribeEditDialog.value = true
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
@@ -231,9 +244,7 @@ function showSubscribeAddToast(result: boolean,
|
||||
if (best_version > 0)
|
||||
subname = '洗版订阅'
|
||||
|
||||
if (result)
|
||||
$toast.success(`${title} 添加${subname}成功!`)
|
||||
else
|
||||
if (!result)
|
||||
$toast.error(`${title} 添加${subname}失败:${message}!`)
|
||||
}
|
||||
|
||||
@@ -684,6 +695,20 @@ onBeforeMount(() => {
|
||||
error-title="出错啦!"
|
||||
error-description="未识别到TMDB媒体信息。"
|
||||
/>
|
||||
<!-- 订阅编辑弹窗 -->
|
||||
<SubscribeEditForm
|
||||
v-model="subscribeEditDialog"
|
||||
:subid="subscribeId"
|
||||
@close="subscribeEditDialog = false"
|
||||
@save="subscribeEditDialog = false"
|
||||
@remove="() => {
|
||||
subscribeEditDialog = false;
|
||||
if (mediaDetail.type === '电影')
|
||||
checkMovieSubscribed()
|
||||
else
|
||||
checkSeasonsSubscribed();
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -1,66 +1,33 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import _ from 'lodash'
|
||||
import api from '@/api'
|
||||
import type { Context } from '@/api/types'
|
||||
import TorrentCard from '@/components/cards/TorrentCard.vue'
|
||||
import NoDataFound from '@/components/NoDataFound.vue'
|
||||
import store from '@/store'
|
||||
|
||||
// 定义输入参数
|
||||
const props = defineProps({
|
||||
// 关键字或TMDBID
|
||||
keyword: String,
|
||||
|
||||
// 类型
|
||||
type: String,
|
||||
|
||||
// 搜索字段
|
||||
area: String,
|
||||
})
|
||||
|
||||
interface SearchTorrent extends Context {
|
||||
more?: Array<Context>
|
||||
}
|
||||
|
||||
// 数据列表
|
||||
const dataList = ref <Array<SearchTorrent>>([])
|
||||
|
||||
// 分组后的数据列表
|
||||
const groupedDataList = ref<Map<string, Context[]>>()
|
||||
|
||||
// 是否刷新过
|
||||
const isRefreshed = ref(false)
|
||||
|
||||
// 加载进度文本
|
||||
const progressText = ref('')
|
||||
|
||||
// 加载进度
|
||||
const progressValue = ref(0)
|
||||
|
||||
// 加载进度SSE
|
||||
const progressEventSource = ref<EventSource>()
|
||||
// 定义输入参数
|
||||
const props = defineProps({
|
||||
// 数据列表
|
||||
items: Array as PropType<SearchTorrent[]>,
|
||||
})
|
||||
|
||||
// 过滤表单
|
||||
const filterForm = reactive({
|
||||
// 站点
|
||||
site: [] as string[],
|
||||
|
||||
// 季
|
||||
season: [] as string[],
|
||||
|
||||
// 制作组
|
||||
releaseGroup: [] as string[],
|
||||
|
||||
// 视频编码
|
||||
videoCode: [] as string[],
|
||||
|
||||
// 促销状态
|
||||
freeState: [] as string[],
|
||||
|
||||
// 质量
|
||||
edition: [] as string[],
|
||||
|
||||
// 分辨率
|
||||
resolution: [] as string[],
|
||||
})
|
||||
@@ -80,110 +47,13 @@ const editionFilterOptions = ref<Array<string>>([])
|
||||
// 获取分辨率过滤选项
|
||||
const resolutionFilterOptions = ref<Array<string>>([])
|
||||
|
||||
// 按过滤项过滤卡片
|
||||
watchEffect(() => {
|
||||
// 清空数据
|
||||
dataList.value.splice(0)
|
||||
// 数据列表
|
||||
const dataList = ref <Array<SearchTorrent>>([])
|
||||
|
||||
const match = (filter: Array<string>, value: string | undefined) =>
|
||||
filter.length === 0 || (value && filter.includes(value))
|
||||
|
||||
groupedDataList.value?.forEach((value) => {
|
||||
if (value.length > 0) {
|
||||
const matchData = value.filter((data) => {
|
||||
const { meta_info, torrent_info } = data
|
||||
// 季、制作组、视频编码
|
||||
const { season_episode, resource_team, video_encode } = meta_info
|
||||
return (
|
||||
// 站点过滤
|
||||
match(filterForm.site, torrent_info.site_name)
|
||||
// 促销状态过滤
|
||||
&& match(filterForm.freeState, torrent_info.volume_factor)
|
||||
// 季过滤
|
||||
&& match(filterForm.season, season_episode)
|
||||
// 制作组过滤
|
||||
&& match(filterForm.releaseGroup, resource_team)
|
||||
// 视频编码过滤
|
||||
&& match(filterForm.videoCode, video_encode)
|
||||
// 分辨率过滤
|
||||
&& match(filterForm.resolution, meta_info.resource_pix)
|
||||
// 质量过滤
|
||||
&& match(filterForm.edition, meta_info.edition)
|
||||
)
|
||||
})
|
||||
if (matchData.length > 0) {
|
||||
const firstData = _.cloneDeepWith(matchData[0]) as SearchTorrent
|
||||
if (matchData.length > 1)
|
||||
firstData.more = matchData.slice(1)
|
||||
|
||||
dataList.value.push(firstData)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 获取订阅列表数据
|
||||
async function fetchData(): Promise<Array<Context>> {
|
||||
try {
|
||||
let searchData: Array<Context>
|
||||
const keyword = props.keyword ?? ''
|
||||
const mtype = props.type ?? ''
|
||||
const area = props.area ?? ''
|
||||
if (!keyword) {
|
||||
// 查询上次搜索结果
|
||||
searchData = await api.get('search/last')
|
||||
}
|
||||
else {
|
||||
startLoadingProgress()
|
||||
// 优先按TMDBID精确查询
|
||||
if (props.keyword?.startsWith('tmdb:') || props.keyword?.startsWith('douban:')) {
|
||||
searchData = await api.get(`search/media/${props.keyword}`, {
|
||||
params: {
|
||||
mtype,
|
||||
area,
|
||||
},
|
||||
})
|
||||
}
|
||||
else {
|
||||
// 按标题模糊查询
|
||||
searchData = await api.get(`search/title/${props.keyword}`)
|
||||
}
|
||||
stopLoadingProgress()
|
||||
}
|
||||
isRefreshed.value = true
|
||||
return Promise.resolve(searchData)
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
return Promise.reject(error)
|
||||
}
|
||||
}
|
||||
|
||||
function initData() {
|
||||
// load data
|
||||
fetchData().then((data) => {
|
||||
const groupMap = new Map<string, Context[]>()
|
||||
|
||||
data.forEach((item) => {
|
||||
const { torrent_info } = item
|
||||
// init options
|
||||
initOptions(item)
|
||||
// group data
|
||||
const key = `${torrent_info.title}_${torrent_info.size}`
|
||||
if (groupMap.has(key)) {
|
||||
// 已存在相同标题和大小的分组,将当前上下文信息添加到分组中
|
||||
const group = groupMap.get(key)
|
||||
group?.push(item)
|
||||
}
|
||||
else {
|
||||
// 创建新的分组,并将当前上下文信息添加到分组中
|
||||
groupMap.set(key, [item])
|
||||
}
|
||||
})
|
||||
groupedDataList.value = groupMap
|
||||
})
|
||||
}
|
||||
// 分组后的数据列表
|
||||
const groupedDataList = ref<Map<string, Context[]>>()
|
||||
|
||||
// 初始化过滤选项
|
||||
function initOptions(data: Context) {
|
||||
const { torrent_info, meta_info } = data
|
||||
const optionValue = (options: Array<string>, value: string | undefined) => {
|
||||
@@ -198,31 +68,69 @@ function initOptions(data: Context) {
|
||||
optionValue(resolutionFilterOptions.value, meta_info?.resource_pix)
|
||||
}
|
||||
|
||||
// 使用SSE监听加载进度
|
||||
function startLoadingProgress() {
|
||||
progressText.value = '正在搜索,请稍候...'
|
||||
|
||||
const token = store.state.auth.token
|
||||
|
||||
progressEventSource.value = new EventSource(
|
||||
`${import.meta.env.VITE_API_BASE_URL}system/progress/search?token=${token}`,
|
||||
)
|
||||
progressEventSource.value.onmessage = (event) => {
|
||||
const progress = JSON.parse(event.data)
|
||||
if (progress) {
|
||||
progressText.value = progress.text
|
||||
progressValue.value = progress.value
|
||||
// 计算分组后的列表
|
||||
watchEffect(() => {
|
||||
// 数据分组
|
||||
const groupMap = new Map<string, Context[]>()
|
||||
// 遍历数据
|
||||
props.items?.forEach((item) => {
|
||||
const { torrent_info } = item
|
||||
// init options
|
||||
initOptions(item)
|
||||
// group data
|
||||
const key = `${torrent_info.title}_${torrent_info.size}`
|
||||
if (groupMap.has(key)) {
|
||||
// 已存在相同标题和大小的分组,将当前上下文信息添加到分组中
|
||||
const group = groupMap.get(key)
|
||||
group?.push(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
// 创建新的分组,并将当前上下文信息添加到分组中
|
||||
groupMap.set(key, [item])
|
||||
}
|
||||
})
|
||||
groupedDataList.value = groupMap
|
||||
})
|
||||
|
||||
// 停止监听加载进度
|
||||
function stopLoadingProgress() {
|
||||
progressEventSource.value?.close()
|
||||
}
|
||||
// 计算过滤后的列表
|
||||
watchEffect(() => {
|
||||
// 清空列表
|
||||
dataList.value.splice(0)
|
||||
// 匹配过滤函数
|
||||
const match = (filter: Array<string>, value: string | undefined) =>
|
||||
filter.length === 0 || (value && filter.includes(value))
|
||||
|
||||
// 加载时获取数据
|
||||
onMounted(initData)
|
||||
groupedDataList.value?.forEach((value) => {
|
||||
if (value.length > 0) {
|
||||
const matchData = value.filter((data) => {
|
||||
const { meta_info, torrent_info } = data
|
||||
// 季、制作组、视频编码
|
||||
return (
|
||||
// 站点过滤
|
||||
match(filterForm.site, torrent_info.site_name)
|
||||
// 促销状态过滤
|
||||
&& match(filterForm.freeState, torrent_info.volume_factor)
|
||||
// 季过滤
|
||||
&& match(filterForm.season, meta_info.season_episode)
|
||||
// 制作组过滤
|
||||
&& match(filterForm.releaseGroup, meta_info.resource_team)
|
||||
// 视频编码过滤
|
||||
&& match(filterForm.videoCode, meta_info.video_encode)
|
||||
// 分辨率过滤
|
||||
&& match(filterForm.resolution, meta_info.resource_pix)
|
||||
// 质量过滤
|
||||
&& match(filterForm.edition, meta_info.edition)
|
||||
)
|
||||
})
|
||||
if (matchData.length > 0) {
|
||||
const firstData = _.cloneDeepWith(matchData[0]) as SearchTorrent
|
||||
if (matchData.length > 1)
|
||||
firstData.more = matchData.slice(1)
|
||||
dataList.value.push(firstData)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -307,20 +215,14 @@ onMounted(initData)
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCard>
|
||||
<div v-if="!isRefreshed" class="mt-12 w-full text-center text-gray-500 text-sm flex flex-col items-center">
|
||||
<VProgressCircular v-if="!props.keyword" size="48" indeterminate color="primary" />
|
||||
<VProgressCircular v-if="props.keyword" class="mb-3" color="primary" :model-value="progressValue" size="64" />
|
||||
<span>{{ progressText }}</span>
|
||||
<div class="grid gap-3 grid-torrent-card items-start">
|
||||
<TorrentCard
|
||||
v-for="(item, index) in dataList"
|
||||
:key="`${index}_${item.torrent_info.title}_${item.torrent_info.site}`"
|
||||
:torrent="item"
|
||||
:more="item.more"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="dataList.length > 0" class="grid gap-3 grid-torrent-card items-start">
|
||||
<TorrentCard v-for="data in dataList" :key="`${data.torrent_info.title}_${data.torrent_info.site_name}_${data.torrent_info.page_url}`" :torrent="data" :more="data.more" />
|
||||
</div>
|
||||
<NoDataFound
|
||||
v-if="dataList.length === 0 && isRefreshed"
|
||||
error-code="404"
|
||||
error-title="没有资源"
|
||||
error-description="没有搜索到符合条件的资源。"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
245
src/views/discover/TorrentRowListView.vue
Normal file
245
src/views/discover/TorrentRowListView.vue
Normal file
@@ -0,0 +1,245 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Context } from '@/api/types'
|
||||
import TorrentItem from '@/components/cards/TorrentItem.vue'
|
||||
|
||||
// 定义输入参数
|
||||
const props = defineProps({
|
||||
// 数据列表
|
||||
items: Array as PropType<Context[]>,
|
||||
})
|
||||
|
||||
// 过滤表单
|
||||
const filterForm = reactive({
|
||||
// 站点
|
||||
site: [] as string[],
|
||||
// 季
|
||||
season: [] as string[],
|
||||
// 制作组
|
||||
releaseGroup: [] as string[],
|
||||
// 视频编码
|
||||
videoCode: [] as string[],
|
||||
// 促销状态
|
||||
freeState: [] as string[],
|
||||
// 质量
|
||||
edition: [] as string[],
|
||||
// 分辨率
|
||||
resolution: [] as string[],
|
||||
})
|
||||
|
||||
// 数据列表
|
||||
const dataList = ref <Array<Context>>([])
|
||||
|
||||
// 获取站点过滤选项
|
||||
const siteFilterOptions = ref<Array<string>>([])
|
||||
// 获取季过滤选项
|
||||
const seasonFilterOptions = ref<Array<string>>([])
|
||||
// 获取制作组过滤选项
|
||||
const releaseGroupFilterOptions = ref<Array<string>>([])
|
||||
// 获取视频编码过滤选项
|
||||
const videoCodeFilterOptions = ref<Array<string>>([])
|
||||
// 获取促销状态过滤选项
|
||||
const freeStateFilterOptions = ref<Array<string>>([])
|
||||
// 获取质量过滤选项
|
||||
const editionFilterOptions = ref<Array<string>>([])
|
||||
// 获取分辨率过滤选项
|
||||
const resolutionFilterOptions = ref<Array<string>>([])
|
||||
|
||||
// 初始化过滤选项
|
||||
function initOptions(data: Context) {
|
||||
const { torrent_info, meta_info } = data
|
||||
const optionValue = (options: Array<string>, value: string | undefined) => {
|
||||
value && !options.includes(value) && options.push(value)
|
||||
}
|
||||
optionValue(siteFilterOptions.value, torrent_info?.site_name)
|
||||
optionValue(seasonFilterOptions.value, meta_info?.season_episode)
|
||||
optionValue(releaseGroupFilterOptions.value, meta_info?.resource_team)
|
||||
optionValue(videoCodeFilterOptions.value, meta_info?.video_encode)
|
||||
optionValue(freeStateFilterOptions.value, torrent_info?.volume_factor)
|
||||
optionValue(editionFilterOptions.value, meta_info?.edition)
|
||||
optionValue(resolutionFilterOptions.value, meta_info?.resource_pix)
|
||||
}
|
||||
|
||||
// 计算过滤后的列表
|
||||
watchEffect(() => {
|
||||
// 清空列表
|
||||
dataList.value.splice(0)
|
||||
// 匹配过滤函数
|
||||
const match = (filter: Array<string>, value: string | undefined) =>
|
||||
filter.length === 0 || (value && filter.includes(value))
|
||||
|
||||
props.items?.forEach((data) => {
|
||||
const { meta_info, torrent_info } = data
|
||||
if (
|
||||
// 站点过滤
|
||||
match(filterForm.site, torrent_info.site_name)
|
||||
// 促销状态过滤
|
||||
&& match(filterForm.freeState, torrent_info.volume_factor)
|
||||
// 季过滤
|
||||
&& match(filterForm.season, meta_info.season_episode)
|
||||
// 制作组过滤
|
||||
&& match(filterForm.releaseGroup, meta_info.resource_team)
|
||||
// 视频编码过滤
|
||||
&& match(filterForm.videoCode, meta_info.video_encode)
|
||||
// 分辨率过滤
|
||||
&& match(filterForm.resolution, meta_info.resource_pix)
|
||||
// 质量过滤
|
||||
&& match(filterForm.edition, meta_info.edition)
|
||||
)
|
||||
dataList.value.push(data)
|
||||
})
|
||||
})
|
||||
|
||||
// 初始化过滤选项
|
||||
onMounted(() => {
|
||||
props.items?.forEach((item) => {
|
||||
initOptions(item)
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VRow>
|
||||
<VCol>
|
||||
<VList
|
||||
lines="three"
|
||||
class="rounded"
|
||||
>
|
||||
<TorrentItem
|
||||
v-for="(item, index) in dataList"
|
||||
:key="`${index}_${item.torrent_info.title}_${item.torrent_info.site}`"
|
||||
:torrent="item"
|
||||
/>
|
||||
<VListItem v-if="dataList.length === 0">
|
||||
<VListItemTitle>没有附合当前过滤条件的资源。</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VCol>
|
||||
<VCol
|
||||
xl="2"
|
||||
md="3"
|
||||
class="d-none d-md-block"
|
||||
>
|
||||
<VList lines="one" class="rounded">
|
||||
<VListSubheader v-if="siteFilterOptions.length > 0">
|
||||
站点
|
||||
</VListSubheader>
|
||||
<VListItem>
|
||||
<VChipGroup v-model="filterForm.site" column multiple>
|
||||
<VChip
|
||||
v-for="site in siteFilterOptions"
|
||||
:key="site"
|
||||
:color="filterForm.site.includes(site) ? 'primary' : ''"
|
||||
filter
|
||||
variant="outlined"
|
||||
:value="site"
|
||||
>
|
||||
{{ site }}
|
||||
</VChip>
|
||||
</VChipGroup>
|
||||
</VListItem>
|
||||
<VListSubheader v-if="editionFilterOptions.length > 0">
|
||||
质量
|
||||
</VListSubheader>
|
||||
<VListItem>
|
||||
<VChipGroup v-model="filterForm.edition" column multiple>
|
||||
<VChip
|
||||
v-for="edition in editionFilterOptions"
|
||||
:key="edition"
|
||||
:color="filterForm.edition.includes(edition) ? 'primary' : ''"
|
||||
filter
|
||||
variant="outlined"
|
||||
:value="edition"
|
||||
>
|
||||
{{ edition }}
|
||||
</VChip>
|
||||
</VChipGroup>
|
||||
</VListItem>
|
||||
<VListSubheader v-if="resolutionFilterOptions.length > 0">
|
||||
分辨率
|
||||
</VListSubheader>
|
||||
<VListItem>
|
||||
<VChipGroup v-model="filterForm.resolution" column multiple>
|
||||
<VChip
|
||||
v-for="resolution in resolutionFilterOptions"
|
||||
:key="resolution"
|
||||
:color="filterForm.resolution.includes(resolution) ? 'primary' : ''"
|
||||
filter
|
||||
variant="outlined"
|
||||
:value="resolution"
|
||||
>
|
||||
{{ resolution }}
|
||||
</VChip>
|
||||
</VChipGroup>
|
||||
</VListItem>
|
||||
<VListSubheader v-if="releaseGroupFilterOptions.length > 0">
|
||||
制作组
|
||||
</VListSubheader>
|
||||
<VListItem>
|
||||
<VChipGroup v-model="filterForm.releaseGroup" column multiple>
|
||||
<VChip
|
||||
v-for="releaseGroup in releaseGroupFilterOptions"
|
||||
:key="releaseGroup"
|
||||
:color="filterForm.releaseGroup.includes(releaseGroup) ? 'primary' : ''"
|
||||
filter
|
||||
variant="outlined"
|
||||
:value="releaseGroup"
|
||||
>
|
||||
{{ releaseGroup }}
|
||||
</VChip>
|
||||
</VChipGroup>
|
||||
</VListItem>
|
||||
<VListSubheader v-if="videoCodeFilterOptions.length > 0">
|
||||
视频编码
|
||||
</VListSubheader>
|
||||
<VListItem>
|
||||
<VChipGroup v-model="filterForm.videoCode" column multiple>
|
||||
<VChip
|
||||
v-for="videoCode in videoCodeFilterOptions"
|
||||
:key="videoCode"
|
||||
:color="filterForm.videoCode.includes(videoCode) ? 'primary' : ''"
|
||||
filter
|
||||
variant="outlined"
|
||||
:value="videoCode"
|
||||
>
|
||||
{{ videoCode }}
|
||||
</VChip>
|
||||
</VChipGroup>
|
||||
</VListItem>
|
||||
<VListSubheader v-if="freeStateFilterOptions.length > 0">
|
||||
促销状态
|
||||
</VListSubheader>
|
||||
<VListItem>
|
||||
<VChipGroup v-model="filterForm.freeState" column multiple>
|
||||
<VChip
|
||||
v-for="freeState in freeStateFilterOptions"
|
||||
:key="freeState"
|
||||
:color="filterForm.freeState.includes(freeState) ? 'primary' : ''"
|
||||
filter
|
||||
variant="outlined"
|
||||
:value="freeState"
|
||||
>
|
||||
{{ freeState }}
|
||||
</VChip>
|
||||
</VChipGroup>
|
||||
</VListItem>
|
||||
<VListSubheader v-if="seasonFilterOptions.length > 0">
|
||||
季集
|
||||
</VListSubheader>
|
||||
<VListItem>
|
||||
<VChipGroup v-model="filterForm.season" column multiple>
|
||||
<VChip
|
||||
v-for="season in seasonFilterOptions"
|
||||
:key="season"
|
||||
:color="filterForm.season.includes(season) ? 'primary' : ''"
|
||||
filter
|
||||
variant="outlined"
|
||||
:value="season"
|
||||
>
|
||||
{{ season }}
|
||||
</VChip>
|
||||
</VChipGroup>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</template>
|
||||
@@ -4,6 +4,11 @@ import api from '@/api'
|
||||
import type { DownloadingInfo } from '@/api/types'
|
||||
import NoDataFound from '@/components/NoDataFound.vue'
|
||||
import DownloadingCard from '@/components/cards/DownloadingCard.vue'
|
||||
import store from '@/store'
|
||||
|
||||
// 从Vuex Store中获取用户信息
|
||||
const superUser = store.state.auth.superUser
|
||||
const userName = store.state.auth.userName
|
||||
|
||||
// 定时器
|
||||
let refreshTimer: NodeJS.Timer | null = null
|
||||
@@ -35,6 +40,14 @@ function onRefresh() {
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
// 过滤数据,管理员用户显示全部,非管理员只显示自己的订阅
|
||||
const filteredDataList = computed(() => {
|
||||
if (superUser)
|
||||
return dataList.value
|
||||
else
|
||||
return dataList.value.filter(data => data.userid === userName)
|
||||
})
|
||||
|
||||
// 加载时获取数据
|
||||
onBeforeMount(() => {
|
||||
fetchData()
|
||||
@@ -71,17 +84,17 @@ onUnmounted(() => {
|
||||
@refresh="onRefresh"
|
||||
>
|
||||
<div
|
||||
v-if="dataList.length > 0"
|
||||
v-if="filteredDataList.length > 0"
|
||||
class="grid gap-3 grid-downloading-card"
|
||||
>
|
||||
<DownloadingCard
|
||||
v-for="data in dataList"
|
||||
v-for="data in filteredDataList"
|
||||
:key="data.hash"
|
||||
:info="data"
|
||||
/>
|
||||
</div>
|
||||
<NoDataFound
|
||||
v-if="dataList.length === 0 && isRefreshed"
|
||||
v-if="filteredDataList.length === 0 && isRefreshed"
|
||||
error-code="404"
|
||||
error-title="没有任务"
|
||||
error-description="正在下载的任务将会显示在这里。"
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import { numberValidator } from '@/@validators'
|
||||
import api from '@/api'
|
||||
import type { TransferHistory } from '@/api/types'
|
||||
import TmdbSelectorCard from '@/components/cards/TmdbSelectorCard.vue'
|
||||
import ReorganizeForm from '@/components/form/ReorganizeForm.vue'
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
@@ -12,22 +11,15 @@ const $toast = useToast()
|
||||
// 重新整理对话框
|
||||
const redoDialog = ref(false)
|
||||
|
||||
// TMDB编号
|
||||
const redoTmdbId = ref('')
|
||||
|
||||
// 类型
|
||||
const redoType = ref('电影')
|
||||
|
||||
// 类型下拉框:电影、电视剧
|
||||
const redoTypeItems = ref([
|
||||
{ title: '自动', value: '' },
|
||||
{ title: '电影', value: '电影' },
|
||||
{ title: '电视剧', value: '电视剧' },
|
||||
])
|
||||
|
||||
// 当前操作记录
|
||||
const currentHistory = ref<TransferHistory>()
|
||||
|
||||
// 重新整理IDS
|
||||
const redoIds = ref<number[]>([])
|
||||
|
||||
// 重新整理target
|
||||
const redoTarget = ref('')
|
||||
|
||||
// 已选中的数据
|
||||
const selected = ref<TransferHistory[]>([])
|
||||
|
||||
@@ -69,9 +61,6 @@ const progressText = ref('请稍候 ...')
|
||||
// 进度值
|
||||
const progressValue = ref(0)
|
||||
|
||||
// TMDB选择对话框
|
||||
const tmdbSelectorDialog = ref(false)
|
||||
|
||||
// 删除确认对话框
|
||||
const deleteConfirmDialog = ref(false)
|
||||
|
||||
@@ -227,85 +216,17 @@ async function retransferBatch() {
|
||||
return
|
||||
// 清空当前操作记录
|
||||
currentHistory.value = undefined
|
||||
// 重新整理IDS
|
||||
redoIds.value = selected.value.map(item => item.id)
|
||||
// 重新整理target
|
||||
if (selected.value.length === 1)
|
||||
redoTarget.value = selected.value[0].dest ?? ''
|
||||
else
|
||||
redoTarget.value = ''
|
||||
// 打开识别弹窗
|
||||
redoType.value = ''
|
||||
redoTmdbId.value = ''
|
||||
redoDialog.value = true
|
||||
}
|
||||
|
||||
// 调API重新整理
|
||||
async function retransfer(item: TransferHistory, redoType = '', redoTmdbId = 0) {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post(
|
||||
'history/transfer',
|
||||
item,
|
||||
{
|
||||
params: {
|
||||
mtype: redoType,
|
||||
new_tmdbid: redoTmdbId,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
if (result.success) {
|
||||
fetchData({
|
||||
page: currentPage.value,
|
||||
itemsPerPage: itemsPerPage.value,
|
||||
})
|
||||
}
|
||||
else {
|
||||
$toast.error(`重新整理失败: ${result.message}!`)
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
// 重新整理
|
||||
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([
|
||||
{
|
||||
@@ -314,10 +235,9 @@ const dropdownItems = ref([
|
||||
props: {
|
||||
prependIcon: 'mdi-redo-variant',
|
||||
click: (item: TransferHistory) => {
|
||||
redoTmdbId.value = ''
|
||||
redoType.value = ''
|
||||
redoIds.value = [item.id]
|
||||
redoTarget.value = item.dest ?? ''
|
||||
redoDialog.value = true
|
||||
currentHistory.value = item
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -444,47 +364,11 @@ const dropdownItems = ref([
|
||||
</template>
|
||||
</VDataTableServer>
|
||||
</VCard>
|
||||
<VDialog
|
||||
v-model="redoDialog"
|
||||
max-width="50rem"
|
||||
<!-- 底部操作按钮 -->
|
||||
<span
|
||||
v-if="selected.length > 0"
|
||||
class="fixed right-5 bottom-5"
|
||||
>
|
||||
<!-- Dialog Content -->
|
||||
<VCard title="重新整理">
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol cols="12" md="4">
|
||||
<VSelect
|
||||
v-model="redoType"
|
||||
label="类型"
|
||||
:items="redoTypeItems"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="8">
|
||||
<VTextField
|
||||
v-model="redoTmdbId"
|
||||
label="TMDB编号"
|
||||
placeholder="留空自动识别"
|
||||
:disabled="redoType === ''"
|
||||
:rules="[numberValidator]"
|
||||
append-inner-icon="mdi-magnify"
|
||||
@click:append-inner.stop="tmdbSelectorDialog = true"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
<VBtn
|
||||
@click="rehandleHistory"
|
||||
@keydown.enter="rehandleHistory"
|
||||
>
|
||||
确定
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
<span v-if="selected.length > 0" class="fixed right-5 bottom-5">
|
||||
<VBtn
|
||||
icon="mdi-redo-variant"
|
||||
class="me-2"
|
||||
@@ -499,39 +383,9 @@ const dropdownItems = ref([
|
||||
@click="removeHistoryBatch"
|
||||
/>
|
||||
</span>
|
||||
<!-- 进度框 -->
|
||||
<vDialog
|
||||
v-model="progressDialog"
|
||||
:scrim="false"
|
||||
width="25rem"
|
||||
>
|
||||
<vCard
|
||||
color="primary"
|
||||
>
|
||||
<vCardText class="text-center">
|
||||
{{ progressText }}
|
||||
<vProgressLinear
|
||||
color="white"
|
||||
class="mb-0 mt-1"
|
||||
:model-value="progressValue"
|
||||
/>
|
||||
</vCardText>
|
||||
</vCard>
|
||||
</vDialog>
|
||||
<!-- TMDB ID搜索框 -->
|
||||
<vDialog
|
||||
v-model="tmdbSelectorDialog"
|
||||
width="600"
|
||||
scrollable
|
||||
>
|
||||
<TmdbSelectorCard
|
||||
v-model="redoTmdbId"
|
||||
@close="tmdbSelectorDialog = false"
|
||||
/>
|
||||
</vDialog>
|
||||
<!-- 底部弹窗 -->
|
||||
<VBottomSheet v-model="deleteConfirmDialog" inset>
|
||||
<VCard class="text-center">
|
||||
<VCard class="text-center rounded-t">
|
||||
<DialogCloseBtn @click="deleteConfirmDialog = false" />
|
||||
<VCardTitle class="pe-10">
|
||||
{{ confirmTitle }}
|
||||
@@ -568,6 +422,24 @@ const dropdownItems = ref([
|
||||
</div>
|
||||
</VCard>
|
||||
</VBottomSheet>
|
||||
<!-- 文件整理弹窗 -->
|
||||
<ReorganizeForm
|
||||
v-model="redoDialog"
|
||||
:logids="redoIds"
|
||||
:target="redoTarget"
|
||||
@done="() => {
|
||||
redoDialog = false
|
||||
// 清空当前操作记录
|
||||
currentHistory = undefined
|
||||
selected = []
|
||||
// 刷新
|
||||
fetchData({
|
||||
page: currentPage,
|
||||
itemsPerPage,
|
||||
})
|
||||
}"
|
||||
@close="redoDialog = false"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -1,163 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import api from '@/api'
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
// 种子优先规则
|
||||
const selectedTorrentPriority = ref<string>('seeder')
|
||||
|
||||
// 种子优先规则下拉框
|
||||
const TorrentPriorityItems = [
|
||||
{ title: '站点优先', value: 'site' },
|
||||
{ title: '做种数优先', value: 'seeder' },
|
||||
]
|
||||
|
||||
// 包含与排除规则
|
||||
const defaultFilterRules = ref({
|
||||
include: '',
|
||||
exclude: '',
|
||||
})
|
||||
|
||||
// 查询种子优先规则
|
||||
async function queryTorrentPriority() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get(
|
||||
'system/setting/TorrentsPriority',
|
||||
)
|
||||
|
||||
selectedTorrentPriority.value = result.data?.value
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 查询包含与排除规则
|
||||
async function queryDefaultFilter() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get(
|
||||
'system/setting/DefaultFilterRules',
|
||||
)
|
||||
if (result.data?.value)
|
||||
defaultFilterRules.value = result.data?.value
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存种子优先规则
|
||||
async function saveTorrentPriority() {
|
||||
try {
|
||||
// 用户名密码
|
||||
const result: { [key: string]: any } = await api.post(
|
||||
'system/setting/TorrentsPriority',
|
||||
selectedTorrentPriority.value,
|
||||
)
|
||||
|
||||
if (result.success)
|
||||
$toast.success('优先规则保存成功')
|
||||
else
|
||||
$toast.error('优先规则保存失败!')
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存包含与排除规则
|
||||
async function saveDefaultFilter() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post(
|
||||
'system/setting/DefaultFilterRules',
|
||||
defaultFilterRules.value,
|
||||
)
|
||||
if (result.success)
|
||||
$toast.success('默认包含/排除规则保存成功')
|
||||
else
|
||||
$toast.error('默认包含/排除规则保存失败!')
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
queryTorrentPriority()
|
||||
queryDefaultFilter()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VCard title="下载优先规则">
|
||||
<VCardSubtitle> 按站点优先级或资源种子数量排序和择优下载。 </VCardSubtitle>
|
||||
<VCardText>
|
||||
<VForm>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VSelect
|
||||
v-model="selectedTorrentPriority"
|
||||
:items="TorrentPriorityItems"
|
||||
label="优先规则"
|
||||
outlined
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</vform>
|
||||
</VCardText>
|
||||
<VCardItem>
|
||||
<VBtn
|
||||
type="submit"
|
||||
@click="saveTorrentPriority"
|
||||
>
|
||||
保存
|
||||
</VBtn>
|
||||
</VCardItem>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VCard title="默认过滤规则">
|
||||
<VCardSubtitle> 设置在搜索和订阅时默认使用的过滤规则。 </VCardSubtitle>
|
||||
<VCardText>
|
||||
<VForm>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="defaultFilterRules.include"
|
||||
type="text"
|
||||
label="包含(关键字、正则式)"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="defaultFilterRules.exclude"
|
||||
type="text"
|
||||
label="排除(关键字、正则式)"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
<VCardItem>
|
||||
<VBtn
|
||||
type="submit"
|
||||
@click="saveDefaultFilter"
|
||||
>
|
||||
保存
|
||||
</VBtn>
|
||||
</VCardItem>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.grid-filterrule-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
</style>
|
||||
@@ -24,6 +24,12 @@ const allSites = ref<Site[]>([])
|
||||
// 选中订阅站点
|
||||
const selectedSites = ref<number[]>([])
|
||||
|
||||
// 包含与排除规则
|
||||
const defaultFilterRules = ref({
|
||||
include: '',
|
||||
exclude: '',
|
||||
})
|
||||
|
||||
// 查询已设置优先级规则
|
||||
async function queryCustomFilters() {
|
||||
try {
|
||||
@@ -190,9 +196,41 @@ function onLevelDown(pri: string) {
|
||||
filterCards.value.sort((a, b) => parseInt(a.pri) - parseInt(b.pri))
|
||||
}
|
||||
|
||||
// 查询包含与排除规则
|
||||
async function queryDefaultFilter() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get(
|
||||
'system/setting/DefaultSearchFilterRules',
|
||||
)
|
||||
if (result.data?.value)
|
||||
defaultFilterRules.value = result.data?.value
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存包含与排除规则
|
||||
async function saveDefaultFilter() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post(
|
||||
'system/setting/DefaultSearchFilterRules',
|
||||
defaultFilterRules.value,
|
||||
)
|
||||
if (result.success)
|
||||
$toast.success('默认包含/排除规则保存成功')
|
||||
else
|
||||
$toast.error('默认包含/排除规则保存失败!')
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
queryCustomFilters()
|
||||
querySites()
|
||||
queryDefaultFilter()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -260,6 +298,39 @@ onMounted(() => {
|
||||
</VCardItem>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VCard title="默认过滤规则">
|
||||
<VCardSubtitle> 设置在搜索时默认使用的过滤规则。 </VCardSubtitle>
|
||||
<VCardText>
|
||||
<VForm>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="defaultFilterRules.include"
|
||||
type="text"
|
||||
label="包含(关键字、正则式)"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="defaultFilterRules.exclude"
|
||||
type="text"
|
||||
label="排除(关键字、正则式)"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
<VCardItem>
|
||||
<VBtn
|
||||
type="submit"
|
||||
@click="saveDefaultFilter"
|
||||
>
|
||||
保存
|
||||
</VBtn>
|
||||
</VCardItem>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -14,6 +14,15 @@ const resetSitesText = ref('重置站点数据')
|
||||
// 站点重置按钮可用状态
|
||||
const resetSitesDisabled = ref(false)
|
||||
|
||||
// 种子优先规则
|
||||
const selectedTorrentPriority = ref<string>('seeder')
|
||||
|
||||
// 种子优先规则下拉框
|
||||
const TorrentPriorityItems = [
|
||||
{ title: '站点优先', value: 'site' },
|
||||
{ title: '做种数优先', value: 'seeder' },
|
||||
]
|
||||
|
||||
// 重置站点
|
||||
async function resetSites() {
|
||||
try {
|
||||
@@ -34,10 +43,74 @@ async function resetSites() {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 查询种子优先规则
|
||||
async function queryTorrentPriority() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get(
|
||||
'system/setting/TorrentsPriority',
|
||||
)
|
||||
|
||||
selectedTorrentPriority.value = result.data?.value
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存种子优先规则
|
||||
async function saveTorrentPriority() {
|
||||
try {
|
||||
// 用户名密码
|
||||
const result: { [key: string]: any } = await api.post(
|
||||
'system/setting/TorrentsPriority',
|
||||
selectedTorrentPriority.value,
|
||||
)
|
||||
|
||||
if (result.success)
|
||||
$toast.success('优先规则保存成功')
|
||||
else
|
||||
$toast.error('优先规则保存失败!')
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
queryTorrentPriority()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VCard title="下载优先规则">
|
||||
<VCardSubtitle> 按站点或做种数量优先下载。 </VCardSubtitle>
|
||||
<VCardText>
|
||||
<VForm>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VSelect
|
||||
v-model="selectedTorrentPriority"
|
||||
:items="TorrentPriorityItems"
|
||||
label="优先规则"
|
||||
outlined
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
<VCardItem>
|
||||
<VBtn
|
||||
type="submit"
|
||||
@click="saveTorrentPriority"
|
||||
>
|
||||
保存
|
||||
</VBtn>
|
||||
</VCardItem>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VCard title="站点重置">
|
||||
<VCardText>
|
||||
|
||||
@@ -27,6 +27,12 @@ const allSites = ref<Site[]>([])
|
||||
// 选中订阅站点
|
||||
const selectedRssSites = ref<number[]>([])
|
||||
|
||||
// 包含与排除规则
|
||||
const defaultFilterRules = ref({
|
||||
include: '',
|
||||
exclude: '',
|
||||
})
|
||||
|
||||
// 查询用户选中的订阅站点
|
||||
async function querySelectedRssSites() {
|
||||
try {
|
||||
@@ -207,10 +213,42 @@ function onLevelDown(filterCards: FilterCard[], pri: string) {
|
||||
filterCards.sort((a, b) => parseInt(a.pri) - parseInt(b.pri))
|
||||
}
|
||||
|
||||
// 查询包含与排除规则
|
||||
async function queryDefaultFilter() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get(
|
||||
'system/setting/DefaultFilterRules',
|
||||
)
|
||||
if (result.data?.value)
|
||||
defaultFilterRules.value = result.data?.value
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存包含与排除规则
|
||||
async function saveDefaultFilter() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post(
|
||||
'system/setting/DefaultFilterRules',
|
||||
defaultFilterRules.value,
|
||||
)
|
||||
if (result.success)
|
||||
$toast.success('默认包含/排除规则保存成功')
|
||||
else
|
||||
$toast.error('默认包含/排除规则保存失败!')
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
querySites()
|
||||
queryCustomFilters('SubscribeFilterRules')
|
||||
queryCustomFilters('BestVersionFilterRules')
|
||||
queryDefaultFilter()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -314,6 +352,39 @@ onMounted(() => {
|
||||
</VCardItem>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VCard title="默认过滤规则">
|
||||
<VCardSubtitle> 设置在订阅时默认使用的过滤规则。 </VCardSubtitle>
|
||||
<VCardText>
|
||||
<VForm>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="defaultFilterRules.include"
|
||||
type="text"
|
||||
label="包含(关键字、正则式)"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="defaultFilterRules.exclude"
|
||||
type="text"
|
||||
label="排除(关键字、正则式)"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
<VCardItem>
|
||||
<VBtn
|
||||
type="submit"
|
||||
@click="saveDefaultFilter"
|
||||
>
|
||||
保存
|
||||
</VBtn>
|
||||
</VCardItem>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import api from '@/api'
|
||||
import type { Site } from '@/api/types'
|
||||
import SiteCard from '@/components/cards/SiteCard.vue'
|
||||
import NoDataFound from '@/components/NoDataFound.vue'
|
||||
import { numberValidator, requiredValidator } from '@/@validators'
|
||||
import { doneNProgress, startNProgress } from '@/api/nprogress'
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
import SiteAddEditForm from '@/components/form/SiteAddEditForm.vue'
|
||||
|
||||
// 数据列表
|
||||
const dataList = ref<Site[]>([])
|
||||
@@ -16,45 +11,9 @@ const dataList = ref<Site[]>([])
|
||||
// 是否刷新过
|
||||
const isRefreshed = ref(false)
|
||||
|
||||
// 新增按钮文本
|
||||
const addBtnText = ref('新增站点')
|
||||
// 新增按钮状态
|
||||
const addBtnState = ref(false)
|
||||
|
||||
// 新增站点对话框
|
||||
const siteAddDialog = ref(false)
|
||||
|
||||
// 状态下拉项
|
||||
const statusItems = [
|
||||
{ title: '启用', value: true },
|
||||
{ title: '停用', value: false },
|
||||
]
|
||||
|
||||
// 生成1到50的优先级下拉框选项
|
||||
const priorityItems = ref(
|
||||
Array.from({ length: 50 }, (_, i) => i + 1).map(item => ({
|
||||
title: item,
|
||||
value: item,
|
||||
})),
|
||||
)
|
||||
|
||||
// 站点编辑表单数据
|
||||
const siteForm = reactive<Site>({
|
||||
id: 0,
|
||||
url: '',
|
||||
pri: 1,
|
||||
is_active: true,
|
||||
cookie: '',
|
||||
ua: '',
|
||||
limit_interval: 0,
|
||||
limit_seconds: 0,
|
||||
limit_count: 0,
|
||||
proxy: 0,
|
||||
render: 0,
|
||||
name: '',
|
||||
domain: '',
|
||||
})
|
||||
|
||||
// 获取站点列表数据
|
||||
async function fetchData() {
|
||||
try {
|
||||
@@ -66,38 +25,6 @@ async function fetchData() {
|
||||
}
|
||||
}
|
||||
|
||||
// 调用API 新增站点
|
||||
async function addSite() {
|
||||
if (!siteForm.url)
|
||||
return
|
||||
|
||||
startNProgress()
|
||||
|
||||
addBtnText.value = '新增中...'
|
||||
addBtnState.value = true
|
||||
|
||||
try {
|
||||
const result: { [key: string]: string } = await api.post('site/', siteForm)
|
||||
if (result.success) {
|
||||
$toast.success('新增站点成功')
|
||||
|
||||
// 刷新数据
|
||||
fetchData()
|
||||
}
|
||||
|
||||
else { $toast.error(`新增站点失败:${result.message}`) }
|
||||
siteAddDialog.value = false
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
|
||||
doneNProgress()
|
||||
|
||||
addBtnText.value = '新增站点'
|
||||
addBtnState.value = false
|
||||
}
|
||||
|
||||
// 加载时获取数据
|
||||
onBeforeMount(fetchData)
|
||||
</script>
|
||||
@@ -132,150 +59,20 @@ onBeforeMount(fetchData)
|
||||
error-title="没有站点"
|
||||
error-description="已添加并支持的站点将会在这里显示。"
|
||||
/>
|
||||
<!-- Dialog Content -->
|
||||
<VDialog
|
||||
<!-- 新增站点按钮 -->
|
||||
<VBtn
|
||||
icon="mdi-plus"
|
||||
size="x-large"
|
||||
class="fixed right-5 bottom-5"
|
||||
oper="add"
|
||||
@click="siteAddDialog = true"
|
||||
/>
|
||||
<SiteAddEditForm
|
||||
v-model="siteAddDialog"
|
||||
max-width="50rem"
|
||||
persistent
|
||||
scrollable
|
||||
>
|
||||
<!-- Dialog Activator -->
|
||||
<template #activator="{ props }">
|
||||
<VBtn
|
||||
icon="mdi-plus"
|
||||
v-bind="props"
|
||||
size="x-large"
|
||||
class="fixed right-5 bottom-5"
|
||||
/>
|
||||
</template>
|
||||
<VCard title="新增站点">
|
||||
<DialogCloseBtn @click="siteAddDialog = false" />
|
||||
<VCardText class="pt-2">
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VTextField
|
||||
v-model="siteForm.url"
|
||||
label="站点地址"
|
||||
:rules="[requiredValidator]"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="3"
|
||||
>
|
||||
<VSelect
|
||||
v-model="siteForm.pri"
|
||||
label="优先级"
|
||||
:items="priorityItems"
|
||||
:rules="[requiredValidator]"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="3"
|
||||
>
|
||||
<VSelect
|
||||
v-model="siteForm.is_active"
|
||||
:items="statusItems"
|
||||
label="状态"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
v-model="siteForm.rss"
|
||||
label="RSS地址"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VTextarea
|
||||
v-model="siteForm.cookie"
|
||||
label="站点Cookie"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
v-model="siteForm.ua"
|
||||
label="站点User-Agent"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<VTextField
|
||||
v-model="siteForm.limit_interval"
|
||||
label="单位周期(秒)"
|
||||
:rules="[numberValidator]"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<VTextField
|
||||
v-model="siteForm.limit_seconds"
|
||||
label="访问次数"
|
||||
:rules="[numberValidator]"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<VTextField
|
||||
v-model="siteForm.limit_seconds"
|
||||
label="访问间隔(秒)"
|
||||
:rules="[numberValidator]"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VSwitch
|
||||
v-model="siteForm.proxy"
|
||||
label="代理"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VSwitch
|
||||
v-model="siteForm.render"
|
||||
label="仿真"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VBtn
|
||||
@click="siteAddDialog = false"
|
||||
>
|
||||
取消
|
||||
</VBtn>
|
||||
<VSpacer />
|
||||
<VBtn
|
||||
color="primary"
|
||||
:disabled="addBtnState"
|
||||
@click="addSite"
|
||||
>
|
||||
{{ addBtnText }}
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
oper="add"
|
||||
@save="siteAddDialog = false; fetchData()"
|
||||
@close="siteAddDialog = false"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -4,12 +4,17 @@ import api from '@/api'
|
||||
import type { Subscribe } from '@/api/types'
|
||||
import NoDataFound from '@/components/NoDataFound.vue'
|
||||
import SubscribeCard from '@/components/cards/SubscribeCard.vue'
|
||||
import store from '@/store'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
type: String,
|
||||
})
|
||||
|
||||
// 从Vuex Store中获取用户信息
|
||||
const superUser = store.state.auth.superUser
|
||||
const userName = store.state.auth.userName
|
||||
|
||||
// 是否刷新过
|
||||
const isRefreshed = ref(false)
|
||||
|
||||
@@ -40,9 +45,12 @@ function onRefresh() {
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
// 过滤数据
|
||||
// 过滤数据,管理员用户显示全部,非管理员只显示自己的订阅
|
||||
const filteredDataList = computed(() => {
|
||||
return dataList.value.filter(data => data.type === props.type)
|
||||
if (superUser)
|
||||
return dataList.value.filter(data => data.type === props.type)
|
||||
else
|
||||
return dataList.value.filter(data => data.type === props.type && data.username === userName)
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ module.exports = {
|
||||
},
|
||||
plugins: [
|
||||
require('@tailwindcss/aspect-ratio'),
|
||||
|
||||
// ...
|
||||
],
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user