Compare commits

..

17 Commits

Author SHA1 Message Date
jxxghp
5a3673efc6 更新 package.json 2023-10-14 14:34:16 +08:00
jxxghp
c03ec1d741 fix ui 2023-10-14 14:29:57 +08:00
jxxghp
e62d0809b3 fix ui 2023-10-14 14:15:38 +08:00
jxxghp
7f13597517 feat merge form 2023-10-14 13:48:02 +08:00
jxxghp
c822f1fffd feat 整合站点编辑组件 2023-10-14 09:13:38 +08:00
jxxghp
14ca74a29d Merge branch 'main' of https://github.com/jxxghp/MoviePilot-Frontend 2023-10-13 22:56:07 +08:00
jxxghp
3ee897a350 fix 2023-10-13 22:56:02 +08:00
jxxghp
789aac60c9 更新 package.json 2023-10-13 22:39:49 +08:00
jxxghp
2c73a8f3e1 fix ui 2023-10-13 22:37:04 +08:00
jxxghp
539bc656f8 fix 2023-10-13 22:33:58 +08:00
jxxghp
feda0cad2d feat 默认过滤规则拆分 2023-10-13 21:28:34 +08:00
jxxghp
c723d89739 fix 2023-10-13 17:30:14 +08:00
jxxghp
0a0e7a059a fix 2023-10-13 17:29:23 +08:00
jxxghp
0263fbbee6 fix 2023-10-13 17:26:12 +08:00
jxxghp
e205296e22 feat 订阅实时编辑 2023-10-13 17:24:18 +08:00
jxxghp
261f5a9c68 fix #822 2023-10-13 15:14:26 +08:00
jxxghp
fa097651f4 fix rules 2023-10-13 11:41:27 +08:00
26 changed files with 1392 additions and 1346 deletions

View File

@@ -1,6 +1,6 @@
{ {
"name": "moviepilot", "name": "moviepilot",
"version": "1.3.2-2", "version": "1.3.3-1",
"private": true, "private": true,
"bin": "dist/service.js", "bin": "dist/service.js",
"scripts": { "scripts": {

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

View File

@@ -77,8 +77,8 @@ export interface Subscribe {
// 订阅站点 // 订阅站点
sites: number[] sites: number[]
// 是否洗版 // 是否洗版数字或者boolean
best_version: number best_version: any
// 当前优先级 // 当前优先级
current_priority: number current_priority: number
@@ -416,13 +416,13 @@ export interface Site {
ua?: string ua?: string
// 是否使用代理 // 是否使用代理
proxy?: number proxy?: any
// 过滤规则 // 过滤规则
filter?: string filter?: string
// 是否演染 // 是否演染
render?: number render?: any
// 是否公开站点 // 是否公开站点
public?: number public?: number

View File

@@ -35,6 +35,11 @@ function filtersChanged(value: string[]) {
const selectFilterOptions = ref<{ [key: string]: string }[]>([ const selectFilterOptions = ref<{ [key: string]: string }[]>([
{ title: '特效字幕', value: ' SPECSUB ' }, { title: '特效字幕', value: ' SPECSUB ' },
{ title: '中文字幕', value: ' CNSUB ' }, { title: '中文字幕', value: ' CNSUB ' },
{ title: '国语配音', value: ' CNVOI ' },
{ title: '排除: 国语配音', value: ' !CNVOI ' },
{ title: '粤语配音', value: ' HKVOI ' },
{ title: '排除: 粤语配音', value: ' !HKVOI ' },
{ title: '促销: 免费', value: ' FREE ' },
{ title: '分辨率: 4K', value: ' 4K ' }, { title: '分辨率: 4K', value: ' 4K ' },
{ title: '分辨率: 1080P', value: ' 1080P ' }, { title: '分辨率: 1080P', value: ' 1080P ' },
{ title: '分辨率: 720P', value: ' 720P ' }, { title: '分辨率: 720P', value: ' 720P ' },
@@ -49,8 +54,8 @@ const selectFilterOptions = ref<{ [key: string]: string }[]>([
{ title: '排除: REMUX', value: ' !REMUX ' }, { title: '排除: REMUX', value: ' !REMUX ' },
{ title: '质量: WEB-DL', value: ' WEBDL ' }, { title: '质量: WEB-DL', value: ' WEBDL ' },
{ title: '排除: WEB-DL', value: ' !WEBDL ' }, { title: '排除: WEB-DL', value: ' !WEBDL ' },
{ title: '质量: 60fps', value: '60FPS' }, { title: '质量: 60fps', value: ' 60FPS ' },
{ title: '排除: 60fps', value: '!60FPS' }, { title: '排除: 60fps', value: ' !60FPS ' },
{ title: '编码: H265', value: ' H265 ' }, { title: '编码: H265', value: ' H265 ' },
{ title: '排除: H265', value: ' !H265 ' }, { title: '排除: H265', value: ' !H265 ' },
{ title: '编码: H264', value: ' H264 ' }, { title: '编码: H264', value: ' H264 ' },
@@ -61,9 +66,10 @@ const selectFilterOptions = ref<{ [key: string]: string }[]>([
{ title: '排除: 杜比全景声', value: ' !ATMOS ' }, { title: '排除: 杜比全景声', value: ' !ATMOS ' },
{ title: '效果: HDR', value: ' HDR ' }, { title: '效果: HDR', value: ' HDR ' },
{ title: '排除: HDR', value: ' !HDR ' }, { title: '排除: HDR', value: ' !HDR ' },
{ title: '国语配音', value: ' CNVOI ' }, { title: '效果: SDR', value: ' SDR ' },
{ title: '排除: 国语配音', value: ' !CNVOI ' }, { title: '排除: SDR', value: ' !SDR ' },
{ title: '促销: 免费', value: ' FREE ' }, { title: '效果: 3D', value: ' 3D ' },
{ title: '排除: 3D', value: ' !3D ' },
]) ])
</script> </script>

View File

@@ -1,6 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { PropType, Ref } from 'vue' import type { PropType, Ref } from 'vue'
import { useToast } from 'vue-toast-notification' import { useToast } from 'vue-toast-notification'
import SubscribeEditForm from '../form/SubscribeEditForm.vue'
import { formatSeason } from '@/@core/utils/formatters' import { formatSeason } from '@/@core/utils/formatters'
import api from '@/api' import api from '@/api'
import { doneNProgress, startNProgress } from '@/api/nprogress' import { doneNProgress, startNProgress } from '@/api/nprogress'
@@ -39,6 +40,12 @@ const seasonsNotExisted = ref<{ [key: number]: number }>({})
// 订阅季弹窗 // 订阅季弹窗
const subscribeSeasonDialog = ref(false) const subscribeSeasonDialog = ref(false)
// 订阅编辑弹窗
const subscribeEditDialog = ref(false)
// 订阅ID
const subscribeId = ref(0)
// 季详情 // 季详情
const seasonInfos = ref<TmdbSeason[]>([]) const seasonInfos = ref<TmdbSeason[]>([])
@@ -86,6 +93,7 @@ async function handleAddSubscribe() {
} }
else { else {
// 弹出季选择列表,支持多选 // 弹出季选择列表,支持多选
seasonsSelected.value = []
subscribeSeasonDialog.value = true subscribeSeasonDialog.value = true
} }
} }
@@ -136,6 +144,12 @@ async function addSubscribe(season = 0) {
result.message, result.message,
best_version, best_version,
) )
// 弹出订阅编辑弹窗
if (result.success && seasonsSelected.value.length <= 1) {
subscribeId.value = result.data.id
subscribeEditDialog.value = true
}
} }
catch (error) { catch (error) {
console.error(error) console.error(error)
@@ -156,9 +170,9 @@ function showSubscribeAddToast(result: boolean,
if (best_version > 0) if (best_version > 0)
subname = '洗版订阅' subname = '洗版订阅'
if (result) if (result && seasonsSelected.value.length > 1)
$toast.success(`${title} 添加${subname}成功!`) $toast.success(`${title} 添加${subname}成功!`)
else else if (!result)
$toast.error(`${title} 添加${subname}失败:${message}`) $toast.error(`${title} 添加${subname}失败:${message}`)
} }
@@ -480,7 +494,7 @@ function getYear(airDate: string) {
inset inset
scrollable scrollable
> >
<VCard> <VCard class="rounded-t">
<DialogCloseBtn @click="subscribeSeasonDialog = false" /> <DialogCloseBtn @click="subscribeSeasonDialog = false" />
<VCardTitle class="pe-10"> <VCardTitle class="pe-10">
订阅 - {{ props.media?.title }} 订阅 - {{ props.media?.title }}
@@ -557,6 +571,14 @@ function getYear(airDate: string) {
</div> </div>
</VCard> </VCard>
</VBottomSheet> </VBottomSheet>
<!-- 订阅编辑弹窗 -->
<SubscribeEditForm
v-model="subscribeEditDialog"
:subid="subscribeId"
@close="subscribeEditDialog = false"
@save="subscribeEditDialog = false"
@remove="() => { subscribeEditDialog = false; handleCheckSubscribe(); }"
/>
</template> </template>
<style lang="scss"> <style lang="scss">

View File

@@ -234,13 +234,15 @@ const dropdownItems = ref([
</VCardText> </VCardText>
</VCard> </VCard>
<!-- 插件配置页面 --> <!-- 插件配置页面 -->
<VDialog <VBottomSheet
v-model="pluginConfigDialog" v-model="pluginConfigDialog"
max-width="50rem"
scrollable scrollable
persistent inset
> >
<VCard :title="`${props.plugin?.plugin_name} - 配置`"> <VCard
:title="`${props.plugin?.plugin_name} - 配置`"
class="rounded-t"
>
<DialogCloseBtn @click="pluginConfigDialog = false" /> <DialogCloseBtn @click="pluginConfigDialog = false" />
<VCardText> <VCardText>
<FormRender <FormRender
@@ -255,21 +257,26 @@ const dropdownItems = ref([
查看详情 查看详情
</VBtn> </VBtn>
<VSpacer /> <VSpacer />
<VBtn @click="savePluginConf"> <VBtn
variant="tonal"
@click="savePluginConf"
>
保存 保存
</VBtn> </VBtn>
</VCardActions> </VCardActions>
</VCard> </VCard>
</VDialog> </VBottomSheet>
<!-- 插件详情页面 --> <!-- 插件详情页面 -->
<VDialog <VBottomSheet
v-model="pluginInfoDialog" v-model="pluginInfoDialog"
max-width="62.5rem"
scrollable scrollable
persistent inset
> >
<VCard :title="`${props.plugin?.plugin_name}`"> <VCard
:title="`${props.plugin?.plugin_name}`"
class="rounded-t"
>
<DialogCloseBtn @click="pluginInfoDialog = false" /> <DialogCloseBtn @click="pluginInfoDialog = false" />
<VCardText> <VCardText>
<PageRender <PageRender
@@ -279,16 +286,21 @@ const dropdownItems = ref([
/> />
</VCardText> </VCardText>
<VCardActions> <VCardActions>
<VBtn @click="showPluginConfig"> <VBtn
@click="showPluginConfig"
>
配置 配置
</VBtn> </VBtn>
<VSpacer /> <VSpacer />
<VBtn @click="pluginInfoDialog = false"> <VBtn
variant="tonal"
@click="pluginInfoDialog = false"
>
关闭 关闭
</VBtn> </VBtn>
</VCardActions> </VCardActions>
</VCard> </VCard>
</VDialog> </VBottomSheet>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -1,8 +1,9 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { PropType } from 'vue' import type { PropType } from 'vue'
import { useToast } from 'vue-toast-notification' import { useToast } from 'vue-toast-notification'
import SiteAddEditForm from '../form/SiteAddEditForm.vue'
import { formatFileSize } from '@core/utils/formatters' import { formatFileSize } from '@core/utils/formatters'
import { numberValidator, requiredValidator } from '@/@validators' import { requiredValidator } from '@/@validators'
import api from '@/api' import api from '@/api'
import type { Site, TorrentInfo } from '@/api/types' import type { Site, TorrentInfo } from '@/api/types'
import ExistIcon from '@core/components/ExistIcon.vue' 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) const isPasswordVisible = ref(false)
@@ -42,7 +43,7 @@ const updateButtonDisable = ref(false)
const siteCookieDialog = ref(false) const siteCookieDialog = ref(false)
// 站点编辑弹窗 // 站点编辑弹窗
const siteInfoDialog = ref(false) const siteEditDialog = ref(false)
// 资源浏览弹窗 // 资源浏览弹窗
const resourceDialog = ref(false) const resourceDialog = ref(false)
@@ -78,27 +79,6 @@ const userPwForm = ref({
password: '', 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) { function openTorrentDetail(page_url: string) {
window.open(page_url, '_blank') window.open(page_url, '_blank')
@@ -144,11 +124,6 @@ async function handleSiteUpdate() {
siteCookieDialog.value = true siteCookieDialog.value = true
} }
// 打开站点编辑弹窗
async function handleSiteInfo() {
siteInfoDialog.value = true
}
// 打开资源浏览弹窗 // 打开资源浏览弹窗
async function handleResourceBrowse() { async function handleResourceBrowse() {
resourceDialog.value = true 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类 // 促销Chip类
function getVolumeFactorClass(downloadVolume: number, uploadVolume: number) { function getVolumeFactorClass(downloadVolume: number, uploadVolume: number) {
if (downloadVolume === 0) if (downloadVolume === 0)
@@ -264,9 +203,9 @@ onMounted(() => {
<VCard <VCard
:height="cardProps.height" :height="cardProps.height"
:width="cardProps.width" :width="cardProps.width"
:flat="!siteForm.is_active" :flat="!cardProps.site?.is_active"
class="overflow-hidden" class="overflow-hidden"
@click="handleSiteInfo" @click="siteEditDialog = true"
> >
<template #image> <template #image>
<VAvatar <VAvatar
@@ -278,17 +217,19 @@ onMounted(() => {
</VAvatar> </VAvatar>
</template> </template>
<VCardItem> <VCardItem>
<VCardTitle class="font-bold" @click.stop="openSitePage"> <VCardTitle class="font-bold">
{{ cardProps.site?.name }} <span @click.stop="openSitePage">{{ cardProps.site?.name }}</span>
</VCardTitle> </VCardTitle>
<VCardSubtitle>{{ cardProps.site?.url }}</VCardSubtitle> <VCardSubtitle>
{{ cardProps.site?.url }}
</VCardSubtitle>
</VCardItem> </VCardItem>
<ExistIcon v-if="siteForm.is_active" /> <ExistIcon v-if="cardProps.site?.is_active" />
<VCardText class="py-2"> <VCardText class="py-2">
<VTooltip <VTooltip
v-if="siteForm.render" v-if="cardProps.site?.render === 1"
text="浏览器仿真" text="浏览器仿真"
> >
<template #activator="{ props }"> <template #activator="{ props }">
@@ -302,7 +243,7 @@ onMounted(() => {
</VTooltip> </VTooltip>
<VTooltip <VTooltip
v-if="siteForm.proxy" v-if="cardProps.site?.proxy === 1"
text="代理" text="代理"
> >
<template #activator="{ props }"> <template #activator="{ props }">
@@ -316,7 +257,7 @@ onMounted(() => {
</VTooltip> </VTooltip>
<VTooltip <VTooltip
v-if="siteForm.limit_interval" v-if="cardProps.site?.limit_interval"
text="流控" text="流控"
> >
<template #activator="{ props }"> <template #activator="{ props }">
@@ -330,7 +271,7 @@ onMounted(() => {
</VTooltip> </VTooltip>
<VTooltip <VTooltip
v-if="siteForm.filter" v-if="cardProps.site?.filter"
text="过滤" text="过滤"
> >
<template #activator="{ props }"> <template #activator="{ props }">
@@ -419,143 +360,22 @@ onMounted(() => {
<VCardActions> <VCardActions>
<VSpacer /> <VSpacer />
<VBtn @click="updateSiteCookie"> <VBtn
variant="tonal"
@click="updateSiteCookie"
>
开始更新 开始更新
</VBtn> </VBtn>
</VCardActions> </VCardActions>
</VCard> </VCard>
</VDialog> </VDialog>
<!-- 站点编辑弹窗 --> <SiteAddEditForm
<VDialog v-model="siteEditDialog"
v-model="siteInfoDialog" :siteid="cardProps.site?.id"
max-width="50rem" @save="siteEditDialog = false; emit('update')"
persistent @remove="emit('remove')"
scrollable @close="siteEditDialog = false"
> />
<!-- 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>
<!-- 站点资源弹窗 --> <!-- 站点资源弹窗 -->
<VDialog <VDialog
v-model="resourceDialog" v-model="resourceDialog"

View File

@@ -1,10 +1,10 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useToast } from 'vue-toast-notification' import { useToast } from 'vue-toast-notification'
import SubscribeEditForm from '../form/SubscribeEditForm.vue'
import { calculateTimeDifference } from '@/@core/utils' import { calculateTimeDifference } from '@/@core/utils'
import { formatSeason } from '@/@core/utils/formatters' import { formatSeason } from '@/@core/utils/formatters'
import { numberValidator } from '@/@validators'
import api from '@/api' import api from '@/api'
import type { Site, Subscribe } from '@/api/types' import type { Subscribe } from '@/api/types'
// 输入参数 // 输入参数
const props = defineProps({ const props = defineProps({
@@ -21,19 +21,7 @@ const $toast = useToast()
const imageLoaded = ref(false) const imageLoaded = ref(false)
// 订阅弹窗 // 订阅弹窗
const subscribeInfoDialog = ref(false) const subscribeEditDialog = 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 lastUpdateText = ref( 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() { async function editSubscribeDialog() {
await getSiteList() subscribeEditDialog.value = true
subscribeInfoDialog.value = true
} }
// 弹出菜单 // 弹出菜单
@@ -196,96 +135,12 @@ const dropdownItems = ref([
}, },
}, },
]) ])
// 质量选择框数据
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: '8K',
value: '8K|4320p|x4320',
},
{
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\\+',
},
])
</script> </script>
<template> <template>
<VCard <VCard
:key="props.media?.id" :key="props.media?.id"
:class="`${subscribeForm.best_version ? 'outline-dashed outline-1' : ''}`" :class="`${props.media?.best_version ? 'outline-dashed outline-1' : ''}`"
@click="editSubscribeDialog" @click="editSubscribeDialog"
> >
<template #image> <template #image>
@@ -407,133 +262,11 @@ const effectOptions = ref([
/> />
</VCard> </VCard>
<!-- 订阅编辑弹窗 --> <!-- 订阅编辑弹窗 -->
<VDialog <SubscribeEditForm
v-model="subscribeInfoDialog" v-model="subscribeEditDialog"
max-width="50rem" :subid="props.media?.id"
persistent @remove="() => { emit('remove');subscribeEditDialog = false; }"
scrollable @save="() => { emit('save');subscribeEditDialog = false; }"
> @close="subscribeEditDialog = false"
<!-- Dialog Content --> />
<VCard :title="`订阅 - ${props.media?.name}`">
<VCardText class="pt-2">
<DialogCloseBtn @click="subscribeInfoDialog = false" />
<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="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="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>
</template> </template>

View File

@@ -64,6 +64,9 @@ async function handleAddDownload(_site: any = undefined,
dialogProps: { dialogProps: {
maxWidth: '50rem', maxWidth: '50rem',
}, },
confirmationButtonProps: {
variant: 'tonal',
},
}) })
if (!isConfirmed) if (!isConfirmed)

View File

@@ -61,6 +61,9 @@ async function handleAddDownload(_site: any = undefined,
dialogProps: { dialogProps: {
maxWidth: '50rem', maxWidth: '50rem',
}, },
confirmationButtonProps: {
variant: 'tonal',
},
}) })
if (!isConfirmed) if (!isConfirmed)

View File

@@ -4,13 +4,12 @@ import type { PropType } from 'vue'
import { useConfirm } from 'vuetify-use-dialog' import { useConfirm } from 'vuetify-use-dialog'
import axios from 'axios' import axios from 'axios'
import { useToast } from 'vue-toast-notification' import { useToast } from 'vue-toast-notification'
import { numberValidator } from '@/@validators' import ReorganizeForm from '../form/ReorganizeForm.vue'
import { formatBytes } from '@core/utils/formatters' import { formatBytes } from '@core/utils/formatters'
import type { Context, EndPoints, FileItem } from '@/api/types' import type { Context, EndPoints, FileItem } from '@/api/types'
import store from '@/store' import store from '@/store'
import api from '@/api' import api from '@/api'
import MediaInfoCard from '@/components/cards/MediaInfoCard.vue' import MediaInfoCard from '@/components/cards/MediaInfoCard.vue'
import TmdbSelectorCard from '@/components/cards/TmdbSelectorCard.vue'
// 输入参数 // 输入参数
const inProps = defineProps({ const inProps = defineProps({
@@ -32,6 +31,15 @@ const $toast = useToast()
// 是否正在加载 // 是否正在加载
const loading = ref(true) const loading = ref(true)
// 识别进度条
const progressDialog = ref(false)
// 识别进度文本
const progressText = ref('请稍候 ...')
// 识别进度
const progressValue = ref(0)
// 确认框 // 确认框
const createConfirm = useConfirm() const createConfirm = useConfirm()
@@ -53,57 +61,18 @@ const renamePopper = ref(false)
// 整理弹窗 // 整理弹窗
const transferPopper = 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 newName = ref('')
// 当前名称 // 当前名称
const currentItem = ref<FileItem>() 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 nameTestResult = ref<Context>()
// 识别结果对话框 // 识别结果对话框
const nameTestDialog = ref(false) 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(() => const dirs = computed(() =>
items.value.filter(item => item.type === 'dir' && item.basename.includes(filter.value)), items.value.filter(item => item.type === 'dir' && item.basename.includes(filter.value)),
@@ -158,6 +127,9 @@ async function deleteItem(item: FileItem) {
dialogProps: { dialogProps: {
maxWidth: '50rem', maxWidth: '50rem',
}, },
cancellationButtonProps: {
variant: 'tonal',
},
}) })
if (confirmed) { if (confirmed) {
@@ -245,41 +217,6 @@ function showTransfer(item: FileItem) {
transferPopper.value = true 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转换为本地时间 // 将文件修改时间timestape转换为本地时间
function formatTime(timestape: number) { function formatTime(timestape: number) {
return new Date(timestape * 1000).toLocaleString() 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识别 // 调用API识别
async function recognize(path: string) { async function recognize(path: string) {
try { try {
@@ -586,23 +500,19 @@ onMounted(() => {
v-model="renamePopper" v-model="renamePopper"
max-width="50rem" max-width="50rem"
> >
<template #activator="{ props }">
<IconBtn title="重命名" v-bind="props">
<VIcon icon="mdi-rename-outline" />
</IconBtn>
</template>
<VCard title="重命名"> <VCard title="重命名">
<VCardText> <VCardText>
<VTextField v-model="newName" label="名称" /> <VTextField v-model="newName" label="名称" />
</VCardText> </VCardText>
<VCardActions> <VCardActions>
<div class="flex-grow-1" />
<VBtn depressed @click="renamePopper = false"> <VBtn depressed @click="renamePopper = false">
取消 取消
</VBtn> </VBtn>
<VSpacer />
<VBtn <VBtn
:disabled="!newName" :disabled="!newName"
depressed depressed
variant="tonal"
@click="rename" @click="rename"
> >
重命名 重命名
@@ -611,183 +521,44 @@ onMounted(() => {
</VCard> </VCard>
</VDialog> </VDialog>
<!-- 文件整理弹窗 --> <!-- 文件整理弹窗 -->
<VDialog <ReorganizeForm
v-model="transferPopper" v-model="transferPopper"
max-width="50rem" :path="currentItem?.path"
scrollable @done="transferPopper = false; load()"
> @close="transferPopper = false"
<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>
<!-- 手动整理进度框 --> <!-- 手动整理进度框 -->
<vDialog <VDialog
v-model="progressDialog" v-model="progressDialog"
:scrim="false" :scrim="false"
width="25rem" width="25rem"
> >
<vCard <VCard
color="primary" color="primary"
> >
<vCardText class="text-center"> <VCardText class="text-center">
{{ progressText }} {{ progressText }}
<vProgressLinear <VProgressLinear
v-if="progressValue" v-if="progressValue"
color="white" color="white"
class="mb-0 mt-1" class="mb-0 mt-1"
:model-value="progressValue" :model-value="progressValue"
/> />
</vCardText> </VCardText>
</vCard> </VCard>
</vDialog> </VDialog>
<!-- 识别结果对话框 --> <!-- 识别结果对话框 -->
<vDialog <VDialog
v-model="nameTestDialog" v-model="nameTestDialog"
width="50rem" width="50rem"
> >
<vCard> <VCard>
<DialogCloseBtn @click="nameTestDialog = false" /> <DialogCloseBtn @click="nameTestDialog = false" />
<VCardItem> <VCardItem>
<MediaInfoCard :context="nameTestResult" /> <MediaInfoCard :context="nameTestResult" />
</VCardItem> </VCardItem>
</vCard> </VCard>
</vDialog> </VDialog>
<!-- TMDB ID搜索框 -->
<vDialog
v-model="tmdbSelectorDialog"
width="40rem"
scrollable
>
<TmdbSelectorCard
v-model="transferForm.tmdbid"
@close="tmdbSelectorDialog = false"
/>
</vDialog>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -171,6 +171,7 @@ const sortIcon = computed(() => {
<VBtn <VBtn
:disabled="!newFolderName" :disabled="!newFolderName"
depressed depressed
variant="tonal"
@click="mkdir" @click="mkdir"
> >
新建 新建

View File

@@ -0,0 +1,309 @@
<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(() => {
if (props.path)
transferForm.path = props.path
if (props.target)
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>
<VBottomSheet
scrollable
inset
>
<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>
</VBottomSheet>
</template>

View 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>
<VBottomSheet
inset
scrollable
>
<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>
</VBottomSheet>
</template>

View 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>
<VBottomSheet
inset
scrollable
>
<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>
</VBottomSheet>
</template>

View File

@@ -63,6 +63,7 @@ function openSearchDialog() {
<VCardActions> <VCardActions>
<VSpacer /> <VSpacer />
<VBtn <VBtn
variant="tonal"
@click="search" @click="search"
> >
搜索 搜索

View File

@@ -38,6 +38,9 @@ async function restart() {
dialogProps: { dialogProps: {
maxWidth: '30rem', maxWidth: '30rem',
}, },
cancellationButtonProps: {
variant: 'tonal',
},
}) })
if (confirmed) { if (confirmed) {
@@ -170,21 +173,21 @@ const avatar = store.state.auth.avatar
<!-- !SECTION --> <!-- !SECTION -->
</VAvatar> </VAvatar>
<!-- 重启进度框 --> <!-- 重启进度框 -->
<vDialog <VDialog
v-model="progressDialog" v-model="progressDialog"
width="25rem" width="25rem"
> >
<vCard <VCard
color="primary" color="primary"
> >
<vCardText class="text-center"> <VCardText class="text-center">
正在重启 ... 正在重启 ...
<vProgressLinear <VProgressLinear
indeterminate indeterminate
color="white" color="white"
class="mb-0 mt-1" class="mb-0 mt-1"
/> />
</vCardText> </VCardText>
</vCard> </VCard>
</vDialog> </VDialog>
</template> </template>

View File

@@ -2,7 +2,6 @@
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import AccountSettingAccount from '@/views/setting/AccountSettingAccount.vue' import AccountSettingAccount from '@/views/setting/AccountSettingAccount.vue'
import AccountSettingNotification from '@/views/setting/AccountSettingNotification.vue' import AccountSettingNotification from '@/views/setting/AccountSettingNotification.vue'
import AccountSettingRule from '@/views/setting/AccountSettingRule.vue'
import AccountSettingSite from '@/views/setting/AccountSettingSite.vue' import AccountSettingSite from '@/views/setting/AccountSettingSite.vue'
import AccountSettingWords from '@/views/setting/AccountSettingWords.vue' import AccountSettingWords from '@/views/setting/AccountSettingWords.vue'
import AccountSettingAbout from '@/views/setting/AccountSettingAbout.vue' import AccountSettingAbout from '@/views/setting/AccountSettingAbout.vue'
@@ -41,11 +40,6 @@ const tabs = [
icon: 'mdi-list-box', icon: 'mdi-list-box',
tab: 'service', tab: 'service',
}, },
{
title: '规则',
icon: 'mdi-filter-cog',
tab: 'filter',
},
{ {
title: '通知', title: '通知',
icon: 'mdi-bell', icon: 'mdi-bell',
@@ -117,13 +111,6 @@ const tabs = [
</transition> </transition>
</VWindowItem> </VWindowItem>
<!-- 规则 -->
<VWindowItem value="filter">
<transition name="fade-slide" appear>
<AccountSettingRule />
</transition>
</VWindowItem>
<!-- 通知 --> <!-- 通知 -->
<VWindowItem value="notification"> <VWindowItem value="notification">
<transition name="fade-slide" appear> <transition name="fade-slide" appear>

View File

@@ -8,6 +8,7 @@ import NoDataFound from '@/components/NoDataFound.vue'
import { doneNProgress, startNProgress } from '@/api/nprogress' import { doneNProgress, startNProgress } from '@/api/nprogress'
import { formatSeason } from '@/@core/utils/formatters' import { formatSeason } from '@/@core/utils/formatters'
import router from '@/router' import router from '@/router'
import SubscribeEditForm from '@/components/form/SubscribeEditForm.vue'
// 输入参数 // 输入参数
const mediaProps = defineProps({ const mediaProps = defineProps({
@@ -21,6 +22,9 @@ const $toast = useToast()
// 媒体详情 // 媒体详情
const mediaDetail = ref<MediaInfo>({} as MediaInfo) const mediaDetail = ref<MediaInfo>({} as MediaInfo)
// 订阅编辑弹窗
const subscribeEditDialog = ref(false)
// 本地是否存在 // 本地是否存在
const isExists = ref(false) const isExists = ref(false)
@@ -39,6 +43,9 @@ const seasonsNotExisted = ref<{ [key: number]: number }>({})
// 各季的订阅状态 // 各季的订阅状态
const seasonsSubscribed = ref<{ [key: number]: boolean }>({}) const seasonsSubscribed = ref<{ [key: number]: boolean }>({})
// 订阅编号
const subscribeId = ref(0)
// 调用API查询详情 // 调用API查询详情
async function getMediaDetail() { async function getMediaDetail() {
if (mediaProps.mediaid && mediaProps.type) { if (mediaProps.mediaid && mediaProps.type) {
@@ -211,6 +218,12 @@ async function addSubscribe(season = 0) {
result.message, result.message,
best_version, best_version,
) )
// 显示编辑弹窗
if (result.success) {
subscribeId.value = result.data.id
subscribeEditDialog.value = true
}
} }
catch (error) { catch (error) {
console.error(error) console.error(error)
@@ -231,9 +244,7 @@ function showSubscribeAddToast(result: boolean,
if (best_version > 0) if (best_version > 0)
subname = '洗版订阅' subname = '洗版订阅'
if (result) if (!result)
$toast.success(`${title} 添加${subname}成功!`)
else
$toast.error(`${title} 添加${subname}失败:${message}`) $toast.error(`${title} 添加${subname}失败:${message}`)
} }
@@ -684,6 +695,20 @@ onBeforeMount(() => {
error-title="出错啦" error-title="出错啦"
error-description="未识别到TMDB媒体信息" 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> </template>
<style lang="scss"> <style lang="scss">

View File

@@ -169,7 +169,7 @@ onMounted(() => {
> >
{{ resolution }} {{ resolution }}
</VChip> </VChip>
</vchipgroup> </VChipGroup>
</VListItem> </VListItem>
<VListSubheader v-if="releaseGroupFilterOptions.length > 0"> <VListSubheader v-if="releaseGroupFilterOptions.length > 0">
制作组 制作组
@@ -186,7 +186,7 @@ onMounted(() => {
> >
{{ releaseGroup }} {{ releaseGroup }}
</VChip> </VChip>
</vchipgroup> </VChipGroup>
</VListItem> </VListItem>
<VListSubheader v-if="videoCodeFilterOptions.length > 0"> <VListSubheader v-if="videoCodeFilterOptions.length > 0">
视频编码 视频编码
@@ -203,7 +203,7 @@ onMounted(() => {
> >
{{ videoCode }} {{ videoCode }}
</VChip> </VChip>
</vchipgroup> </VChipGroup>
</VListItem> </VListItem>
<VListSubheader v-if="freeStateFilterOptions.length > 0"> <VListSubheader v-if="freeStateFilterOptions.length > 0">
促销状态 促销状态
@@ -220,7 +220,7 @@ onMounted(() => {
> >
{{ freeState }} {{ freeState }}
</VChip> </VChip>
</vchipgroup> </VChipGroup>
</VListItem> </VListItem>
<VListSubheader v-if="seasonFilterOptions.length > 0"> <VListSubheader v-if="seasonFilterOptions.length > 0">
季集 季集

View File

@@ -1,10 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref } from 'vue'
import { useToast } from 'vue-toast-notification' import { useToast } from 'vue-toast-notification'
import { numberValidator } from '@/@validators'
import api from '@/api' import api from '@/api'
import type { TransferHistory } from '@/api/types' import type { TransferHistory } from '@/api/types'
import TmdbSelectorCard from '@/components/cards/TmdbSelectorCard.vue' import ReorganizeForm from '@/components/form/ReorganizeForm.vue'
// 提示框 // 提示框
const $toast = useToast() const $toast = useToast()
@@ -12,22 +11,15 @@ const $toast = useToast()
// 重新整理对话框 // 重新整理对话框
const redoDialog = ref(false) const redoDialog = ref(false)
// TMDB编号
const redoTmdbId = ref('')
// 类型
const redoType = ref('电影')
// 类型下拉框:电影、电视剧
const redoTypeItems = ref([
{ title: '自动', value: '' },
{ title: '电影', value: '电影' },
{ title: '电视剧', value: '电视剧' },
])
// 当前操作记录 // 当前操作记录
const currentHistory = ref<TransferHistory>() const currentHistory = ref<TransferHistory>()
// 重新整理IDS
const redoIds = ref<number[]>([])
// 重新整理target
const redoTarget = ref('')
// 已选中的数据 // 已选中的数据
const selected = ref<TransferHistory[]>([]) const selected = ref<TransferHistory[]>([])
@@ -69,9 +61,6 @@ const progressText = ref('请稍候 ...')
// 进度值 // 进度值
const progressValue = ref(0) const progressValue = ref(0)
// TMDB选择对话框
const tmdbSelectorDialog = ref(false)
// 删除确认对话框 // 删除确认对话框
const deleteConfirmDialog = ref(false) const deleteConfirmDialog = ref(false)
@@ -227,85 +216,17 @@ async function retransferBatch() {
return return
// 清空当前操作记录 // 清空当前操作记录
currentHistory.value = undefined 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 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([ const dropdownItems = ref([
{ {
@@ -314,10 +235,9 @@ const dropdownItems = ref([
props: { props: {
prependIcon: 'mdi-redo-variant', prependIcon: 'mdi-redo-variant',
click: (item: TransferHistory) => { click: (item: TransferHistory) => {
redoTmdbId.value = '' redoIds.value = [item.id]
redoType.value = '' redoTarget.value = item.dest ?? ''
redoDialog.value = true redoDialog.value = true
currentHistory.value = item
}, },
}, },
}, },
@@ -444,47 +364,11 @@ const dropdownItems = ref([
</template> </template>
</VDataTableServer> </VDataTableServer>
</VCard> </VCard>
<VDialog <!-- 底部操作按钮 -->
v-model="redoDialog" <span
max-width="50rem" 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 <VBtn
icon="mdi-redo-variant" icon="mdi-redo-variant"
class="me-2" class="me-2"
@@ -499,39 +383,9 @@ const dropdownItems = ref([
@click="removeHistoryBatch" @click="removeHistoryBatch"
/> />
</span> </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> <VBottomSheet v-model="deleteConfirmDialog" inset>
<VCard class="text-center"> <VCard class="text-center rounded-t">
<DialogCloseBtn @click="deleteConfirmDialog = false" /> <DialogCloseBtn @click="deleteConfirmDialog = false" />
<VCardTitle class="pe-10"> <VCardTitle class="pe-10">
{{ confirmTitle }} {{ confirmTitle }}
@@ -568,6 +422,24 @@ const dropdownItems = ref([
</div> </div>
</VCard> </VCard>
</VBottomSheet> </VBottomSheet>
<!-- 文件整理弹窗 -->
<ReorganizeForm
v-model="redoDialog"
:logids="redoIds"
:target="redoTarget"
@done="() => {
redoDialog = false
// 清空当前操作记录
currentHistory = undefined
selected = []
// 刷新
fetchData({
page: currentPage,
itemsPerPage,
})
}"
@close="redoDialog = false"
/>
</template> </template>
<style lang="scss"> <style lang="scss">

View File

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

View File

@@ -24,6 +24,12 @@ const allSites = ref<Site[]>([])
// 选中订阅站点 // 选中订阅站点
const selectedSites = ref<number[]>([]) const selectedSites = ref<number[]>([])
// 包含与排除规则
const defaultFilterRules = ref({
include: '',
exclude: '',
})
// 查询已设置优先级规则 // 查询已设置优先级规则
async function queryCustomFilters() { async function queryCustomFilters() {
try { try {
@@ -190,9 +196,41 @@ function onLevelDown(pri: string) {
filterCards.value.sort((a, b) => parseInt(a.pri) - parseInt(b.pri)) 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(() => { onMounted(() => {
queryCustomFilters() queryCustomFilters()
querySites() querySites()
queryDefaultFilter()
}) })
</script> </script>
@@ -260,6 +298,39 @@ onMounted(() => {
</VCardItem> </VCardItem>
</VCard> </VCard>
</VCol> </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> </VRow>
</template> </template>

View File

@@ -14,6 +14,15 @@ const resetSitesText = ref('重置站点数据')
// 站点重置按钮可用状态 // 站点重置按钮可用状态
const resetSitesDisabled = ref(false) const resetSitesDisabled = ref(false)
// 种子优先规则
const selectedTorrentPriority = ref<string>('seeder')
// 种子优先规则下拉框
const TorrentPriorityItems = [
{ title: '站点优先', value: 'site' },
{ title: '做种数优先', value: 'seeder' },
]
// 重置站点 // 重置站点
async function resetSites() { async function resetSites() {
try { try {
@@ -34,10 +43,74 @@ async function resetSites() {
console.log(error) 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> </script>
<template> <template>
<VRow> <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"> <VCol cols="12">
<VCard title="站点重置"> <VCard title="站点重置">
<VCardText> <VCardText>

View File

@@ -27,6 +27,12 @@ const allSites = ref<Site[]>([])
// 选中订阅站点 // 选中订阅站点
const selectedRssSites = ref<number[]>([]) const selectedRssSites = ref<number[]>([])
// 包含与排除规则
const defaultFilterRules = ref({
include: '',
exclude: '',
})
// 查询用户选中的订阅站点 // 查询用户选中的订阅站点
async function querySelectedRssSites() { async function querySelectedRssSites() {
try { try {
@@ -207,10 +213,42 @@ function onLevelDown(filterCards: FilterCard[], pri: string) {
filterCards.sort((a, b) => parseInt(a.pri) - parseInt(b.pri)) 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(() => { onMounted(() => {
querySites() querySites()
queryCustomFilters('SubscribeFilterRules') queryCustomFilters('SubscribeFilterRules')
queryCustomFilters('BestVersionFilterRules') queryCustomFilters('BestVersionFilterRules')
queryDefaultFilter()
}) })
</script> </script>
@@ -314,6 +352,39 @@ onMounted(() => {
</VCardItem> </VCardItem>
</VCard> </VCard>
</VCol> </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> </VRow>
</template> </template>

View File

@@ -1,14 +1,9 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useToast } from 'vue-toast-notification'
import api from '@/api' import api from '@/api'
import type { Site } from '@/api/types' import type { Site } from '@/api/types'
import SiteCard from '@/components/cards/SiteCard.vue' import SiteCard from '@/components/cards/SiteCard.vue'
import NoDataFound from '@/components/NoDataFound.vue' import NoDataFound from '@/components/NoDataFound.vue'
import { numberValidator, requiredValidator } from '@/@validators' import SiteAddEditForm from '@/components/form/SiteAddEditForm.vue'
import { doneNProgress, startNProgress } from '@/api/nprogress'
// 提示框
const $toast = useToast()
// 数据列表 // 数据列表
const dataList = ref<Site[]>([]) const dataList = ref<Site[]>([])
@@ -16,45 +11,9 @@ const dataList = ref<Site[]>([])
// 是否刷新过 // 是否刷新过
const isRefreshed = ref(false) const isRefreshed = ref(false)
// 新增按钮文本
const addBtnText = ref('新增站点')
// 新增按钮状态
const addBtnState = ref(false)
// 新增站点对话框 // 新增站点对话框
const siteAddDialog = 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() { async function fetchData() {
try { 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) onBeforeMount(fetchData)
</script> </script>
@@ -132,150 +59,20 @@ onBeforeMount(fetchData)
error-title="没有站点" error-title="没有站点"
error-description="已添加并支持的站点将会在这里显示" 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" v-model="siteAddDialog"
max-width="50rem" oper="add"
persistent @save="siteAddDialog = false; fetchData()"
scrollable @close="siteAddDialog = false"
> />
<!-- 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>
</template> </template>
<style lang="scss"> <style lang="scss">