Compare commits

..

40 Commits

Author SHA1 Message Date
jxxghp
43b1f7e620 v1.3.4-2 2023-10-17 14:04:29 +08:00
jxxghp
ba76f79d85 fix ui 2023-10-17 14:02:54 +08:00
jxxghp
ce47afa698 fix ui 2023-10-16 19:56:49 +08:00
jxxghp
6da110948c v1.3.4 2023-10-16 17:35:23 +08:00
jxxghp
533c564db5 feat 普通用户下载中只显示自己添加的下载 2023-10-16 08:32:02 +08:00
jxxghp
4a65056909 fix 普通用户订阅权限 2023-10-16 08:23:38 +08:00
jxxghp
c52ad73101 fix dialogs 2023-10-16 06:58:09 +08:00
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
jxxghp
c94d5f7e7d fix bug 2023-10-12 22:45:59 +08:00
jxxghp
e34f18799f fix 2023-10-12 22:25:15 +08:00
jxxghp
1681a311f7 fix 2023-10-12 21:44:48 +08:00
jxxghp
da08d8ec19 fix ui 2023-10-12 21:36:08 +08:00
jxxghp
730178c838 fix 2023-10-12 20:02:24 +08:00
jxxghp
a04450ae98 feat 60fps 2023-10-12 17:06:37 +08:00
jxxghp
2b2fd66a29 fix bug 2023-10-12 16:31:17 +08:00
jxxghp
58fe08ad3d build ui 2023-10-12 16:30:27 +08:00
jxxghp
240d6bede0 fix ui 2023-10-12 16:26:05 +08:00
jxxghp
23d808f8b1 feat 资源页面视图切换 2023-10-12 16:09:15 +08:00
jxxghp
2f293706cb fix 2023-10-12 09:46:04 +08:00
jxxghp
9aaaf0c520 feat 更多订阅设置项 2023-10-12 09:43:55 +08:00
jxxghp
6694e7e929 feat 搜索框聚焦、发现页缓存 2023-10-12 08:13:05 +08:00
jxxghp
d3768cb994 feat switch view button 2023-10-11 21:30:59 +08:00
jxxghp
c59d3e28b9 feat torrents page 2023-10-11 21:21:49 +08:00
jxxghp
914239f434 feat 热门动漫 2023-10-11 16:12:38 +08:00
34 changed files with 2165 additions and 1435 deletions

View File

@@ -1,6 +1,6 @@
{ {
"name": "moviepilot", "name": "moviepilot",
"version": "1.3.1", "version": "1.3.4-2",
"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

@@ -44,6 +44,15 @@ export interface Subscribe {
// 排除 // 排除
exclude?: string exclude?: string
// 质量
quality?: string
// 分辨率
resolution?: string
// 特效
effect?: string
// 总集数 // 总集数
total_episode?: number total_episode?: number
@@ -68,8 +77,8 @@ export interface Subscribe {
// 订阅站点 // 订阅站点
sites: number[] sites: number[]
// 是否洗版 // 是否洗版数字或者boolean
best_version: number best_version: any
// 当前优先级 // 当前优先级
current_priority: number current_priority: number
@@ -407,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
@@ -469,6 +478,9 @@ export interface DownloadingInfo {
// 媒体信息 // 媒体信息
media: { [key: string]: any } media: { [key: string]: any }
// 下载用户
userid?: string
} }
// 缺失剧集信息 // 缺失剧集信息

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,17 +54,22 @@ 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: '编码: H265', value: ' H265 ' }, { title: '编码: H265', value: ' H265 ' },
{ title: '排除: H265', value: ' !H265 ' }, { title: '排除: H265', value: ' !H265 ' },
{ title: '编码: H264', value: ' H264 ' }, { title: '编码: H264', value: ' H264 ' },
{ title: '排除: H264', value: ' !H264 ' }, { title: '排除: H264', value: ' !H264 ' },
{ title: '效果: 杜比视界', value: ' DOLBY ' }, { title: '效果: 杜比视界', value: ' DOLBY ' },
{ title: '排除: 杜比视界', value: ' !DOLBY ' }, { title: '排除: 杜比视界', value: ' !DOLBY ' },
{ 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

@@ -236,11 +236,13 @@ const dropdownItems = ref([
<!-- 插件配置页面 --> <!-- 插件配置页面 -->
<VDialog <VDialog
v-model="pluginConfigDialog" v-model="pluginConfigDialog"
max-width="50rem"
scrollable scrollable
persistent max-width="60rem"
> >
<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,7 +257,10 @@ const dropdownItems = ref([
查看详情 查看详情
</VBtn> </VBtn>
<VSpacer /> <VSpacer />
<VBtn @click="savePluginConf"> <VBtn
variant="tonal"
@click="savePluginConf"
>
保存 保存
</VBtn> </VBtn>
</VCardActions> </VCardActions>
@@ -265,11 +270,13 @@ const dropdownItems = ref([
<!-- 插件详情页面 --> <!-- 插件详情页面 -->
<VDialog <VDialog
v-model="pluginInfoDialog" v-model="pluginInfoDialog"
max-width="62.5rem"
scrollable scrollable
persistent max-width="80rem"
> >
<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,11 +286,16 @@ 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>

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"
@@ -654,7 +474,7 @@ onMounted(() => {
<template #prepend> <template #prepend>
<VIcon icon="mdi-download" /> <VIcon icon="mdi-download" />
</template> </template>
<VListItemTitle>下载种子</VListItemTitle> <VListItemTitle>下载种子文件</VListItemTitle>
</VListItem> </VListItem>
</VList> </VList>
</VMenu> </VMenu>

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
} }
// 弹出菜单 // 弹出菜单
@@ -201,7 +140,7 @@ const dropdownItems = ref([
<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>
@@ -323,100 +262,11 @@ const dropdownItems = 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">
<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>
</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)
@@ -176,7 +179,7 @@ onMounted(() => {
<template #prepend> <template #prepend>
<VIcon icon="mdi-download" /> <VIcon icon="mdi-download" />
</template> </template>
<VListItemTitle>下载种子</VListItemTitle> <VListItemTitle>下载种子文件</VListItemTitle>
</VListItem> </VListItem>
</VList> </VList>
</VMenu> </VMenu>

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

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

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

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

View File

@@ -91,7 +91,6 @@ const superUser = store.state.auth.superUser
}" }"
/> />
<VerticalNavLink <VerticalNavLink
v-if="superUser"
:item="{ :item="{
title: '电影', title: '电影',
icon: 'mdi-movie-check-outline', icon: 'mdi-movie-check-outline',
@@ -99,7 +98,6 @@ const superUser = store.state.auth.superUser
}" }"
/> />
<VerticalNavLink <VerticalNavLink
v-if="superUser"
:item="{ :item="{
title: '电视剧', title: '电视剧',
icon: 'mdi-television-classic', icon: 'mdi-television-classic',

View File

@@ -8,6 +8,9 @@ const searchWord = ref<string>('')
// 搜索弹窗 // 搜索弹窗
const searchDialog = ref(false) const searchDialog = ref(false)
// ref
const searchWordInput = ref<HTMLElement | null>(null)
// Search // Search
function search() { function search() {
if (!searchWord.value) if (!searchWord.value)
@@ -21,6 +24,14 @@ function search() {
}, },
}) })
} }
// 打开搜索弹窗
function openSearchDialog() {
searchDialog.value = true
nextTick(() => {
searchWordInput.value?.focus()
})
}
</script> </script>
<template> <template>
@@ -34,23 +45,16 @@ function search() {
max-width="50rem" max-width="50rem"
transition="dialog-top-transition" 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 --> <!-- Dialog Content -->
<VCard title="搜索"> <VCard title="搜索">
<VCardText> <VCardText>
<VRow> <VRow>
<VCol cols="12"> <VCol cols="12">
<VTextField <VTextField
ref="searchWordInput"
v-model="searchWord" v-model="searchWord"
label="电影、电视剧名称" label="电影、电视剧名称"
@keydown.enter="search"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -59,8 +63,8 @@ function search() {
<VCardActions> <VCardActions>
<VSpacer /> <VSpacer />
<VBtn <VBtn
variant="tonal"
@click="search" @click="search"
@keydown.enter="search"
> >
搜索 搜索
</VBtn> </VBtn>
@@ -68,7 +72,13 @@ function search() {
</VCard> </VCard>
</VDialog> </VDialog>
</div> </div>
<!-- 👉 Search Icon -->
<IconBtn
class="d-lg-none"
@click="openSearchDialog"
>
<VIcon icon="mdi-magnify" />
</IconBtn>
<!-- 👉 Search Textfield --> <!-- 👉 Search Textfield -->
<span class="w-1/5"> <span class="w-1/5">
<VTextField <VTextField

View File

@@ -38,6 +38,9 @@ async function restart() {
dialogProps: { dialogProps: {
maxWidth: '30rem', maxWidth: '30rem',
}, },
cancellationButtonProps: {
variant: 'tonal',
},
}) })
if (confirmed) { if (confirmed) {
@@ -121,6 +124,25 @@ const avatar = store.state.auth.avatar
<VListItemTitle>设定</VListItemTitle> <VListItemTitle>设定</VListItemTitle>
</VListItem> </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 --> <!-- 👉 FAQ -->
<VListItem <VListItem
href="https://github.com/jxxghp/MoviePilot/blob/main/README.md" href="https://github.com/jxxghp/MoviePilot/blob/main/README.md"
@@ -137,22 +159,6 @@ const avatar = store.state.auth.avatar
<VListItemTitle>帮助</VListItemTitle> <VListItemTitle>帮助</VListItemTitle>
</VListItem> </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 --> <!-- 👉 Logout -->
<VListItem @click="logout"> <VListItem @click="logout">
<template #prepend> <template #prepend>
@@ -170,21 +176,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

@@ -28,6 +28,12 @@ import MediaCardSlideView from '@/views/discover/MediaCardSlideView.vue'
title="热门电视剧" title="热门电视剧"
/> />
<MediaCardSlideView
apipath="douban/tv_animation"
linkurl="/browse/douban/tv_animation?title=热门动漫"
title="热门动漫"
/>
<MediaCardSlideView <MediaCardSlideView
apipath="douban/movies" apipath="douban/movies"
linkurl="/browse/douban/movies?title=最新电影" linkurl="/browse/douban/movies?title=最新电影"

View File

@@ -1,5 +1,9 @@
<script setup lang="ts"> <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 TorrentCardListView from '@/views/discover/TorrentCardListView.vue'
import TorrentRowListView from '@/views/discover/TorrentRowListView.vue'
// 路由参数 // 路由参数
const route = useRoute() const route = useRoute()
@@ -12,14 +16,130 @@ const type = route.query?.type?.toString() ?? ''
// 搜索字段 // 搜索字段
const area = route.query?.area?.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> </script>
<template> <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 <TorrentCardListView
:keyword="keyword" v-else
:type="type" :items="dataList"
:area="area"
/> />
</div> </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> </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

@@ -103,6 +103,7 @@ const router = createRouter({
component: () => import('../pages/browse.vue'), component: () => import('../pages/browse.vue'),
props: true, props: true,
meta: { meta: {
keepAlive: true,
requiresAuth: true, requiresAuth: true,
}, },
}, },
@@ -111,6 +112,7 @@ const router = createRouter({
component: () => import('../pages/credits.vue'), component: () => import('../pages/credits.vue'),
props: true, props: true,
meta: { meta: {
keepAlive: true,
requiresAuth: true, requiresAuth: true,
}, },
}, },

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

@@ -1,66 +1,33 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue' import { ref } from 'vue'
import _ from 'lodash' import _ from 'lodash'
import api from '@/api'
import type { Context } from '@/api/types' import type { Context } from '@/api/types'
import TorrentCard from '@/components/cards/TorrentCard.vue' 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 { interface SearchTorrent extends Context {
more?: Array<Context> more?: Array<Context>
} }
// 数据列表 // 定义输入参
const dataList = ref <Array<SearchTorrent>>([]) const props = defineProps({
// 数据列表
// 分组后的数据列表 items: Array as PropType<SearchTorrent[]>,
const groupedDataList = ref<Map<string, Context[]>>() })
// 是否刷新过
const isRefreshed = ref(false)
// 加载进度文本
const progressText = ref('')
// 加载进度
const progressValue = ref(0)
// 加载进度SSE
const progressEventSource = ref<EventSource>()
// 过滤表单 // 过滤表单
const filterForm = reactive({ const filterForm = reactive({
// 站点 // 站点
site: [] as string[], site: [] as string[],
// 季 // 季
season: [] as string[], season: [] as string[],
// 制作组 // 制作组
releaseGroup: [] as string[], releaseGroup: [] as string[],
// 视频编码 // 视频编码
videoCode: [] as string[], videoCode: [] as string[],
// 促销状态 // 促销状态
freeState: [] as string[], freeState: [] as string[],
// 质量 // 质量
edition: [] as string[], edition: [] as string[],
// 分辨率 // 分辨率
resolution: [] as string[], resolution: [] as string[],
}) })
@@ -80,110 +47,13 @@ const editionFilterOptions = ref<Array<string>>([])
// 获取分辨率过滤选项 // 获取分辨率过滤选项
const resolutionFilterOptions = ref<Array<string>>([]) const resolutionFilterOptions = ref<Array<string>>([])
// 按过滤项过滤卡片 // 数据列表
watchEffect(() => { const dataList = ref <Array<SearchTorrent>>([])
// 清空数据
dataList.value.splice(0)
const match = (filter: Array<string>, value: string | undefined) => // 分组后的数据列表
filter.length === 0 || (value && filter.includes(value)) const groupedDataList = ref<Map<string, Context[]>>()
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
})
}
// 初始化过滤选项
function initOptions(data: Context) { function initOptions(data: Context) {
const { torrent_info, meta_info } = data const { torrent_info, meta_info } = data
const optionValue = (options: Array<string>, value: string | undefined) => { const optionValue = (options: Array<string>, value: string | undefined) => {
@@ -198,31 +68,69 @@ function initOptions(data: Context) {
optionValue(resolutionFilterOptions.value, meta_info?.resource_pix) optionValue(resolutionFilterOptions.value, meta_info?.resource_pix)
} }
// 使用SSE监听加载进度 // 计算分组后的列表
function startLoadingProgress() { watchEffect(() => {
progressText.value = '正在搜索,请稍候...' // 数据分组
const groupMap = new Map<string, Context[]>()
const token = store.state.auth.token // 遍历数据
props.items?.forEach((item) => {
progressEventSource.value = new EventSource( const { torrent_info } = item
`${import.meta.env.VITE_API_BASE_URL}system/progress/search?token=${token}`, // init options
) initOptions(item)
progressEventSource.value.onmessage = (event) => { // group data
const progress = JSON.parse(event.data) const key = `${torrent_info.title}_${torrent_info.size}`
if (progress) { if (groupMap.has(key)) {
progressText.value = progress.text // 已存在相同标题和大小的分组,将当前上下文信息添加到分组中
progressValue.value = progress.value const group = groupMap.get(key)
group?.push(item)
} }
} else {
} // 创建新的分组,并将当前上下文信息添加到分组中
groupMap.set(key, [item])
}
})
groupedDataList.value = groupMap
})
// 停止监听加载进度 // 计算过滤后的列表
function stopLoadingProgress() { watchEffect(() => {
progressEventSource.value?.close() // 清空列表
} dataList.value.splice(0)
// 匹配过滤函数
const match = (filter: Array<string>, value: string | undefined) =>
filter.length === 0 || (value && filter.includes(value))
// 加载时获取数据 groupedDataList.value?.forEach((value) => {
onMounted(initData) 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> </script>
<template> <template>
@@ -307,20 +215,14 @@ onMounted(initData)
</VCol> </VCol>
</VRow> </VRow>
</VCard> </VCard>
<div v-if="!isRefreshed" class="mt-12 w-full text-center text-gray-500 text-sm flex flex-col items-center"> <div class="grid gap-3 grid-torrent-card items-start">
<VProgressCircular v-if="!props.keyword" size="48" indeterminate color="primary" /> <TorrentCard
<VProgressCircular v-if="props.keyword" class="mb-3" color="primary" :model-value="progressValue" size="64" /> v-for="(item, index) in dataList"
<span>{{ progressText }}</span> :key="`${index}_${item.torrent_info.title}_${item.torrent_info.site}`"
:torrent="item"
:more="item.more"
/>
</div> </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> </template>
<style lang="scss"> <style lang="scss">

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

View File

@@ -4,6 +4,11 @@ import api from '@/api'
import type { DownloadingInfo } from '@/api/types' import type { DownloadingInfo } from '@/api/types'
import NoDataFound from '@/components/NoDataFound.vue' import NoDataFound from '@/components/NoDataFound.vue'
import DownloadingCard from '@/components/cards/DownloadingCard.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 let refreshTimer: NodeJS.Timer | null = null
@@ -35,6 +40,14 @@ function onRefresh() {
loading.value = false loading.value = false
} }
// 过滤数据,管理员用户显示全部,非管理员只显示自己的订阅
const filteredDataList = computed(() => {
if (superUser)
return dataList.value
else
return dataList.value.filter(data => data.userid === userName)
})
// 加载时获取数据 // 加载时获取数据
onBeforeMount(() => { onBeforeMount(() => {
fetchData() fetchData()
@@ -71,17 +84,17 @@ onUnmounted(() => {
@refresh="onRefresh" @refresh="onRefresh"
> >
<div <div
v-if="dataList.length > 0" v-if="filteredDataList.length > 0"
class="grid gap-3 grid-downloading-card" class="grid gap-3 grid-downloading-card"
> >
<DownloadingCard <DownloadingCard
v-for="data in dataList" v-for="data in filteredDataList"
:key="data.hash" :key="data.hash"
:info="data" :info="data"
/> />
</div> </div>
<NoDataFound <NoDataFound
v-if="dataList.length === 0 && isRefreshed" v-if="filteredDataList.length === 0 && isRefreshed"
error-code="404" error-code="404"
error-title="没有任务" error-title="没有任务"
error-description="正在下载的任务将会显示在这里" error-description="正在下载的任务将会显示在这里"

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

View File

@@ -4,12 +4,17 @@ import api from '@/api'
import type { Subscribe } from '@/api/types' import type { Subscribe } from '@/api/types'
import NoDataFound from '@/components/NoDataFound.vue' import NoDataFound from '@/components/NoDataFound.vue'
import SubscribeCard from '@/components/cards/SubscribeCard.vue' import SubscribeCard from '@/components/cards/SubscribeCard.vue'
import store from '@/store'
// 输入参数 // 输入参数
const props = defineProps({ const props = defineProps({
type: String, type: String,
}) })
// 从Vuex Store中获取用户信息
const superUser = store.state.auth.superUser
const userName = store.state.auth.userName
// 是否刷新过 // 是否刷新过
const isRefreshed = ref(false) const isRefreshed = ref(false)
@@ -40,9 +45,12 @@ function onRefresh() {
loading.value = false loading.value = false
} }
// 过滤数据 // 过滤数据,管理员用户显示全部,非管理员只显示自己的订阅
const filteredDataList = computed(() => { 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> </script>

View File

@@ -12,7 +12,6 @@ module.exports = {
}, },
plugins: [ plugins: [
require('@tailwindcss/aspect-ratio'), require('@tailwindcss/aspect-ratio'),
// ... // ...
], ],
} }