mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-05-09 22:22:58 +08:00
Compare commits
63 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2aa5a64aa | ||
|
|
2620a55c5a | ||
|
|
14e33215f8 | ||
|
|
6862c2a744 | ||
|
|
fb215e8d87 | ||
|
|
f52ad2151b | ||
|
|
1a47b7d09d | ||
|
|
f292071a34 | ||
|
|
dd616d29e8 | ||
|
|
0509f18d66 | ||
|
|
f59fb119e4 | ||
|
|
46127cac1f | ||
|
|
c1abf76211 | ||
|
|
fe5b45d48d | ||
|
|
10ac1ebf7b | ||
|
|
e5d8144510 | ||
|
|
f9a65fba7a | ||
|
|
9b4138349b | ||
|
|
db9c9db5a9 | ||
|
|
24e992339f | ||
|
|
f26d1babf7 | ||
|
|
de3347cea1 | ||
|
|
e900fac4bd | ||
|
|
396218a467 | ||
|
|
d3a66ffa8c | ||
|
|
1e7ffb4c2e | ||
|
|
3df5d4c690 | ||
|
|
02a8331996 | ||
|
|
a29ad6a091 | ||
|
|
3ef1e65412 | ||
|
|
2deaec1fc6 | ||
|
|
c9b0b23d36 | ||
|
|
f06cca4ead | ||
|
|
a1990ce3e4 | ||
|
|
cbbf023030 | ||
|
|
307aa724eb | ||
|
|
cd6f37d80f | ||
|
|
b903134770 | ||
|
|
11effdd297 | ||
|
|
8873d8372d | ||
|
|
964aa29d12 | ||
|
|
b45a3c6539 | ||
|
|
b72b7ad0fb | ||
|
|
0e3106d8c1 | ||
|
|
71a6626fa9 | ||
|
|
68006bac88 | ||
|
|
34cbcc38a6 | ||
|
|
f4daee85c7 | ||
|
|
dd347039b5 | ||
|
|
0c9367d58a | ||
|
|
af10c4f1c3 | ||
|
|
52fbeda941 | ||
|
|
ace23af363 | ||
|
|
a097d89d68 | ||
|
|
77cb817523 | ||
|
|
c956e271a2 | ||
|
|
6413f30d18 | ||
|
|
789e748df0 | ||
|
|
c89edae375 | ||
|
|
f4dca4922b | ||
|
|
73b9ef5ee7 | ||
|
|
462742961a | ||
|
|
5a647fabfa |
45
.github/ISSUE_TEMPLATE/rfc.yml
vendored
Normal file
45
.github/ISSUE_TEMPLATE/rfc.yml
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
name: 功能提案
|
||||
description: Request for Comments
|
||||
title: '[RFC]'
|
||||
labels: ['RFC']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
一份提案(RFC)定位为 **「在某功能/重构的具体开发前,用于开发者间 review 技术设计/方案的文档」**,
|
||||
目的是让协作的开发者间清晰的知道「要做什么」和「具体会怎么做」,以及所有的开发者都能公开透明的参与讨论;
|
||||
以便评估和讨论产生的影响 (遗漏的考虑、向后兼容性、与现有功能的冲突),
|
||||
因此提案侧重在对解决问题的 **方案、设计、步骤** 的描述上。
|
||||
|
||||
如果仅希望讨论是否添加或改进某功能本身,请使用 -> [Issue: 功能改进](https://github.com/jxxghp/MoviePilot/issues/new?assignees=&labels=feature+request&projects=&template=feature_request.yml&title=%5BFeature+Request%5D%3A+)
|
||||
- type: textarea
|
||||
id: background
|
||||
attributes:
|
||||
label: 背景 or 问题
|
||||
description: 简单描述遇到的什么问题或需要改动什么。可以引用其他 issue、讨论、文档等。
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: goal
|
||||
attributes:
|
||||
label: '目标 & 方案简述'
|
||||
description: 简单描述提案此提案实现后,**预期的目标效果**,以及简单大致描述会采取的方案/步骤,可能会/不会产生什么影响。
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: design
|
||||
attributes:
|
||||
label: '方案设计 & 实现步骤'
|
||||
description: |
|
||||
详细描述你设计的具体方案,可以考虑拆分列表或要点,一步步描述具体打算如何实现的步骤和相关细节。
|
||||
这部份不需要一次性写完整,即使在创建完此提案 issue 后,依旧可以再次编辑修改。
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: alternative
|
||||
attributes:
|
||||
label: '替代方案 & 对比'
|
||||
description: |
|
||||
[可选] 为来实现目标效果,还考虑过什么其他方案,有什么对比?
|
||||
validations:
|
||||
required: false
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "moviepilot",
|
||||
"version": "2.0.7",
|
||||
"version": "2.1.2",
|
||||
"private": true,
|
||||
"bin": "dist/service.js",
|
||||
"scripts": {
|
||||
@@ -85,6 +85,7 @@
|
||||
"stylelint-config-idiomatic-order": "10.0.0",
|
||||
"stylelint-config-standard-scss": "13.1.0",
|
||||
"stylelint-use-logical-spec": "5.0.1",
|
||||
"terser": "^5.36.0",
|
||||
"type-fest": "^4.15.0",
|
||||
"typescript": "^5.0.4",
|
||||
"unplugin-auto-import": "^0.17.5",
|
||||
|
||||
@@ -71,3 +71,10 @@ export const storageDict = storageOptions.reduce((dict, item) => {
|
||||
dict[item.value] = item.title
|
||||
return dict
|
||||
}, {} as Record<string, string>)
|
||||
|
||||
export const transferTypeOptions = [
|
||||
{ title: '复制', value: 'copy' },
|
||||
{ title: '移动', value: 'move' },
|
||||
{ title: '硬链接', value: 'link' },
|
||||
{ title: '软链接', value: 'softlink' },
|
||||
]
|
||||
|
||||
@@ -44,7 +44,7 @@ export interface Subscribe {
|
||||
lack_episode?: number
|
||||
// 附加信息
|
||||
note?: string
|
||||
// 状态:N-新建, R-订阅中
|
||||
// 状态:N-新建 R-订阅中 P-待定 S-暂停
|
||||
state: string
|
||||
// 最后更新时间
|
||||
last_update: string
|
||||
@@ -73,7 +73,7 @@ export interface Subscribe {
|
||||
// 过滤规则组
|
||||
filter_groups?: string[]
|
||||
// 下载器
|
||||
downloader?: string
|
||||
downloader: string
|
||||
}
|
||||
|
||||
// 订阅分享
|
||||
@@ -390,7 +390,7 @@ export interface Site {
|
||||
// RSS地址
|
||||
rss?: string
|
||||
// 下载器
|
||||
downloader?: string
|
||||
downloader: string
|
||||
// Cookie
|
||||
cookie?: string
|
||||
// ApiKey
|
||||
@@ -1051,7 +1051,7 @@ export interface TransferDirectoryConf {
|
||||
// 监控模式 fast/compatibility
|
||||
monitor_mode?: string
|
||||
// 整理方式 move/copy/link/softlink
|
||||
transfer_type?: string
|
||||
transfer_type: string
|
||||
// 文件覆盖模式 always/size/never/latest
|
||||
overwrite_mode?: string
|
||||
// 整理到媒体库目录
|
||||
@@ -1139,3 +1139,42 @@ export interface SubscrbieInfo {
|
||||
// 集信息 {集号: {download: 文件路径,library: 文件路径, backdrop: url, title: 标题, description: 描述}}
|
||||
episodes: Record<number, SubscribeEpisodeInfo>
|
||||
}
|
||||
|
||||
export interface TransferForm {
|
||||
// 文件项
|
||||
fileitem: FileItem
|
||||
// 历史ID
|
||||
logid: number
|
||||
// 目标存储
|
||||
target_storage: string
|
||||
// 目标路径
|
||||
target_path: string
|
||||
// TMDB ID
|
||||
tmdbid?: number
|
||||
// 豆瓣 ID
|
||||
doubanid?: string
|
||||
// 季号
|
||||
season?: number
|
||||
// 类型
|
||||
type_name?: string
|
||||
// 整理方式
|
||||
transfer_type: string
|
||||
// 自定义格式
|
||||
episode_format?: string
|
||||
// 指定集数
|
||||
episode_detail?: string
|
||||
// 指定PART
|
||||
episode_part?: string
|
||||
// 集数偏移
|
||||
episode_offset?: string
|
||||
// 最小文件大小
|
||||
min_filesize: number
|
||||
// 刮削
|
||||
scrape: boolean
|
||||
// 复用历史识别信息
|
||||
from_history: boolean
|
||||
// 媒体库类型子目录
|
||||
library_type_folder?: boolean
|
||||
// 媒体库类别子目录
|
||||
library_category_folder?: boolean
|
||||
}
|
||||
|
||||
@@ -220,7 +220,12 @@ watch(
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="4">
|
||||
<VSelect v-model="props.directory.storage" variant="underlined" :items="storageOptions" label="下载存储" />
|
||||
<VSelect
|
||||
v-model="props.directory.storage"
|
||||
variant="underlined"
|
||||
:items="storageOptions"
|
||||
label="下载存储/源存储"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="8">
|
||||
<VPathField @update:modelValue="updateDownloadPath" :storage="props.directory.storage">
|
||||
@@ -229,7 +234,7 @@ watch(
|
||||
v-model="props.directory.download_path"
|
||||
v-bind="menuprops"
|
||||
variant="underlined"
|
||||
label="下载目录"
|
||||
label="下载目录/源目录"
|
||||
/>
|
||||
</template>
|
||||
</VPathField>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useToast } from 'vue-toast-notification'
|
||||
import type { DownloaderInfo } from '@/api/types'
|
||||
import qbittorrent_image from '@images/logos/qbittorrent.png'
|
||||
import transmission_image from '@images/logos/transmission.png'
|
||||
import {cloneDeep} from "lodash";
|
||||
import { cloneDeep } from 'lodash'
|
||||
|
||||
// 定义输入
|
||||
const props = defineProps({
|
||||
@@ -211,7 +211,6 @@ onUnmounted(() => {
|
||||
<VTextField
|
||||
v-model="downloaderInfo.config.username"
|
||||
label="用户名"
|
||||
placeholder="admin"
|
||||
hint="登录使用的用户名"
|
||||
persistent-hint
|
||||
active
|
||||
@@ -289,7 +288,6 @@ onUnmounted(() => {
|
||||
<VTextField
|
||||
v-model="downloaderInfo.config.username"
|
||||
label="用户名"
|
||||
placeholder="admin"
|
||||
hint="登录使用的用户名"
|
||||
persistent-hint
|
||||
active
|
||||
|
||||
@@ -72,12 +72,17 @@ const getCategories = computed(() => {
|
||||
|
||||
// 规则组规则卡片列表
|
||||
const filterRuleCards = ref<FilterCard[]>([])
|
||||
// 规则组类型,仅用于导入判断
|
||||
const filterRuleCardsType = ref<FilterCard>({
|
||||
pri: '',
|
||||
rules: [],
|
||||
})
|
||||
|
||||
// 导入代码弹窗
|
||||
const importCodeDialog = ref(false)
|
||||
|
||||
// 导入的代码
|
||||
const importCodeString = ref('')
|
||||
// 导入代码类型
|
||||
const importCodeType = ref('')
|
||||
|
||||
// 更新规则卡片的值
|
||||
function updateFilterCardValue(pri: string, rules: string[]) {
|
||||
@@ -109,28 +114,37 @@ function shareRules() {
|
||||
$toast.success('优先级规则已复制到剪贴板')
|
||||
} catch (error) {
|
||||
$toast.error('优先级规则复制失败!')
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 导入规则
|
||||
async function importRules() {
|
||||
importCodeString.value = ''
|
||||
async function importRules(ruleType: string) {
|
||||
importCodeType.value = ruleType
|
||||
importCodeDialog.value = true
|
||||
}
|
||||
|
||||
// 监听导入代码变化
|
||||
watchEffect(() => {
|
||||
if (!importCodeString.value) return
|
||||
|
||||
if (!importCodeString.value.startsWith(' ')) importCodeString.value = ` ${importCodeString.value}`
|
||||
if (!importCodeString.value.endsWith(' ')) importCodeString.value = `${importCodeString.value} `
|
||||
|
||||
const groups = importCodeString.value.split('>')
|
||||
filterRuleCards.value = groups.map((group: string, index: number) => ({
|
||||
pri: (index + 1).toString(),
|
||||
rules: group.split('&').filter(rule => rule),
|
||||
}))
|
||||
})
|
||||
// 保存导入的代码,直接覆盖原有值
|
||||
function saveCodeString(type: string, code: any) {
|
||||
try {
|
||||
code = code.value
|
||||
if (type === 'priority') {
|
||||
// 解析值
|
||||
if (!code) return
|
||||
// 首尾增加空格
|
||||
if (!code.startsWith(' ')) code = ` ${code}`
|
||||
if (!code.endsWith(' ')) code = `${code} `
|
||||
const groups = code.split('>')
|
||||
filterRuleCards.value = groups.map((group: string, index: number) => ({
|
||||
pri: (index + 1).toString(),
|
||||
rules: group.split('&').filter(rule => rule),
|
||||
}))
|
||||
}
|
||||
} catch (error) {
|
||||
$toast.error('导入失败!')
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 增加卡片
|
||||
function addFilterCard() {
|
||||
@@ -268,7 +282,7 @@ function onClose() {
|
||||
<VBtn color="primary" variant="tonal" @click="addFilterCard">
|
||||
<VIcon icon="mdi-plus" />
|
||||
</VBtn>
|
||||
<VBtn color="success" variant="tonal" @click="importRules">
|
||||
<VBtn color="success" variant="tonal" @click="importRules('priority')">
|
||||
<VIcon icon="mdi-import" />
|
||||
</VBtn>
|
||||
<VBtn color="info" variant="tonal" @click="shareRules">
|
||||
@@ -279,8 +293,13 @@ function onClose() {
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
<VDialog v-model="importCodeDialog" width="60rem" scrollable>
|
||||
<ImportCodeDialog v-model="importCodeString" title="导入优先级规则" @close="importCodeDialog = false" />
|
||||
</VDialog>
|
||||
<ImportCodeDialog
|
||||
v-if="importCodeDialog"
|
||||
v-model="importCodeDialog"
|
||||
title="导入规则优先级"
|
||||
:dataType="importCodeType"
|
||||
@close="importCodeDialog = false"
|
||||
@save="saveCodeString"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -144,10 +144,17 @@ onMounted(() => {
|
||||
<AliyunAuthDialog
|
||||
v-if="aliyunAuthDialog"
|
||||
v-model="aliyunAuthDialog"
|
||||
:conf="props.storage.config || {}"
|
||||
@close="aliyunAuthDialog = false"
|
||||
@done="handleDone"
|
||||
/>
|
||||
<U115AuthDialog v-if="u115AuthDialog" v-model="u115AuthDialog" @close="u115AuthDialog = false" @done="handleDone" />
|
||||
<U115AuthDialog
|
||||
v-if="u115AuthDialog"
|
||||
v-model="u115AuthDialog"
|
||||
:conf="props.storage.config || {}"
|
||||
@close="u115AuthDialog = false"
|
||||
@done="handleDone"
|
||||
/>
|
||||
<RcloneConfigDialog
|
||||
v-if="rcloneConfigDialog"
|
||||
v-model="rcloneConfigDialog"
|
||||
|
||||
@@ -38,6 +38,9 @@ const subscribeFilesDialog = ref(false)
|
||||
// 分享订阅弹窗
|
||||
const subscribeShareDialog = ref(false)
|
||||
|
||||
// 定义一个变量来保存当前的订阅状态
|
||||
const subscribeState = ref<string>(props.media?.state ?? 'P')
|
||||
|
||||
// 上一次更新时间
|
||||
const lastUpdateText = ref(props.media && props.media.last_update ? formatDateDifference(props.media.last_update) : '')
|
||||
|
||||
@@ -81,6 +84,32 @@ async function searchSubscribe() {
|
||||
}
|
||||
}
|
||||
|
||||
// 切换订阅状态
|
||||
async function toggleSubscribeStatus(state: 'R' | 'S') {
|
||||
try {
|
||||
// 根据传入的 state 判断对应的操作文字
|
||||
const action = state === 'S' ? '暂停' : '启用'
|
||||
// 弹出确认框
|
||||
const isConfirmed = await createConfirm({
|
||||
title: `确认${action}`,
|
||||
content: `是否${action}订阅 ${props.media?.name}?`,
|
||||
})
|
||||
if (!isConfirmed) return
|
||||
// 调用 API 更新订阅状态
|
||||
const result: { [key: string]: any } = await api.put(`subscribe/status/${props.media?.id}?state=${state}`)
|
||||
// 提示
|
||||
if (result.success) {
|
||||
$toast.success(`${props.media?.name} 已${action}!`)
|
||||
subscribeState.value = state
|
||||
emit('save')
|
||||
} else {
|
||||
$toast.error(`${action}失败:${result.message}`)
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
// 重置订阅
|
||||
async function resetSubscribe() {
|
||||
// 确认
|
||||
@@ -129,7 +158,7 @@ async function viewSubscribeFiles() {
|
||||
}
|
||||
|
||||
// 弹出菜单
|
||||
const dropdownItems = ref([
|
||||
const dropdownItems = computed(() => [
|
||||
{
|
||||
title: '编辑',
|
||||
value: 1,
|
||||
@@ -163,8 +192,17 @@ const dropdownItems = ref([
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '重置',
|
||||
title: subscribeState.value === 'S' ? '启用' : '暂停',
|
||||
value: 5,
|
||||
props: {
|
||||
prependIcon: subscribeState.value === 'S' ? 'mdi-play' : 'mdi-pause',
|
||||
click: () => toggleSubscribeStatus(subscribeState.value === 'S' ? 'R' : 'S'),
|
||||
color: subscribeState.value === 'S' ? 'success' : 'info',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '重置',
|
||||
value: 6,
|
||||
props: {
|
||||
prependIcon: 'mdi-restore-alert',
|
||||
click: resetSubscribe,
|
||||
@@ -174,7 +212,7 @@ const dropdownItems = ref([
|
||||
},
|
||||
{
|
||||
title: '分享',
|
||||
value: 6,
|
||||
value: 7,
|
||||
props: {
|
||||
prependIcon: 'mdi-share',
|
||||
click: shareSubscribe,
|
||||
@@ -184,7 +222,7 @@ const dropdownItems = ref([
|
||||
},
|
||||
{
|
||||
title: '取消订阅',
|
||||
value: 7,
|
||||
value: 8,
|
||||
props: {
|
||||
prependIcon: 'mdi-trash-can-outline',
|
||||
color: 'error',
|
||||
@@ -242,6 +280,7 @@ function onSubscribeEditRemove() {
|
||||
:class="{
|
||||
'outline-dashed outline-1': props.media?.best_version && imageLoaded,
|
||||
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
|
||||
'opacity-70': subscribeState === 'S',
|
||||
}"
|
||||
min-height="170"
|
||||
@click="editSubscribeDialog"
|
||||
@@ -277,6 +316,10 @@ function onSubscribeEditRemove() {
|
||||
</template>
|
||||
<div class="absolute inset-0 subscribe-card-background"></div>
|
||||
</VImg>
|
||||
<div
|
||||
v-if="subscribeState === 'P'"
|
||||
class="absolute inset-0 bg-yellow-900 opacity-40 pointer-events-none"
|
||||
></div>
|
||||
</template>
|
||||
<div>
|
||||
<VCardText class="flex items-center">
|
||||
|
||||
@@ -13,6 +13,9 @@ const props = defineProps({
|
||||
torrent: Object as PropType<TorrentInfo>,
|
||||
})
|
||||
|
||||
// 定义成功和失败事件
|
||||
const emit = defineEmits(['done', 'error', 'close'])
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
@@ -22,8 +25,8 @@ const selectedDownloader = ref<string | null>(null)
|
||||
// 选择的保存目录
|
||||
const selectedDirectory = ref<string | null>(null)
|
||||
|
||||
// 定义成功和失败事件
|
||||
const emit = defineEmits(['done', 'error', 'close'])
|
||||
// 下载器
|
||||
const downloaders = ref<DownloaderConf[]>([])
|
||||
|
||||
// 所有目录设置
|
||||
const directories = ref<TransferDirectoryConf[]>([])
|
||||
@@ -53,14 +56,10 @@ const targetDirectories = computed(() => {
|
||||
return [...new Set(downloadDirectories)]
|
||||
})
|
||||
|
||||
// 下载器
|
||||
const downloaders = ref<DownloaderConf[]>([])
|
||||
|
||||
// 调用API查询下载器设置
|
||||
async function loadDownloaderSetting() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/setting/Downloaders')
|
||||
downloaders.value = result.data?.value ?? []
|
||||
downloaders.value = await api.get('download/clients')
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,14 @@
|
||||
import QrcodeVue from 'qrcode.vue'
|
||||
import api from '@/api'
|
||||
|
||||
// 定义输入
|
||||
const props = defineProps({
|
||||
conf: {
|
||||
type: Object as PropType<{ [key: string]: any }>,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
// 定义事件
|
||||
const emit = defineEmits(['done', 'close'])
|
||||
|
||||
@@ -25,6 +33,10 @@ let timeoutTimer: NodeJS.Timeout | undefined = undefined
|
||||
|
||||
// 完成
|
||||
async function handleDone() {
|
||||
clearTimeout(timeoutTimer)
|
||||
if (props.conf?.refreshToken) {
|
||||
await savaAliPanConfig()
|
||||
}
|
||||
emit('done')
|
||||
}
|
||||
|
||||
@@ -75,6 +87,15 @@ async function checkQrcode() {
|
||||
}
|
||||
}
|
||||
|
||||
// 保存cookie设置
|
||||
async function savaAliPanConfig() {
|
||||
try {
|
||||
await api.post(`storage/save/alipan`, props.conf)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await getQrcode()
|
||||
timeoutTimer = setTimeout(checkQrcode, 3000)
|
||||
@@ -97,6 +118,13 @@ onUnmounted(() => {
|
||||
<template #prepend />
|
||||
</VAlert>
|
||||
</VCardText>
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol class="mt-2">
|
||||
<VTextField label="自定义refreshToken" v-model="props.conf.refreshToken" outlined dense />
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
<VBtn variant="elevated" @click="handleDone" prepend-icon="mdi-check" class="px-5 me-3"> 完成 </VBtn>
|
||||
|
||||
@@ -2,17 +2,18 @@
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
title: String,
|
||||
dataType: String,
|
||||
})
|
||||
|
||||
// 定义事件
|
||||
const emit = defineEmits(['update:modelValue', 'close'])
|
||||
|
||||
// 代码
|
||||
const codeString = ref('')
|
||||
|
||||
// 定义事件
|
||||
const emit = defineEmits(['close', 'save'])
|
||||
|
||||
// 导入
|
||||
function handleImport() {
|
||||
emit('update:modelValue', codeString.value)
|
||||
emit('save', props.dataType, codeString)
|
||||
emit('close')
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import MediaIdSelector from '../misc/MediaIdSelector.vue'
|
||||
import api from '@/api'
|
||||
import { storageOptions } from '@/api/constants'
|
||||
import { storageOptions, transferTypeOptions } from '@/api/constants'
|
||||
import { numberValidator } from '@/@validators'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import ProgressDialog from './ProgressDialog.vue'
|
||||
import { FileItem, TransferDirectoryConf } from '@/api/types'
|
||||
import { FileItem, TransferDirectoryConf, TransferForm } from '@/api/types'
|
||||
|
||||
// 显示器宽度
|
||||
const display = useDisplay()
|
||||
@@ -66,20 +66,12 @@ const dialogTitle = computed(() => {
|
||||
})
|
||||
|
||||
// 表单
|
||||
const transferForm = reactive({
|
||||
fileitem: {},
|
||||
const transferForm = reactive<TransferForm>({
|
||||
fileitem: {} as FileItem,
|
||||
logid: 0,
|
||||
target_storage: props.target_storage ?? 'local',
|
||||
target_path: props.target_path ?? null,
|
||||
tmdbid: null,
|
||||
doubanid: null,
|
||||
season: null,
|
||||
type_name: '',
|
||||
transfer_type: '',
|
||||
episode_format: '',
|
||||
episode_detail: '',
|
||||
episode_part: '',
|
||||
episode_offset: null,
|
||||
target_path: '',
|
||||
min_filesize: 0,
|
||||
scrape: false,
|
||||
from_history: false,
|
||||
@@ -87,7 +79,6 @@ const transferForm = reactive({
|
||||
|
||||
// 所有媒体库目录
|
||||
const directories = ref<TransferDirectoryConf[]>([])
|
||||
|
||||
// 查询目录
|
||||
async function loadDirectories() {
|
||||
try {
|
||||
@@ -100,7 +91,8 @@ async function loadDirectories() {
|
||||
|
||||
// 目的目录下拉框
|
||||
const targetDirectories = computed(() => {
|
||||
return directories.value.map(item => item.library_path)
|
||||
const libraryDirectories = directories.value.map(item => item.library_path)
|
||||
return [...new Set(libraryDirectories)]
|
||||
})
|
||||
|
||||
// 监听目的路径变化,配置默认值
|
||||
@@ -110,12 +102,24 @@ watch(
|
||||
if (newPath) {
|
||||
const directory = directories.value.find(item => item.library_path === newPath)
|
||||
if (directory) {
|
||||
transferForm.target_storage = directory.storage ?? 'local'
|
||||
transferForm.transfer_type = directory.transfer_type ?? ''
|
||||
transferForm.target_storage = directory.library_storage ?? 'local'
|
||||
transferForm.transfer_type = transferForm.transfer_type || directory.transfer_type
|
||||
transferForm.scrape = directory.scraping ?? false
|
||||
transferForm.library_category_folder = directory.library_category_folder ?? false
|
||||
transferForm.library_type_folder = directory.library_type_folder ?? false
|
||||
} else {
|
||||
transferForm.transfer_type = transferForm.transfer_type || 'copy'
|
||||
transferForm.scrape = false
|
||||
transferForm.library_category_folder = false
|
||||
transferForm.library_type_folder = false
|
||||
}
|
||||
} else {
|
||||
// 路径为空时, 恢复到`自动`条件
|
||||
transferForm.transfer_type = ''
|
||||
transferForm.library_type_folder = undefined
|
||||
transferForm.library_category_folder = undefined
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// 使用SSE监听加载进度
|
||||
@@ -182,7 +186,7 @@ async function handleTransfer(item: FileItem) {
|
||||
// 整理日志
|
||||
async function handleTransferLog(logid: number) {
|
||||
transferForm.logid = logid
|
||||
transferForm.fileitem = {}
|
||||
transferForm.fileitem = {} as FileItem
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post('transfer/manual', transferForm)
|
||||
if (!result.success) $toast.error(`历史记录 ${logid} 重新整理失败:${result.message}!`)
|
||||
@@ -218,18 +222,15 @@ onMounted(() => {
|
||||
<VSelect
|
||||
v-model="transferForm.transfer_type"
|
||||
label="整理方式"
|
||||
:items="[
|
||||
{ title: '默认', value: '' },
|
||||
{ title: '移动', value: 'move' },
|
||||
{ title: '复制', value: 'copy' },
|
||||
{ title: '硬链接', value: 'link' },
|
||||
{ title: '软链接', value: 'softlink' },
|
||||
]"
|
||||
:items="transferTypeOptions"
|
||||
hint="文件操作整理方式"
|
||||
persistent-hint
|
||||
/>
|
||||
persistent-hint>
|
||||
<template v-slot:selection="{ item }">
|
||||
{{ transferForm.transfer_type === '' ? '自动' : item.title }}
|
||||
</template>
|
||||
</VSelect>
|
||||
</VCol>
|
||||
<VCol cols="12" md="12">
|
||||
<VCol cols="12">
|
||||
<VCombobox
|
||||
v-model="transferForm.target_path"
|
||||
:items="targetDirectories"
|
||||
@@ -340,6 +341,22 @@ onMounted(() => {
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6" v-if="transferForm.target_path">
|
||||
<VSwitch
|
||||
v-model="transferForm.library_type_folder"
|
||||
label="按类型分类"
|
||||
hint="整理时目的路径下按媒体类型添加子目录"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6" v-if="transferForm.target_path">
|
||||
<VSwitch
|
||||
v-model="transferForm.library_category_folder"
|
||||
label="按类别分类"
|
||||
hint="整理时在目的路径下按媒体类别添加子目录"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="transferForm.scrape"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import type { Site } from '@/api/types'
|
||||
import type { DownloaderConf, Site } from '@/api/types'
|
||||
import { doneNProgress, startNProgress } from '@/api/nprogress'
|
||||
import { numberValidator, requiredValidator } from '@/@validators'
|
||||
import api from '@/api'
|
||||
@@ -35,6 +35,7 @@ const siteForm = ref<Site>({
|
||||
limit_seconds: 0,
|
||||
name: '',
|
||||
domain: '',
|
||||
downloader: '',
|
||||
})
|
||||
|
||||
// 提示框
|
||||
@@ -65,10 +66,9 @@ const downloaderOptions = ref<{ title: string; value: string }[]>([])
|
||||
|
||||
async function loadDownloaderSetting() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/setting/Downloaders')
|
||||
const downloaders = result.data?.value ?? []
|
||||
const downloaders: DownloaderConf[] = await api.get('download/clients')
|
||||
downloaderOptions.value = [
|
||||
{ title: '默认', value: null },
|
||||
{ title: '默认', value: '' },
|
||||
...downloaders.map((item: { name: any }) => ({
|
||||
title: item.name,
|
||||
value: item.name,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import { numberValidator } from '@/@validators'
|
||||
import api from '@/api'
|
||||
import type { FilterRuleGroup, Site, Subscribe, TransferDirectoryConf } from '@/api/types'
|
||||
import type { DownloaderConf, FilterRuleGroup, Site, Subscribe, TransferDirectoryConf } from '@/api/types'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import { useConfirm } from 'vuetify-use-dialog'
|
||||
import { VTextarea, VTextField } from 'vuetify/lib/components/index.mjs'
|
||||
@@ -50,6 +50,7 @@ const subscribeForm = ref<Subscribe>({
|
||||
sites: [],
|
||||
best_version: undefined,
|
||||
current_priority: 0,
|
||||
downloader: '',
|
||||
date: '',
|
||||
show_edit_dialog: false,
|
||||
})
|
||||
@@ -62,10 +63,9 @@ const downloaderOptions = ref<{ title: string; value: string }[]>([])
|
||||
|
||||
async function loadDownloaderSetting() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/setting/Downloaders')
|
||||
const downloaders = result.data?.value ?? []
|
||||
const downloaders: DownloaderConf[] = await api.get('download/clients')
|
||||
downloaderOptions.value = [
|
||||
{ title: '默认', value: null },
|
||||
{ title: '默认', value: '' },
|
||||
...downloaders.map((item: { name: any }) => ({
|
||||
title: item.name,
|
||||
value: item.name,
|
||||
@@ -417,7 +417,7 @@ onMounted(() => {
|
||||
v-model="subscribeForm.downloader"
|
||||
:items="downloaderOptions"
|
||||
label="下载器"
|
||||
hint="指定该订阅使用的下载器,留空自动使用默认下载器"
|
||||
hint="指定该订阅使用的下载器"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
@@ -86,7 +86,7 @@ async function reSubscribe(item: Subscribe) {
|
||||
else progressText.value = `正在重新订阅 ${item.name} 第 ${item.season} 季 ...`
|
||||
progressDialog.value = true
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post('subscribe', item)
|
||||
const result: { [key: string]: any } = await api.post('subscribe/', item)
|
||||
if (result.success) {
|
||||
emit('save')
|
||||
}
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
<script lang="ts" setup>
|
||||
import api from '@/api'
|
||||
import QrcodeVue from 'qrcode.vue'
|
||||
import { VCardItem, VTextField } from 'vuetify/lib/components/index.mjs'
|
||||
|
||||
// 定义输入
|
||||
const props = defineProps({
|
||||
conf: {
|
||||
type: Object as PropType<{ [key: string]: any }>,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
// 定义事件
|
||||
const emit = defineEmits(['done', 'close'])
|
||||
@@ -9,7 +18,7 @@ const emit = defineEmits(['done', 'close'])
|
||||
const qrCodeContent = ref('')
|
||||
|
||||
// 下方的提示信息
|
||||
const text = ref('请使用微信或115客户端扫码')
|
||||
const text = ref('请使用微信或115客户端扫码,或在下方输入Cookie')
|
||||
|
||||
// 提醒类型
|
||||
const alertType = ref<'success' | 'info' | 'error' | 'warning' | undefined>('info')
|
||||
@@ -19,6 +28,10 @@ let timeoutTimer: NodeJS.Timeout | undefined = undefined
|
||||
|
||||
// 完成
|
||||
async function handleDone() {
|
||||
clearTimeout(timeoutTimer)
|
||||
if (props.conf?.cookie) {
|
||||
await savaU115Config()
|
||||
}
|
||||
emit('done')
|
||||
}
|
||||
|
||||
@@ -71,6 +84,15 @@ async function checkQrcode() {
|
||||
}
|
||||
}
|
||||
|
||||
// 保存cookie设置
|
||||
async function savaU115Config() {
|
||||
try {
|
||||
await api.post(`storage/save/u115`, props.conf)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await getQrcode()
|
||||
timeoutTimer = setTimeout(checkQrcode, 3000)
|
||||
@@ -93,6 +115,13 @@ onUnmounted(() => {
|
||||
<template #prepend />
|
||||
</VAlert>
|
||||
</VCardText>
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol class="mt-2">
|
||||
<VTextField label="自定义Cookie" v-model="props.conf.cookie" outlined dense />
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
<VBtn variant="elevated" @click="handleDone" prepend-icon="mdi-check" class="px-5 me-3"> 完成 </VBtn>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import { isNullOrEmptyObject } from '@/@core/utils'
|
||||
import api from '@/api'
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
|
||||
@@ -61,7 +62,10 @@ async function loadLastAuthParams() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get(`system/setting/UserSiteAuthParams`)
|
||||
if (result.success) {
|
||||
authForm.value = result.data?.value || { site: null, params: {} }
|
||||
const ret = result.data?.value
|
||||
if (ret && !isNullOrEmptyObject(ret.params)) {
|
||||
authForm.value = ret
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
@@ -84,6 +88,22 @@ async function handleDone() {
|
||||
|
||||
// 认证处理
|
||||
async function checkUser() {
|
||||
if (!authForm.value.site) {
|
||||
$toast.error('请选择认证站点!')
|
||||
return
|
||||
}
|
||||
if (!authSites.value[authForm.value.site]) {
|
||||
$toast.error('站点配置不存在!')
|
||||
return
|
||||
}
|
||||
if (formFields.value.length > 0) {
|
||||
for (const field of formFields.value) {
|
||||
if (!authForm.value.params[field.site.toUpperCase() + '_' + field.key.toUpperCase()]) {
|
||||
$toast.error(`请输入${field.name}!`)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post(`site/auth`, authForm.value)
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import api from '@/api'
|
||||
import { FileItem } from '@/api/types'
|
||||
import { VTreeview } from 'vuetify/labs/VTreeview'
|
||||
|
||||
// 输入变量为默认路径
|
||||
const props = defineProps({
|
||||
root: {
|
||||
type: String,
|
||||
@@ -16,16 +14,12 @@ const props = defineProps({
|
||||
},
|
||||
})
|
||||
|
||||
// update:modelValue 事件
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
// 激活的目录
|
||||
const activedDirs = ref<string[]>([])
|
||||
|
||||
// 打开的目录
|
||||
const openedDirs = ref<string[]>([])
|
||||
const isUserAction = ref(false) // 标志:是否为用户主动操作
|
||||
|
||||
// 目录列表
|
||||
const treeItems = ref<FileItem[]>([
|
||||
{
|
||||
name: '/',
|
||||
@@ -37,19 +31,16 @@ const treeItems = ref<FileItem[]>([
|
||||
},
|
||||
])
|
||||
|
||||
// 拉取子目录
|
||||
async function fetchDirs(item: any) {
|
||||
return api
|
||||
.post('/storage/list', item)
|
||||
.then((data: any) => {
|
||||
// 只添加目录到子目录
|
||||
data = data.filter((i: any) => i.type === 'dir')
|
||||
item.children.push(...data)
|
||||
})
|
||||
.catch(err => console.warn(err))
|
||||
}
|
||||
|
||||
// 获取选择的目录路径
|
||||
const selectedPath = computed(() => {
|
||||
if (activedDirs.value.length > 0) {
|
||||
return activedDirs.value[0]
|
||||
@@ -57,13 +48,12 @@ const selectedPath = computed(() => {
|
||||
return ''
|
||||
})
|
||||
|
||||
// 监听目录变化
|
||||
watch(activedDirs, newVal => {
|
||||
if (!newVal.length) return
|
||||
emit('update:modelValue', selectedPath)
|
||||
if (!newVal.length || !isUserAction.value) return
|
||||
emit('update:modelValue', selectedPath.value)
|
||||
isUserAction.value = false
|
||||
})
|
||||
|
||||
// 监听存储变化
|
||||
watch(
|
||||
() => props.storage,
|
||||
async newVal => {
|
||||
@@ -81,6 +71,10 @@ watch(
|
||||
activedDirs.value = []
|
||||
},
|
||||
)
|
||||
|
||||
function handleUserSelect() {
|
||||
isUserAction.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -102,7 +96,7 @@ watch(
|
||||
max-height="20rem"
|
||||
expand-icon="mdi-folder"
|
||||
collapse-icon="mdi-folder-open"
|
||||
>
|
||||
</VTreeview>
|
||||
@update:activated="handleUserSelect"
|
||||
/>
|
||||
</VMenu>
|
||||
</template>
|
||||
|
||||
@@ -111,7 +111,7 @@ const userLevel = computed(() => store.state.auth.level)
|
||||
</VListItem>
|
||||
|
||||
<!-- 👉 Site Auth -->
|
||||
<VListItem v-if="userLevel < 2" link @click="showSiteAuthDialog">
|
||||
<VListItem v-if="userLevel < 2 && superUser" link @click="showSiteAuthDialog">
|
||||
<template #prepend>
|
||||
<VIcon class="me-2" icon="mdi-lock-check-outline" size="22" />
|
||||
</template>
|
||||
|
||||
@@ -12,7 +12,6 @@ import { isPWA } from './@core/utils/navigator'
|
||||
import './ace-config'
|
||||
import { VAceEditor } from 'vue3-ace-editor'
|
||||
import { PerfectScrollbarPlugin } from 'vue3-perfect-scrollbar'
|
||||
import { VTreeview } from 'vuetify/labs/VTreeview'
|
||||
import ToastPlugin from 'vue-toast-notification'
|
||||
import VuetifyUseDialog from 'vuetify-use-dialog'
|
||||
import VueApexCharts from 'vue3-apexcharts'
|
||||
@@ -49,6 +48,11 @@ async function initializeApp() {
|
||||
|
||||
// 注册全局组件
|
||||
initializeApp().then(() => {
|
||||
// 优先注册框架
|
||||
app
|
||||
.use(vuetify)
|
||||
|
||||
// 注册全局组件
|
||||
app
|
||||
.component('VAceEditor', VAceEditor)
|
||||
.component('VApexChart', VueApexCharts)
|
||||
@@ -60,12 +64,10 @@ initializeApp().then(() => {
|
||||
.component('VMediaInfoCard', MediaInfoCard)
|
||||
.component('VTorrentCard', TorrentCard)
|
||||
.component('VMediaIdSelector', MediaIdSelector)
|
||||
.component('VTreeview', VTreeview)
|
||||
.component('VPathField', PathField)
|
||||
|
||||
// 注册插件
|
||||
app
|
||||
.use(vuetify)
|
||||
.use(router)
|
||||
.use(store)
|
||||
.use(ToastPlugin, {
|
||||
|
||||
@@ -11,17 +11,12 @@ const activeTab = ref(route.query.tab)
|
||||
// 下载器
|
||||
const downloaders = ref<DownloaderConf[]>([])
|
||||
|
||||
// 获取启用的下载器
|
||||
const enabledDownloaders = computed(() => downloaders.value.filter(item => item.enabled))
|
||||
|
||||
// 调用API查询下载器设置
|
||||
async function loadDownloaderSetting() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/setting/Downloaders')
|
||||
if (result.data?.value && result.data.value.length > 0) {
|
||||
downloaders.value = result.data?.value ?? []
|
||||
if (!activeTab.value) activeTab.value = downloaders.value[0].name
|
||||
}
|
||||
downloaders.value = await api.get('download/clients')
|
||||
if (downloaders.value && downloaders.value.length > 0 && !activeTab.value)
|
||||
activeTab.value = downloaders.value[0].name
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
@@ -31,21 +26,21 @@ function jumpTab(tab: string) {
|
||||
router.push('/subscribe/movie?tab=' + tab)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadDownloaderSetting()
|
||||
onMounted(async () => {
|
||||
await loadDownloaderSetting()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="enabledDownloaders.length > 0">
|
||||
<div v-if="downloaders.length > 0">
|
||||
<VTabs v-model="activeTab">
|
||||
<VTab v-for="item in enabledDownloaders" :value="item.name" @to="jumpTab(item.name)">
|
||||
<VTab v-for="item in downloaders" :value="item.name" @to="jumpTab(item.name)">
|
||||
<span class="mx-5">{{ item.name }}</span>
|
||||
</VTab>
|
||||
</VTabs>
|
||||
|
||||
<VWindow v-model="activeTab" class="mt-5 disable-tab-transition" :touch="false">
|
||||
<VWindowItem v-for="item in enabledDownloaders" :value="item.name">
|
||||
<VWindowItem v-for="item in downloaders" :value="item.name">
|
||||
<transition name="fade-slide" appear>
|
||||
<DownloadingListView :name="item.name" />
|
||||
</transition>
|
||||
|
||||
@@ -446,31 +446,32 @@ onBeforeMount(async () => {
|
||||
</VWindow>
|
||||
</div>
|
||||
|
||||
<!-- 插件搜索图标 -->
|
||||
<VFab
|
||||
icon="mdi-magnify"
|
||||
color="info"
|
||||
location="bottom"
|
||||
size="x-large"
|
||||
fixed
|
||||
app
|
||||
appear
|
||||
@click="SearchDialog = true"
|
||||
:class="appMode ? 'mb-28' : 'mb-16'"
|
||||
/>
|
||||
<!-- 插件市场设置图标 -->
|
||||
<VFab
|
||||
icon="mdi-store-cog"
|
||||
color="warning"
|
||||
location="bottom"
|
||||
size="x-large"
|
||||
fixed
|
||||
app
|
||||
appear
|
||||
@click="MarketSettingDialog = true"
|
||||
:class="{ 'mb-12': appMode }"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<!-- 插件搜索图标 -->
|
||||
<VFab
|
||||
icon="mdi-magnify"
|
||||
color="info"
|
||||
location="bottom"
|
||||
size="x-large"
|
||||
fixed
|
||||
app
|
||||
appear
|
||||
@click="SearchDialog = true"
|
||||
:class="appMode ? 'mb-28' : 'mb-16'"
|
||||
/>
|
||||
<!-- 插件市场设置图标 -->
|
||||
<VFab
|
||||
icon="mdi-store-cog"
|
||||
color="warning"
|
||||
location="bottom"
|
||||
size="x-large"
|
||||
fixed
|
||||
app
|
||||
appear
|
||||
@click="MarketSettingDialog = true"
|
||||
:class="{ 'mb-12': appMode }"
|
||||
/>
|
||||
</div>
|
||||
<!-- 插件市场设置窗口 -->
|
||||
<PluginMarketSettingDialog
|
||||
v-if="MarketSettingDialog"
|
||||
|
||||
@@ -28,6 +28,7 @@ const currentHistory = ref<TransferHistory>()
|
||||
|
||||
// 重新整理IDS
|
||||
const redoIds = ref<number[]>([])
|
||||
const redoTargetStorage = ref<string>()
|
||||
|
||||
// 已选中的数据
|
||||
const selected = ref<TransferHistory[]>([])
|
||||
@@ -301,6 +302,7 @@ const dropdownItems = ref([
|
||||
prependIcon: 'mdi-redo-variant',
|
||||
click: (item: TransferHistory) => {
|
||||
redoIds.value = [item.id]
|
||||
redoTargetStorage.value = item.dest_storage
|
||||
redoDialog.value = true
|
||||
},
|
||||
},
|
||||
@@ -479,7 +481,7 @@ onMounted(fetchData)
|
||||
</VCard>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<span>
|
||||
<div>
|
||||
<VFab
|
||||
v-if="selected.length > 0"
|
||||
icon="mdi-trash-can-outline"
|
||||
@@ -503,7 +505,7 @@ onMounted(fetchData)
|
||||
appear
|
||||
@click="retransferBatch"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<!-- 底部弹窗 -->
|
||||
<VBottomSheet v-model="deleteConfirmDialog" inset>
|
||||
<VCard class="text-center rounded-t">
|
||||
@@ -530,6 +532,7 @@ onMounted(fetchData)
|
||||
v-if="redoDialog"
|
||||
v-model="redoDialog"
|
||||
:logids="redoIds"
|
||||
:target_storage="redoTargetStorage"
|
||||
@done="transferDone"
|
||||
@close="redoDialog = false"
|
||||
/>
|
||||
|
||||
@@ -3,18 +3,46 @@
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import { copyToClipboard } from '@/@core/utils/navigator'
|
||||
import draggable from 'vuedraggable'
|
||||
import { VRow } from 'vuetify/lib/components/index.mjs'
|
||||
import api from '@/api'
|
||||
import { CustomRule, FilterRuleGroup } from '@/api/types'
|
||||
import CustomerRuleCard from '@/components/cards/CustomRuleCard.vue'
|
||||
import FilterRuleGroupCard from '@/components/cards/FilterRuleGroupCard.vue'
|
||||
import ImportCodeDialog from '@/components/dialog/ImportCodeDialog.vue'
|
||||
import internal from 'stream'
|
||||
|
||||
// 自定义规则列表
|
||||
const customRules = ref<CustomRule[]>([])
|
||||
// 自定义规则类型,仅用于导入判断
|
||||
const customRulesType = ref<CustomRule>({
|
||||
// 规则ID
|
||||
id: '',
|
||||
// 名称
|
||||
name: '',
|
||||
// 包含
|
||||
include: '',
|
||||
// 排除
|
||||
exclude: '',
|
||||
// 大小范围
|
||||
size_range: '',
|
||||
// 最少做种人数
|
||||
seeders: '',
|
||||
// 发布时间
|
||||
publish_time: '',
|
||||
})
|
||||
|
||||
// 所有规则组列表
|
||||
const filterRuleGroups = ref<FilterRuleGroup[]>([])
|
||||
// 规则组类型,仅用于导入判断
|
||||
const filterRuleGroupsType = ref<FilterRuleGroup>({
|
||||
// 名称
|
||||
name: '',
|
||||
// 规则串
|
||||
rule_string: '',
|
||||
// 适用类媒体类型 None-全部 电影/电视剧
|
||||
media_type: '',
|
||||
// # 适用媒体类别 None-全部 对应二级分类
|
||||
category: '',
|
||||
})
|
||||
|
||||
// 种子优先规则
|
||||
const selectedTorrentPriority = ref<string>('seeder')
|
||||
@@ -25,9 +53,6 @@ const mediaCategories = ref<{ [key: string]: any }>({})
|
||||
// 导入代码弹窗
|
||||
const importCodeDialog = ref(false)
|
||||
|
||||
// 导入的代码
|
||||
const importCodeString = ref('')
|
||||
|
||||
// 导入代码类型
|
||||
const importCodeType = ref('')
|
||||
|
||||
@@ -171,48 +196,96 @@ function shareRules(rules: CustomRule[] | FilterRuleGroup[]) {
|
||||
}
|
||||
}
|
||||
|
||||
// 导入规则
|
||||
// 打开弹窗
|
||||
async function importRules(ruleType: string) {
|
||||
importCodeType.value = ruleType
|
||||
importCodeString.value = ''
|
||||
importCodeDialog.value = true
|
||||
}
|
||||
|
||||
// 监听导入代码变化
|
||||
watchEffect(() => {
|
||||
if (!importCodeString.value) return
|
||||
// 导入代码需要json格式
|
||||
// 保存导入的代码
|
||||
function saveCodeString(type: string, codeString: any) {
|
||||
// codeString从子组件传递过来,从对象转换为JSON
|
||||
let parsedCode
|
||||
try {
|
||||
if (importCodeType.value === 'custom') {
|
||||
// 将导入的代码转换为规则卡片,并追加到已有的 customRules
|
||||
const newCustomRules = JSON.parse(importCodeString.value).map((item: any) => {
|
||||
return {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
include: item.include,
|
||||
exclude: item.exclude,
|
||||
publish_time: item.publish_time,
|
||||
seeders: item.seeders,
|
||||
size_range: item.size_range,
|
||||
}
|
||||
})
|
||||
customRules.value = [...customRules.value, ...newCustomRules] // 合并已有的和新导入的规则
|
||||
} else if (importCodeType.value === 'group') {
|
||||
// 将导入的代码转换为规则卡片,并追加到已有的 filterRuleGroups
|
||||
const newFilterRuleGroups = JSON.parse(importCodeString.value).map((item: any) => {
|
||||
return {
|
||||
name: item.name,
|
||||
rule_string: item.rule_string,
|
||||
media_type: item.media_type,
|
||||
category: item.category,
|
||||
}
|
||||
})
|
||||
filterRuleGroups.value = [...filterRuleGroups.value, ...newFilterRuleGroups] // 合并已有的和新导入的规则
|
||||
}
|
||||
} catch (error) {
|
||||
$toast.error('规则导入失败!')
|
||||
parsedCode = JSON.parse(codeString.value)
|
||||
} catch (e) {
|
||||
$toast.error('导入规则失败!无法解析输入的数据!')
|
||||
console.error(e)
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
// 更新数据
|
||||
try {
|
||||
if (type === 'custom') {
|
||||
for (const value of parsedCode) {
|
||||
if (!validateValueAgainstInterface(value, customRulesType)) return false
|
||||
}
|
||||
const newCustomRules = extractCustomRules(parsedCode) || []
|
||||
customRules.value = [...customRules.value, ...newCustomRules]
|
||||
} else if (type === 'group') {
|
||||
for (const value of parsedCode) {
|
||||
if (!validateValueAgainstInterface(value, filterRuleGroupsType)) return false
|
||||
}
|
||||
const newFilterRuleGroups = extractFilterRuleGroups(parsedCode) || []
|
||||
filterRuleGroups.value = [...filterRuleGroups.value, ...newFilterRuleGroups]
|
||||
} else {
|
||||
$toast.error('导入规则失败!未知的数据类型!')
|
||||
}
|
||||
} catch (e) {
|
||||
$toast.error('导入规则失败!')
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
// 赋值自定义规则,避免存在多余的属性
|
||||
function extractCustomRules(value: any) {
|
||||
try {
|
||||
return value.map((item: any) => {
|
||||
return {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
include: item.include,
|
||||
exclude: item.exclude,
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
// 赋值规则组,避免存在多余的属性
|
||||
function extractFilterRuleGroups(value: any) {
|
||||
try {
|
||||
return value.map((item: any) => {
|
||||
return {
|
||||
name: item.name,
|
||||
rule_string: item.rule_string,
|
||||
media_type: item.media_type,
|
||||
category: item.category,
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
function validateValueAgainstInterface(value: any, interfaceDefinition: any): boolean {
|
||||
console.log(value)
|
||||
console.log(interfaceDefinition)
|
||||
try {
|
||||
// 循环判断是否命中全部接口定义,暂时只检查是否存在,不检查是否多出
|
||||
for (const key in interfaceDefinition.value) {
|
||||
if (value[key] === undefined) {
|
||||
$toast.error(`导入规则失败!输入了不符合要求的数据!`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 规则变化时赋值
|
||||
function onRuleChange(rule: CustomRule, id: string) {
|
||||
@@ -368,12 +441,17 @@ onMounted(() => {
|
||||
</div>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
<VDialog v-model="importCodeDialog" width="60rem" scrollable>
|
||||
<ImportCodeDialog v-model="importCodeString" title="导入规则" @close="importCodeDialog = false" />
|
||||
</VDialog>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<ImportCodeDialog
|
||||
v-if="importCodeDialog"
|
||||
v-model="importCodeDialog"
|
||||
:title="`导入${importCodeType === 'custom' ? '自定义规则' : '规则组'}`"
|
||||
:dataType="importCodeType"
|
||||
@close="importCodeDialog = false"
|
||||
@save="saveCodeString"
|
||||
/>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VCard>
|
||||
|
||||
@@ -105,8 +105,10 @@ async function saveSiteSetting(value: { [key: string]: any }) {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post('system/env', value)
|
||||
if (result.success) {
|
||||
$toast.success('保存设置成功')
|
||||
$toast.success('保存站点设置成功')
|
||||
await reloadSystem()
|
||||
} else {
|
||||
$toast.error('站点设置保存失败!')
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
|
||||
@@ -28,6 +28,7 @@ const SystemSettings = ref<any>({
|
||||
GLOBAL_IMAGE_CACHE: false,
|
||||
BIG_MEMORY_MODE: false,
|
||||
DB_WAL_ENABLE: false,
|
||||
ENCODING_DETECTION_PERFORMANCE_MODE: true,
|
||||
// 媒体
|
||||
TMDB_API_DOMAIN: null,
|
||||
TMDB_IMAGE_DOMAIN: null,
|
||||
@@ -700,7 +701,7 @@ onDeactivated(() => {
|
||||
<VWindowItem value="dev">
|
||||
<div>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VCol cols="12" md="4">
|
||||
<VSwitch
|
||||
v-model="SystemSettings.Advanced.DEBUG"
|
||||
label="DEBUG日志"
|
||||
@@ -708,7 +709,7 @@ onDeactivated(() => {
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VCol cols="12" md="4">
|
||||
<VSwitch
|
||||
v-model="SystemSettings.Advanced.PLUGIN_AUTO_RELOAD"
|
||||
label="插件热加载"
|
||||
@@ -716,6 +717,14 @@ onDeactivated(() => {
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VSwitch
|
||||
v-model="SystemSettings.Advanced.ENCODING_DETECTION_PERFORMANCE_MODE"
|
||||
label="编码探测性能模式"
|
||||
hint="优先提升探测效率,但可能降低编码探测的准确性"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</div>
|
||||
</VWindowItem>
|
||||
|
||||
@@ -100,29 +100,31 @@ onActivated(async () => {
|
||||
/>
|
||||
</VPullToRefresh>
|
||||
<!-- 底部操作按钮 -->
|
||||
<VFab
|
||||
v-if="store.state.auth.superUser"
|
||||
icon="mdi-clipboard-edit"
|
||||
location="bottom"
|
||||
size="x-large"
|
||||
fixed
|
||||
app
|
||||
appear
|
||||
@click="subscribeEditDialog = true"
|
||||
:class="{ 'mb-12': appMode }"
|
||||
/>
|
||||
<VFab
|
||||
v-if="store.state.auth.superUser"
|
||||
icon="mdi-history"
|
||||
color="info"
|
||||
location="bottom"
|
||||
:class="appMode ? 'mb-28' : 'mb-16'"
|
||||
size="x-large"
|
||||
fixed
|
||||
app
|
||||
appear
|
||||
@click="historyDialog = true"
|
||||
/>
|
||||
<div>
|
||||
<VFab
|
||||
v-if="store.state.auth.superUser"
|
||||
icon="mdi-clipboard-edit"
|
||||
location="bottom"
|
||||
size="x-large"
|
||||
fixed
|
||||
app
|
||||
appear
|
||||
@click="subscribeEditDialog = true"
|
||||
:class="{ 'mb-12': appMode }"
|
||||
/>
|
||||
<VFab
|
||||
v-if="store.state.auth.superUser"
|
||||
icon="mdi-history"
|
||||
color="info"
|
||||
location="bottom"
|
||||
:class="appMode ? 'mb-28' : 'mb-16'"
|
||||
size="x-large"
|
||||
fixed
|
||||
app
|
||||
appear
|
||||
@click="historyDialog = true"
|
||||
/>
|
||||
</div>
|
||||
<!-- 订阅编辑弹窗 -->
|
||||
<SubscribeEditDialog
|
||||
v-if="subscribeEditDialog"
|
||||
|
||||
@@ -129,6 +129,26 @@ const targets = ref<Address[]>([
|
||||
message: '未测试',
|
||||
btndisable: false,
|
||||
},
|
||||
{
|
||||
image: github,
|
||||
name: 'api.github.com',
|
||||
url: 'https://api.github.com',
|
||||
proxy: true,
|
||||
status: 'Normal',
|
||||
time: '',
|
||||
message: '未测试',
|
||||
btndisable: false,
|
||||
},
|
||||
{
|
||||
image: github,
|
||||
name: 'raw.githubusercontent.com',
|
||||
url: 'https://raw.githubusercontent.com',
|
||||
proxy: true,
|
||||
status: 'Normal',
|
||||
time: '',
|
||||
message: '未测试',
|
||||
btndisable: false,
|
||||
},
|
||||
])
|
||||
|
||||
const resolveStatusColor: Status = {
|
||||
|
||||
@@ -138,6 +138,15 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
build: {
|
||||
minify: 'terser',
|
||||
terserOptions: {
|
||||
compress: {
|
||||
// 控制console.log()是否被移除,生产环境建议移除,存在内存泄漏风险
|
||||
drop_console: true,
|
||||
// 控制debugger是否被移除,酌情处理
|
||||
drop_debugger: false,
|
||||
},
|
||||
},
|
||||
chunkSizeWarningLimit: 5000,
|
||||
cssCodeSplit: false,
|
||||
rollupOptions: {
|
||||
|
||||
10
yarn.lock
10
yarn.lock
@@ -7040,6 +7040,16 @@ terser@^5.17.4:
|
||||
commander "^2.20.0"
|
||||
source-map-support "~0.5.20"
|
||||
|
||||
terser@^5.36.0:
|
||||
version "5.36.0"
|
||||
resolved "https://registry.yarnpkg.com/terser/-/terser-5.36.0.tgz#8b0dbed459ac40ff7b4c9fd5a3a2029de105180e"
|
||||
integrity sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w==
|
||||
dependencies:
|
||||
"@jridgewell/source-map" "^0.3.3"
|
||||
acorn "^8.8.2"
|
||||
commander "^2.20.0"
|
||||
source-map-support "~0.5.20"
|
||||
|
||||
text-table@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
|
||||
|
||||
Reference in New Issue
Block a user