Merge pull request #225 from Aqr-K/v2-settings

feat(settings): 配置中心基本功能内置化,修复部分bug
This commit is contained in:
jxxghp
2024-11-04 10:20:16 +08:00
committed by GitHub
22 changed files with 1797 additions and 573 deletions

View File

@@ -2,6 +2,7 @@
import { CustomRule } from '@/api/types'
import { useToast } from 'vue-toast-notification'
import filter_svg from '@images/svg/filter.svg'
import { cloneDeep } from 'lodash'
// 输入参数
const props = defineProps({
@@ -37,50 +38,93 @@ const ruleInfo = ref<CustomRule>({
publish_time: '',
})
// 规则ID
const ruleId = ref('')
// 规则名称
const ruleName = ref('')
// 内置的规则
const selectFilterOptions = ref<{ [key: string]: string }[]>([
{ title: '特效字幕', value: ' SPECSUB ' },
{ title: '中文字幕', value: ' CNSUB ' },
{ title: '国语配音', value: ' CNVOI ' },
{ title: '官种', value: ' GZ ' },
{ title: '排除: 国语配音', value: ' !CNVOI ' },
{ title: '粤语配音', value: ' HKVOI ' },
{ title: '排除: 粤语配音', value: ' !HKVOI ' },
{ title: '促销: 免费', value: ' FREE ' },
{ title: '分辨率: 4K', value: ' 4K ' },
{ title: '分辨率: 1080P', value: ' 1080P ' },
{ title: '分辨率: 720P', value: ' 720P ' },
{ title: '排除: 720P', value: ' !720P ' },
{ title: '质量: 蓝光原盘', value: ' BLU ' },
{ title: '排除: 蓝光原盘', value: ' !BLU ' },
{ title: '质量: BLURAY', value: ' BLURAY ' },
{ title: '排除: BLURAY', value: ' !BLURAY ' },
{ title: '质量: UHD', value: ' UHD ' },
{ title: '排除: UHD', value: ' !UHD ' },
{ title: '质量: REMUX', value: ' REMUX ' },
{ title: '排除: REMUX', value: ' !REMUX ' },
{ title: '质量: WEB-DL', value: ' WEBDL ' },
{ title: '排除: WEB-DL', value: ' !WEBDL ' },
{ title: '质量: 60fps', value: ' 60FPS ' },
{ title: '排除: 60fps', value: ' !60FPS ' },
{ title: '编码: H265', value: ' H265 ' },
{ title: '排除: H265', value: ' !H265 ' },
{ title: '编码: H264', value: ' H264 ' },
{ title: '排除: H264', value: ' !H264 ' },
{ title: '效果: 杜比视界', value: ' DOLBY ' },
{ title: '排除: 杜比视界', value: ' !DOLBY ' },
{ title: '效果: 杜比全景声', value: ' ATMOS ' },
{ title: '排除: 杜比全景声', value: ' !ATMOS ' },
{ title: '效果: HDR', value: ' HDR ' },
{ title: '排除: HDR', value: ' !HDR ' },
{ title: '效果: SDR', value: ' SDR ' },
{ title: '排除: SDR', value: ' !SDR ' },
{ title: '效果: 3D', value: ' 3D ' },
{ title: '排除: 3D', value: ' !3D ' },
])
// 打开详情弹窗
function openRuleInfoDialog() {
ruleInfo.value = props.rule
ruleId.value = props.rule.id
ruleName.value = props.rule.name
// 深复制
ruleInfo.value = cloneDeep(props.rule)
ruleInfoDialog.value = true
}
// 保存详情数据
function saveRuleInfo() {
// 有空值
if (!ruleId.value && !ruleName.value) {
if (!ruleId.value && ruleName.value) {
if (!ruleInfo.value.id || !ruleInfo.value.name) {
if (!ruleInfo.value.id && ruleInfo.value.name) {
$toast.error('规则ID不能为空')
}
if (ruleId.value && !ruleName.value) {
if (ruleInfo.value.id && !ruleInfo.value.name) {
$toast.error('规则名称不能为空')
}
if (!ruleId.value && !ruleName.value) {
if (!ruleInfo.value.id && !ruleInfo.value.name) {
$toast.error('规则ID和规则名称不能为空')
}
return
}
// 检查ID是否在内置的规则中
if (selectFilterOptions.value.find(option => option.value === ruleInfo.value.id)) {
$toast.error('当前规则ID已被内置规则占用请替换')
return
}
// 检查规则名称是否在内置的规则中
if (selectFilterOptions.value.find(option => option.title === ruleInfo.value.name)) {
$toast.error('当前规则名称已被内置规则占用,请替换')
return
}
// ID已存在
if (ruleId.value !== props.rule.id && props.rules.find(rule => rule.id === ruleId.value)) {
$toast.error(`规则ID【${ruleId.value}】已存在,请替换`)
if (ruleInfo.value.id !== props.rule.id && props.rules.find(rule => rule.id === ruleInfo.value.id)) {
$toast.error(`规则ID【${ruleInfo.value.id}】已存在,请替换`)
return
}
// 规则名称已存在
if (ruleName.value !== props.rule.name && props.rules.find(rule => rule.name === ruleName.value)) {
$toast.error(`规则名称【${ruleName.value}】已存在,请替换`)
if (ruleInfo.value.name !== props.rule.name && props.rules.find(rule => rule.name === ruleInfo.value.name)) {
$toast.error(`规则名称【${ruleInfo.value.name}】已存在,请替换`)
return
}
// 保存数据
ruleInfoDialog.value = false
ruleInfo.value.id = ruleId.value
ruleInfo.value.name = ruleName.value
emit('change', ruleInfo.value)
emit('change', ruleInfo.value, props.rule.id)
emit('done')
}
@@ -107,7 +151,7 @@ function onClose() {
<VImg :src="filter_svg" cover class="mt-7" max-width="3rem" />
</VCardText>
</VCard>
<VDialog v-model="ruleInfoDialog" scrollable max-width="40rem" persistent >
<VDialog v-model="ruleInfoDialog" scrollable max-width="40rem" persistent>
<VCard :title="`${props.rule.id} - 配置`" class="rounded-t">
<DialogCloseBtn v-model="ruleInfoDialog" />
<VDivider />
@@ -116,7 +160,7 @@ function onClose() {
<VRow>
<VCol cols="12" md="6">
<VTextField
v-model="ruleId"
v-model="ruleInfo.id"
label="规则ID"
placeholder="必填不可与其他规则ID重名"
hint="字符与数字组合,不能含空格"
@@ -126,7 +170,7 @@ function onClose() {
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="ruleName"
v-model="ruleInfo.name"
label="规则名称"
placeholder="必填;不可与其他规则名称重名"
hint="使用别名便于区分规则"

View File

@@ -6,6 +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";
// 定义输入
const props = defineProps({
@@ -44,9 +45,6 @@ const download_rate = ref(0)
// 下载器详情弹窗
const downloaderInfoDialog = ref(false)
// 下载器名称
const downloaderName = ref('')
// 下载器详情
const downloaderInfo = ref<DownloaderConf>({
name: '',
@@ -84,21 +82,21 @@ async function loadDownloaderInfo() {
// 打开详情弹窗
function openDownloaderInfoDialog() {
downloaderInfo.value = props.downloader
downloaderName.value = props.downloader.name
// 深复制
downloaderInfo.value = cloneDeep(props.downloader)
downloaderInfoDialog.value = true
}
// 保存详情数据
function saveDownloaderInfo() {
// 为空不保存,跳出警告框
if (!downloaderName.value) {
if (!downloaderInfo.value.name) {
$toast.error('名称不能为空,请输入后再确定')
return
}
// 重名判断
if (props.downloaders.some(item => item.name === downloaderName.value && item !== props.downloader)) {
$toast.error(`${downloaderName.value}】已存在,请替换为其他名称`)
if (props.downloaders.some(item => item.name === downloaderInfo.value.name && item !== props.downloader)) {
$toast.error(`${downloaderInfo.value.name}】已存在,请替换为其他名称`)
return
}
// 默认下载器去重
@@ -106,14 +104,13 @@ function saveDownloaderInfo() {
props.downloaders.forEach(item => {
if (item.default && item !== props.downloader) {
item.default = false
$toast.info(`${item.name}】存在默认下载器,已替换成【${downloaderName.value}`)
$toast.info(`${item.name}】存在默认下载器,已替换成【${downloaderInfo.value.name}`)
}
})
}
// 执行保存
downloaderInfoDialog.value = false
downloaderInfo.value.name = downloaderName.value
emit('change', downloaderInfo.value)
emit('change', downloaderInfo.value, props.downloader.name)
emit('done')
}
@@ -171,7 +168,7 @@ onUnmounted(() => {
</div>
</div>
<div class="h-20">
<VImg :src="getIcon" cover class="mt-7" max-width="3rem" min-width="3rem" />
<VImg :src="getIcon" cover class="mt-7 me-3" max-width="3rem" min-width="3rem" />
</div>
</VCardText>
</VCard>
@@ -192,7 +189,7 @@ onUnmounted(() => {
<VRow v-if="downloaderInfo.type == 'qbittorrent'">
<VCol cols="12" md="6">
<VTextField
v-model="downloaderName"
v-model="downloaderInfo.name"
label="名称"
placeholder="必填;不可与其他名称重名"
hint="下载器的别名"
@@ -270,7 +267,7 @@ onUnmounted(() => {
<VRow v-if="downloaderInfo.type == 'transmission'">
<VCol cols="12" md="6">
<VTextField
v-model="downloaderName"
v-model="downloaderInfo.name"
label="名称"
placeholder="必填;不可与其他名称重名"
hint="下载器的别名"

View File

@@ -6,6 +6,7 @@ import FilterRuleCard from '@/components/cards/FilterRuleCard.vue'
import { useToast } from 'vue-toast-notification'
import ImportCodeDialog from '@/components/dialog/ImportCodeDialog.vue'
import filter_group_svg from '@images/svg/filter-group.svg'
import { cloneDeep } from 'lodash'
// 输入参数
const props = defineProps({
@@ -53,9 +54,6 @@ const groupInfo = ref<FilterRuleGroup>({
category: props.group?.category,
})
// 规则组名称
const groupName = ref('')
// 媒体类型字典
const mediaTypeItems = [
{ title: '通用', value: '' },
@@ -88,15 +86,12 @@ function updateFilterCardValue(pri: string, rules: string[]) {
// 移除卡片
function filterCardClose(pri: string) {
// 将pri对应的卡片从列表中删除并更新剩余卡片的序号
const updatedCards = filterRuleCards.value
filterRuleCards.value = filterRuleCards.value
.filter(card => card.pri !== pri)
.map((card, index) => {
card.pri = (index + 1).toString()
return card
})
// 更新 filterRuleCards.value
filterRuleCards.value = updatedCards
}
// 分享规则
@@ -163,8 +158,8 @@ function dragOrderEnd() {
// 打开详情弹窗
function opengroupInfoDialog() {
groupInfo.value = props.group
groupName.value = props.group.name
// 深复制
groupInfo.value = cloneDeep(props.group)
if (props.group.rule_string) {
filterRuleCards.value = props.group.rule_string.split('>').map((group: string, index: number) => {
return {
@@ -177,26 +172,25 @@ function opengroupInfoDialog() {
}
// 保存详情数据
function savegroupInfo() {
function saveGroupInfo() {
// 为空
if (!groupName.value) {
if (!groupInfo.value.name) {
$toast.error('规则组名称不能为空')
return
}
// 重名判断
if (props.groups.some(item => item.name === groupName.value && item !== props.group)) {
$toast.error(`规则组名称【${groupName.value}】已存在,请替换`)
if (props.groups.some(item => item.name === groupInfo.value.name && item !== props.group)) {
$toast.error(`规则组名称【${groupInfo.value.name}】已存在,请替换`)
return
}
// 保存
groupInfoDialog.value = false
groupInfo.value.name = groupName.value
// 更新到 groupInfo的rule_string
groupInfo.value.rule_string = filterRuleCards.value
.filter(card => card.rules.length > 0)
.map(card => card.rules.join('&'))
.join('>')
emit('change', groupInfo.value)
emit('change', groupInfo.value, props.group.name)
emit('done')
}
@@ -226,7 +220,7 @@ function onClose() {
<VImg :src="filter_group_svg" cover class="mt-10" max-width="3rem" />
</VCardText>
</VCard>
<VDialog v-model="groupInfoDialog" scrollable max-width="80rem" persistent >
<VDialog v-model="groupInfoDialog" scrollable max-width="80rem" persistent>
<VCard :title="`${props.group.name} - 配置`" class="rounded-t">
<DialogCloseBtn v-model="groupInfoDialog" />
<VDivider />
@@ -234,7 +228,7 @@ function onClose() {
<VRow>
<VCol cols="12" md="6">
<VTextField
v-model="groupName"
v-model="groupInfo.name"
label="规则组名称"
placeholder="必填;不可与其他规则组重名"
hint="自定义规则组名称"
@@ -297,7 +291,7 @@ function onClose() {
<VIcon icon="mdi-share" />
</VBtn>
<VSpacer />
<VBtn @click="savegroupInfo" variant="elevated" prepend-icon="mdi-content-save" class="px-5"> 确定 </VBtn>
<VBtn @click="saveGroupInfo" variant="elevated" prepend-icon="mdi-content-save" class="px-5"> 确定 </VBtn>
</VCardActions>
</VCard>
</VDialog>

View File

@@ -5,6 +5,7 @@ import emby_image from '@images/logos/emby.png'
import jellyfin_image from '@images/logos/jellyfin.png'
import plex_image from '@images/logos/plex.png'
import api from '@/api'
import { cloneDeep } from 'lodash'
// 定义输入
const props = defineProps({
@@ -56,9 +57,6 @@ const librariesOptions = ref<{ title: string; value: string | undefined }[]>([
// 媒体服务器详情弹窗
const mediaServerInfoDialog = ref(false)
// 媒体服务器名称
const mediaServerName = ref('')
// 媒体服务器详情
const mediaServerInfo = ref<MediaServerConf>({
name: '',
@@ -70,8 +68,8 @@ const mediaServerInfo = ref<MediaServerConf>({
// 打开详情弹窗
function openMediaServerInfoDialog() {
loadLibrary(props.mediaserver.name)
mediaServerInfo.value = props.mediaserver
mediaServerName.value = props.mediaserver.name
// 深复制
mediaServerInfo.value = cloneDeep(props.mediaserver)
mediaServerInfoDialog.value = true
if (!props.mediaserver.sync_libraries) {
mediaServerInfo.value.sync_libraries = ['all']
@@ -81,19 +79,18 @@ function openMediaServerInfoDialog() {
// 保存详情数据
function saveMediaServerInfo() {
// 为空不保存,跳出警告框
if (!mediaServerName.value) {
if (!mediaServerInfo.value.name) {
$toast.error('名称不能为空,请输入后再确定')
return
}
// 重名判断
if (props.mediaservers.some(item => item.name === mediaServerName.value && item !== props.mediaserver)) {
$toast.error(`${mediaServerName.value}】已存在,请替换为其他名称`)
if (props.mediaservers.some(item => item.name === mediaServerInfo.value.name && item !== props.mediaserver)) {
$toast.error(`${mediaServerInfo.value.name}】已存在,请替换为其他名称`)
return
}
// 执行保存
mediaServerInfoDialog.value = false
mediaServerInfo.value.name = mediaServerName.value
emit('change', mediaServerInfo.value)
emit('change', mediaServerInfo.value, props.mediaserver.name)
emit('done')
}
@@ -185,7 +182,7 @@ onMounted(() => {
</span>
</div>
</div>
<VImg :src="getIcon" cover class="mt-5 me-3" max-width="3rem" min-width="3rem" />
<VImg :src="getIcon" cover class="mt-7 me-3" max-width="3rem" min-width="3rem" />
</VCardText>
</VCard>
<VDialog v-model="mediaServerInfoDialog" scrollable max-width="40rem" persistent>
@@ -202,7 +199,7 @@ onMounted(() => {
<VRow v-if="mediaServerInfo.type == 'emby'">
<VCol cols="12" md="6">
<VTextField
v-model="mediaServerName"
v-model="mediaServerInfo.name"
label="名称"
placeholder="必填;不可与其他名称重名"
hint="媒体服务器的别名"
@@ -243,7 +240,7 @@ onMounted(() => {
<VRow v-if="mediaServerInfo.type == 'jellyfin'">
<VCol cols="12" md="6">
<VTextField
v-model="mediaServerName"
v-model="mediaServerInfo.name"
label="名称"
placeholder="必填;不可与其他名称重名"
hint="媒体服务器的别名"
@@ -284,7 +281,7 @@ onMounted(() => {
<VRow v-if="mediaServerInfo.type == 'plex'">
<VCol cols="12" md="6">
<VTextField
v-model="mediaServerName"
v-model="mediaServerInfo.name"
label="名称"
placeholder="必填;不可与其他名称重名"
hint="媒体服务器的别名"

View File

@@ -7,6 +7,7 @@ import synologychat_image from '@images/logos/synologychat.png'
import slack_image from '@images/logos/slack.webp'
import chrome_image from '@images/logos/chrome.png'
import { useToast } from 'vue-toast-notification'
import { cloneDeep } from "lodash"
// 定义输入
const props = defineProps({
@@ -31,9 +32,6 @@ const $toast = useToast()
// 通知详情弹窗
const notificationInfoDialog = ref(false)
// 通知名称
const notificationName = ref('')
// 通知详情
const notificationInfo = ref<NotificationConf>({
name: '',
@@ -66,26 +64,26 @@ const notificationTypes = [
// 打开详情弹窗
function openNotificationInfoDialog() {
notificationInfo.value = props.notification
notificationName.value = props.notification.name
// 替换成深复制,避免修改时影响原数据
notificationInfo.value = cloneDeep(props.notification)
console.log(`当前卡片的通知信息:${JSON.stringify(notificationInfo.value)}`)
notificationInfoDialog.value = true
}
// 保存详情数据
function saveNotificationInfo() {
// 为空不保存,跳出警告框
if (!notificationName.value) {
if (!notificationInfo.value.name) {
$toast.error('名称不能为空,请输入后再确定')
return
}
// 重名判断
if (props.notifications.some(item => item.name === notificationName.value && item !== props.notification)) {
$toast.error(`${notificationName.value}】已存在,请替换为其他名称`)
if (props.notifications.some(item => item.name === notificationInfo.value.name && item !== props.notification)) {
$toast.error(`通知渠道${notificationInfo.value.name}】已存在,请替换`)
return
}
notificationInfoDialog.value = false
notificationInfo.value.name = notificationName.value
emit('change', notificationInfo.value)
emit('change', notificationInfo.value, props.notification.name)
emit('done')
}
@@ -131,7 +129,7 @@ function onClose() {
</div>
<div class="text-body-1 mb-3">{{ notificationTypeNames[notification.type] }}</div>
</div>
<VImg :src="getIcon" cover class="mt-5 me-7" max-width="3rem" />
<VImg :src="getIcon" cover class="mt-7 me-3" max-width="3rem" />
</VCardText>
</VCard>
<VDialog v-model="notificationInfoDialog" scrollable max-width="40rem" persistent>
@@ -160,7 +158,7 @@ function onClose() {
<VRow v-if="notificationInfo.type == 'wechat'">
<VCol cols="12" md="6">
<VTextField
v-model="notificationName"
v-model="notificationInfo.name"
label="名称"
placeholder="别名"
hint="通知渠道的别名"
@@ -228,7 +226,7 @@ function onClose() {
<VRow v-if="notificationInfo.type == 'telegram'">
<VCol cols="12" md="6">
<VTextField
v-model="notificationName"
v-model="notificationInfo.name"
label="名称"
placeholder="别名"
hint="通知渠道的别名"
@@ -273,7 +271,7 @@ function onClose() {
<VRow v-if="notificationInfo.type == 'slack'">
<VCol cols="12" md="6">
<VTextField
v-model="notificationName"
v-model="notificationInfo.name"
label="名称"
placeholder="别名"
hint="通知渠道的别名"
@@ -311,7 +309,7 @@ function onClose() {
<VRow v-if="notificationInfo.type == 'synologychat'">
<VCol cols="12" md="6">
<VTextField
v-model="notificationName"
v-model="notificationInfo.name"
label="名称"
placeholder="别名"
hint="通知渠道的别名"
@@ -338,7 +336,7 @@ function onClose() {
<VRow v-if="notificationInfo.type == 'vocechat'">
<VCol cols="12" md="6">
<VTextField
v-model="notificationName"
v-model="notificationInfo.name"
label="名称"
placeholder="别名"
hint="通知渠道的别名"
@@ -374,7 +372,7 @@ function onClose() {
<VRow v-if="notificationInfo.type == 'webpush'">
<VCol cols="12" md="6">
<VTextField
v-model="notificationName"
v-model="notificationInfo.name"
label="名称"
placeholder="别名"
hint="通知渠道的别名"

View File

@@ -0,0 +1,98 @@
<script setup lang="ts">
import { cloneDeep } from "lodash"
const props = defineProps({
AdvancedNetworkSettings: Object as any,
})
// 高级设置默认值,使用深复制,避免引用传递
const AdvancedSettings = ref(cloneDeep(props.AdvancedNetworkSettings))
// 定义触发的自定义事件
const emit = defineEmits(['change', 'close'])
// 保存高级设置
function saveAdvancedSettings() {
emit('change', AdvancedSettings.value, 'AdvancedNetwork')
}
// 恢复默认值
function loadAdvancedSettings() {
// 将AdvancedNetworkSettings中部分值赋值为默认值
AdvancedSettings.value = {
OCR_HOST: 'https://movie-pilot.org',
DOH_RESOLVERS: '1.0.0.1,1.1.1.1,9.9.9.9,149.112.112.112',
DOH_DOMAINS:
'api.themoviedb.org,api.tmdb.org,webservice.fanart.tv,api.github.com,github.com,raw.githubusercontent.com,api.telegram.org',
}
}
</script>
<template>
<VDialog scrollable max-width="60rem" persistent>
<VCard>
<VCardItem>
<VCardTitle>高级网络设置</VCardTitle>
<VCardSubtitle>修改前请先了解清楚这些设置的作用</VCardSubtitle>
</VCardItem>
<DialogCloseBtn @click="emit('close')" />
<VDivider />
<VCardText>
<VForm>
<VRow>
<VCol cols="12">
<VTextField
v-model="AdvancedSettings.OCR_HOST"
label="验证码识别服务器"
hint="用于识别验证码"
persistent-hint
clearable
active
/>
</VCol>
<VCol cols="12">
<VTextarea
v-model="AdvancedSettings.DOH_RESOLVERS"
label="DOH 服务器"
placeholder="格式https://dns.google/dns-query,1.1.1.1"
hint="多个服务器使用逗号分隔"
persistent-hint
clearable
active
/>
</VCol>
<VCol cols="12">
<VTextarea
v-model="AdvancedSettings.DOH_DOMAINS"
label="DOH 域名"
placeholder="格式example.com,example2.com"
hint="多个域名使用逗号分隔"
persistent-hint
clearable
active
/>
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardActions class="pt-3">
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn color="info" variant="elevated" prepend-icon="mdi-reload" @click="loadAdvancedSettings" class="px-5">
恢复默认值
</VBtn>
<VBtn
color="primary"
variant="elevated"
prepend-icon="mdi-content-save"
@click="saveAdvancedSettings"
class="px-5"
>
确定
</VBtn>
</div>
</VForm>
</VCardActions>
</VCard>
</VDialog>
</template>

View File

@@ -0,0 +1,93 @@
<script setup lang="ts">
import { cloneDeep } from "lodash";
const props = defineProps({
AdvancedSystemSettings: Object as any,
})
// 高级设置默认值,使用深复制,避免引用传递
const AdvancedSettings = ref(cloneDeep(props.AdvancedSystemSettings))
// 定义触发事件
const emit = defineEmits(['change', 'close'])
// 保存高级设置
function saveAdvancedSettings() {
emit('change', AdvancedSettings.value, 'Advanced')
}
</script>
<template>
<VDialog scrollable max-width="60rem" persistent>
<VCard>
<VCardItem>
<VCardTitle>高级系统设置</VCardTitle>
<VCardSubtitle>修改前请先了解清楚这些设置的作用</VCardSubtitle>
</VCardItem>
<DialogCloseBtn @click="emit('close')" />
<VDivider />
<VCardText>
<VRow>
<VCol cols="12" md="4">
<VSwitch
v-model="AdvancedSettings.DEV"
label="DEV模式"
hint="包括DEBUG日志、插件热加载"
persistent-hint
/>
</VCol>
<VCol cols="12" md="4">
<VSwitch
v-model="AdvancedSettings.DEBUG"
label="DEBUG日志"
hint="显示DEBUG日志"
persistent-hint
/>
</VCol>
<VCol cols="12" md="4">
<VSwitch
v-model="AdvancedSettings.PLUGIN_AUTO_RELOAD"
label="插件热加载"
hint="插件热加载调试模式"
persistent-hint
/>
</VCol>
<VCol cols="12" class="justify-center">
<VAlert
type="error"
variant="tonal"
style="inline-size: fit-content"
text="以上三项开关需要在保存设置后再重启MP才能生效"
/>
</VCol>
<VCol cols="12">
<VTextarea
v-model="AdvancedSettings.REPO_GITHUB_TOKEN"
label="指定仓库Github token"
placeholder="格式:{user1}/{repo1}:ghp_****,{user2}/{repo2}:github_pat_****"
hint="指定的单个仓库Github token。支持多个仓库使用,分隔"
persistent-hint
clearable
active
/>
</VCol>
</VRow>
</VCardText>
<VCardActions class="pt-3">
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn
color="primary"
variant="elevated"
prepend-icon="mdi-content-save"
@click="saveAdvancedSettings"
class="px-5"
>
确定
</VBtn>
</div>
</VForm>
</VCardActions>
</VCard>
</VDialog>
</template>

View File

@@ -82,10 +82,24 @@ function changeAvatar(file: Event) {
const fileReader = new FileReader()
const { files } = file.target as HTMLInputElement
if (files && files.length > 0) {
fileReader.readAsDataURL(files[0])
const selectedFile = files[0]
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif']
const maxSize = 800 * 1024
// 检查文件是否为图片
if (!allowedTypes.includes(selectedFile.type)) {
$toast.error('上传的文件不符合要求,请重新选择头像');
return;
}
// 检查文件大小
if (selectedFile.size > maxSize) {
$toast.error('文件大小不得大于800KB')
return
}
fileReader.readAsDataURL(selectedFile)
fileReader.onload = () => {
if (typeof fileReader.result === 'string') {
currentAvatar.value = fileReader.result
$toast.success('新头像上传成功,待保存后生效!')
}
}
}
@@ -289,7 +303,7 @@ onMounted(() => {
</VBtn>
</div>
<p class="text-body-1 mb-0">允许 JPGPNGGIF 格式 最大尺寸 800K</p>
<p class="text-body-1 mb-0">允许 JPGPNGGIF 格式 最大尺寸 800KB</p>
</form>
</VCardText>
<VCardText>

View File

@@ -9,8 +9,10 @@ import AccountSettingSearch from '@/views/setting/AccountSettingSearch.vue'
import AccountSettingSubscribe from '@/views/setting/AccountSettingSubscribe.vue'
import AccountSettingService from '@/views/setting/AccountSettingService.vue'
import AccountSettingSystem from '@/views/setting/AccountSettingSystem.vue'
import AccountSettingScheduler from '@/views/setting/AccountSettingScheduler.vue'
import AccountSettingDirectory from '@/views/setting/AccountSettingDirectory.vue'
import AccountSettingRule from '@/views/setting/AccountSettingRule.vue'
import AccountSettingTransfer from "@/views/setting/AccountSettingTransfer.vue"
import { SettingTabs } from '@/router/menu'
const route = useRoute()
@@ -32,7 +34,7 @@ function jumpTab(tab: string) {
@click="jumpTab(item.tab)"
selected-class="v-slide-group-item--active v-tab--selected"
>
<div>
<div class="flex align-center">
<VIcon size="20" start :icon="item.icon" />
{{ item.title }}
</div>
@@ -40,7 +42,7 @@ function jumpTab(tab: string) {
</VTabs>
<VWindow v-model="activeTab" class="mt-5 disable-tab-transition" :touch="false">
<!-- 连接 -->
<!-- 系统 -->
<VWindowItem value="system">
<transition name="fade-slide" appear>
<div>
@@ -49,6 +51,15 @@ function jumpTab(tab: string) {
</transition>
</VWindowItem>
<!-- 连接 -->
<VWindowItem value="service">
<transition name="fade-slide" appear>
<div>
<AccountSettingService />
</div>
</transition>
</VWindowItem>
<!-- 目录 -->
<VWindowItem value="directory">
<transition name="fade-slide" appear>
@@ -83,6 +94,15 @@ function jumpTab(tab: string) {
</transition>
</VWindowItem>
<!-- 整理 -->
<VWindowItem value="transfer">
<transition name="fade-slide" appear>
<div>
<AccountSettingTransfer />
</div>
</transition>
</VWindowItem>
<!-- 订阅 -->
<VWindowItem value="subscribe">
<transition name="fade-slide" appear>
@@ -93,10 +113,10 @@ function jumpTab(tab: string) {
</VWindowItem>
<!-- 服务 -->
<VWindowItem value="service">
<VWindowItem value="scheduler">
<transition name="fade-slide" appear>
<div>
<AccountSettingService />
<AccountSettingScheduler />
</div>
</transition>
</VWindowItem>

View File

@@ -131,10 +131,16 @@ export const UserfulMenus = [
// 设定标签页
export const SettingTabs = [
{
title: '系统',
icon: 'mdi-cog',
tab: 'system',
description: '基本设定、网络设定、高级设定',
},
{
title: '连接',
icon: 'mdi-server-network',
tab: 'system',
tab: 'service',
description: '下载器Qbittorrent、Transmission、媒体服务器Emby、Jellyfin、Plex',
},
{
@@ -161,6 +167,12 @@ export const SettingTabs = [
tab: 'search',
description: '媒体数据源TheMovieDb、豆瓣、Bangumi、搜索站点、搜索优先级、默认过滤规则',
},
{
title: '整理',
icon: 'mdi-folder-multiple-outline',
tab: 'transfer',
description: '转移重命名、刮削来源',
},
{
title: '订阅',
icon: 'mdi-rss',
@@ -168,9 +180,9 @@ export const SettingTabs = [
description: '订阅站点、订阅模式、订阅优先级、洗版优先级、默认过滤规则',
},
{
title: '服务',
title: '调度',
icon: 'mdi-list-box',
tab: 'service',
tab: 'scheduler',
description: '定时作业',
},
{

View File

@@ -7,6 +7,10 @@ import api from '@/api'
import { TransferDirectoryConf, StorageConf } from '@/api/types'
import DirectoryCard from '@/components/cards/DirectoryCard.vue'
import StorageCard from '@/components/cards/StorageCard.vue'
import debounce from 'lodash/debounce'
// 防抖时间
const debounceTime = 500
// 所有下载目录
const directories = ref<TransferDirectoryConf[]>([])
@@ -51,7 +55,7 @@ async function loadStorages() {
}
// 保存存储
async function saveStorages() {
const saveStorages = debounce(async () => {
try {
const result: { [key: string]: any } = await api.post('system/setting/Storages', storages.value)
if (result.success) $toast.success('存储设置保存成功')
@@ -59,11 +63,11 @@ async function saveStorages() {
} catch (error) {
console.log(error)
}
}
}, debounceTime)
// 修改后生效
async function updatedStorage() {
loadStorages()
await loadStorages()
}
// 查询目录
@@ -77,7 +81,7 @@ async function loadDirectories() {
}
// 保存目录
async function saveDirectories() {
const saveDirectories = debounce(async () => {
orderDirectoryCards()
try {
const names = directories.value.map(item => item.name)
@@ -93,10 +97,10 @@ async function saveDirectories() {
} catch (error) {
console.log(error)
}
}
}, debounceTime)
// 添加媒体库目录
function addDirectory() {
const addDirectory = debounce(() => {
let name = `目录${directories.value.length + 1}`
while (directories.value.some(item => item.name === name)) {
name = `目录${parseInt(name.split('目录')[1]) + 1}`
@@ -111,15 +115,15 @@ function addDirectory() {
media_category: '',
})
orderDirectoryCards()
}
}, debounceTime)
// 移除媒体库目录
function removeDirectory(directory: TransferDirectoryConf) {
const removeDirectory = debounce((directory: TransferDirectoryConf) => {
const index = directories.value.indexOf(directory)
if (index > -1) {
directories.value.splice(index, 1)
}
}
}, debounceTime)
// 调用API查询自动分类配置
async function loadMediaCategories() {
@@ -160,10 +164,16 @@ onMounted(() => {
</draggable>
</VCardText>
<VCardText>
<VBtn type="submit" class="me-2" @click="saveStorages"> 保存 </VBtn>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" class="me-2" @click="saveStorages"> 保存 </VBtn>
</div>
</VForm>
</VCardText>
</VCard>
</VCol>
</VRow>
<VRow>
<VCol cols="12">
<VCard>
<VCardItem>
@@ -190,10 +200,14 @@ onMounted(() => {
</draggable>
</VCardText>
<VCardText>
<VBtn type="submit" class="me-2" @click="saveDirectories"> 保存 </VBtn>
<VBtn color="success" variant="tonal" @click="addDirectory">
<VIcon icon="mdi-plus" />
</VBtn>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveDirectories"> 保存 </VBtn>
<VBtn color="success" variant="tonal" @click="addDirectory">
<VIcon icon="mdi-plus" />
</VBtn>
</div>
</VForm>
</VCardText>
</VCard>
</VCol>

View File

@@ -4,10 +4,14 @@ import api from '@/api'
import draggable from 'vuedraggable'
import type { NotificationConf, NotificationSwitchConf } from '@/api/types'
import NotificationChannelCard from '@/components/cards/NotificationChannelCard.vue'
import debounce from 'lodash/debounce'
// 所有消息渠道
const notifications = ref<NotificationConf[]>([])
// 防抖时间
const debounceTime = 500
// 提示框
const $toast = useToast()
@@ -58,8 +62,8 @@ async function reloadSystem() {
}
}
// 添加媒体服务器
function addNotification(notification: string) {
// 添加通知渠道
const addNotification = debounce((notification: string) => {
let name = `通知${notifications.value.length + 1}`;
while (notifications.value.some(item => item.name === name)) {
name = `通知${parseInt(name.split('通知')[1]) + 1}`;
@@ -70,15 +74,15 @@ function addNotification(notification: string) {
enabled: false,
config: {},
})
}
}, debounceTime)
// 移除媒体服务器
// 移除通知渠道
function removeNotification(notification: NotificationConf) {
const index = notifications.value.indexOf(notification)
if (index > -1) notifications.value.splice(index, 1)
}
// 调用API查询通知设置
// 调用API查询通知渠道设置
async function loadNotificationSetting() {
try {
const result: { [key: string]: any } = await api.get('system/setting/Notifications')
@@ -89,7 +93,7 @@ async function loadNotificationSetting() {
}
// 调用API保存通知设置
async function saveNotificationSetting() {
const saveNotificationSetting = debounce(async () => {
try {
const result: { [key: string]: any } = await api.post('system/setting/Notifications', notifications.value)
if (result.success) {
@@ -99,6 +103,12 @@ async function saveNotificationSetting() {
} catch (error) {
console.log(error)
}
}, debounceTime)
// 通知渠道设置变化时赋值
function changNotificationSetting(notification: NotificationConf, name: string) {
const index = notifications.value.findIndex(item => item.name === name)
if (index !== -1) notifications.value[index] = notification
}
// 加载消息类型开关
@@ -112,7 +122,7 @@ async function loadNotificationSwitchs() {
}
// 保存消息类型开关
async function saveNotificationSwitchs() {
const saveNotificationSwitchs = debounce(async () => {
try {
const result: { [key: string]: any } = await api.post(
'system/setting/NotificationSwitchs',
@@ -123,7 +133,7 @@ async function saveNotificationSwitchs() {
} catch (error) {
console.log(error)
}
}
}, debounceTime)
// 加载数据
onMounted(() => {
@@ -152,6 +162,7 @@ onMounted(() => {
<NotificationChannelCard
:notification="element"
:notifications="notifications"
@change="changNotificationSetting"
@close="removeNotification(element)"
/>
</template>
@@ -161,7 +172,7 @@ onMounted(() => {
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn mtype="submit" @click="saveNotificationSetting"> 保存 </VBtn>
<VBtn color="success" variant="tonal" @click="">
<VBtn color="success" variant="tonal">
<VIcon icon="mdi-plus" />
<VMenu activator="parent" close-on-content-click>
<VList>
@@ -225,7 +236,7 @@ onMounted(() => {
<VCardText>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn mtype="submit" @click="saveNotificationSwitchs"> 保存 </VBtn>
<VBtn type="submit" @click="saveNotificationSwitchs"> 保存 </VBtn>
</div>
</VForm>
</VCardText>

View File

@@ -9,6 +9,10 @@ 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 debounce from 'lodash/debounce'
// 防抖时间
const debounceTime = 500
// 自定义规则列表
const customRules = ref<CustomRule[]>([])
@@ -42,6 +46,48 @@ const TorrentPriorityItems = [
{ title: '资源做种数', value: 'seeder' },
]
// 内置的规则
const selectFilterOptions = ref<{ [key: string]: string }[]>([
{ title: '特效字幕', value: ' SPECSUB ' },
{ title: '中文字幕', value: ' CNSUB ' },
{ title: '国语配音', value: ' CNVOI ' },
{ title: '官种', value: ' GZ ' },
{ title: '排除: 国语配音', value: ' !CNVOI ' },
{ title: '粤语配音', value: ' HKVOI ' },
{ title: '排除: 粤语配音', value: ' !HKVOI ' },
{ title: '促销: 免费', value: ' FREE ' },
{ title: '分辨率: 4K', value: ' 4K ' },
{ title: '分辨率: 1080P', value: ' 1080P ' },
{ title: '分辨率: 720P', value: ' 720P ' },
{ title: '排除: 720P', value: ' !720P ' },
{ title: '质量: 蓝光原盘', value: ' BLU ' },
{ title: '排除: 蓝光原盘', value: ' !BLU ' },
{ title: '质量: BLURAY', value: ' BLURAY ' },
{ title: '排除: BLURAY', value: ' !BLURAY ' },
{ title: '质量: UHD', value: ' UHD ' },
{ title: '排除: UHD', value: ' !UHD ' },
{ title: '质量: REMUX', value: ' REMUX ' },
{ title: '排除: REMUX', value: ' !REMUX ' },
{ title: '质量: WEB-DL', value: ' WEBDL ' },
{ title: '排除: WEB-DL', value: ' !WEBDL ' },
{ title: '质量: 60fps', value: ' 60FPS ' },
{ title: '排除: 60fps', value: ' !60FPS ' },
{ title: '编码: H265', value: ' H265 ' },
{ title: '排除: H265', value: ' !H265 ' },
{ title: '编码: H264', value: ' H264 ' },
{ title: '排除: H264', value: ' !H264 ' },
{ title: '效果: 杜比视界', value: ' DOLBY ' },
{ title: '排除: 杜比视界', value: ' !DOLBY ' },
{ title: '效果: 杜比全景声', value: ' ATMOS ' },
{ title: '排除: 杜比全景声', value: ' !ATMOS ' },
{ title: '效果: HDR', value: ' HDR ' },
{ title: '排除: HDR', value: ' !HDR ' },
{ title: '效果: SDR', value: ' SDR ' },
{ title: '排除: SDR', value: ' !SDR ' },
{ title: '效果: 3D', value: ' 3D ' },
{ title: '排除: 3D', value: ' !3D ' },
])
// 调用API查询自动分类配置
async function loadMediaCategories() {
try {
@@ -52,7 +98,40 @@ async function loadMediaCategories() {
}
// 保存自定义规则
async function saveCustomRules() {
const saveCustomRules = debounce(async () => {
// 检查是否存在空id规则
if (customRules.value.some(item => !item.id)) {
$toast.error('存在空ID的规则无法保存请修改')
return
}
// 检查是否存在空的规则名称
if (customRules.value.some(item => !item.name)) {
$toast.error('存在空名字的规则!无法保存,请修改!')
return
}
// 获取所有规则ID和名称
const ids = customRules.value.map(item => item.id)
const names = customRules.value.map(item => item.name)
// 检查是否存在有规则ID是否已经被内置规则使用如果有则提示并提示出具体是哪个规则ID
if (ids.some(id => selectFilterOptions.value.some(option => option.value === id))) {
$toast.error('存在规则ID与内置规则ID重复无法保存请修改')
return
}
// 检查是否存在有规则名称是否已经被内置规则使用,如果有则提示,并提示出具体是哪个规则名称
if (names.some(name => selectFilterOptions.value.some(option => option.title === name))) {
$toast.error('存在规则名称与内置规则名称重复!无法保存,请修改!')
return
}
// 检查是否存在重名的规则ID
if (new Set(ids).size !== ids.length) {
$toast.error('存在重复规则ID无法保存请修改')
return
}
// 检查是否存在重名规则名称
if (new Set(names).size !== names.length) {
$toast.error('存在重复规则名称!无法保存,请修改!')
return
}
try {
const result: { [key: string]: any } = await api.post('system/setting/CustomFilterRules', customRules.value)
if (result.success) $toast.success('自定义规则保存成功')
@@ -60,10 +139,10 @@ async function saveCustomRules() {
} catch (error) {
console.log(error)
}
}
}, debounceTime)
// 添加自定义规则
function addCustomRule() {
const addCustomRule = debounce(async () => {
let id = `RULE${customRules.value.length + 1}`
while (customRules.value.some(item => item.id === id)) {
id = `RULE${parseInt(id.split('RULE')[1]) + 1}`
@@ -78,7 +157,7 @@ function addCustomRule() {
include: '',
exclude: '',
})
}
}, debounceTime)
// 移除自定义规则
function removeCustomRule(rule: CustomRule) {
@@ -97,7 +176,18 @@ async function queryFilterRuleGroups() {
}
// 保存规则组
async function saveFilterRuleGroups() {
const saveFilterRuleGroups = debounce(async () => {
// 检查是否存在空的规则组名称
if (filterRuleGroups.value.some(item => !item.name)) {
$toast.error('存在空名字的规则组!无法保存,请修改!')
return
}
// 检查是否存在重名规则组
const names = filterRuleGroups.value.map(item => item.name)
if (new Set(names).size !== names.length) {
$toast.error('存在重复规则组名称!无法保存,请修改!')
return
}
try {
const result: { [key: string]: any } = await api.post('system/setting/UserFilterRuleGroups', filterRuleGroups.value)
if (result.success) $toast.success('优先级规则组保存成功')
@@ -105,10 +195,10 @@ async function saveFilterRuleGroups() {
} catch (error) {
console.log(error)
}
}
}, debounceTime)
// 添加规则组
function addFilterRuleGroup() {
const addFilterRuleGroup = debounce(() => {
let name = `规则组${filterRuleGroups.value.length + 1}`
while (filterRuleGroups.value.some(item => item.name === name)) {
name = `规则组${parseInt(name.split('规则组')[1]) + 1}`
@@ -119,10 +209,10 @@ function addFilterRuleGroup() {
media_type: '',
category: '',
})
}
}, debounceTime)
// 分享规则
function shareRules(rules: CustomRule[] | FilterRuleGroup[]) {
const shareRules = debounce((rules: CustomRule[] | FilterRuleGroup[]) => {
if (!rules || rules.length === 0) return
// 将卡片规则接装为字符串
@@ -135,7 +225,7 @@ function shareRules(rules: CustomRule[] | FilterRuleGroup[]) {
} catch (error) {
$toast.error('优先级规则复制失败!')
}
}
}, debounceTime)
// 导入规则
async function importRules(ruleType: string) {
@@ -179,8 +269,8 @@ watchEffect(() => {
})
// 规则变化时赋值
function onRuleChange(rule: CustomRule) {
const index = customRules.value.findIndex(item => item.id === rule.id)
function onRuleChange(rule: CustomRule, id: string) {
const index = customRules.value.findIndex(item => item.id === id)
if (index !== -1) customRules.value[index] = rule
}
@@ -191,8 +281,8 @@ function removeFilterRuleGroup(rule: FilterRuleGroup) {
}
// 规则组变化时赋值
function changeRuleGroup(group: FilterRuleGroup) {
const index = filterRuleGroups.value.findIndex(item => item.name === group.name)
function changeRuleGroup(group: FilterRuleGroup, name: string) {
const index = filterRuleGroups.value.findIndex(item => item.name === name)
if (index !== -1) filterRuleGroups.value[index] = group
}
@@ -218,20 +308,18 @@ async function queryCustomRules() {
}
// 保存种子优先规则
async function saveTorrentPriority() {
const saveTorrentPriority = debounce(async () => {
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)
}
}
}, debounceTime)
// 加载数据
onMounted(() => {
@@ -269,21 +357,27 @@ onMounted(() => {
</draggable>
</VCardText>
<VCardText>
<VBtn type="submit" class="me-2" @click="saveCustomRules"> 保存 </VBtn>
<VBtnGroup density="comfortable">
<VBtn color="success" variant="tonal" @click="addCustomRule">
<VIcon icon="mdi-plus" />
</VBtn>
<VBtn color="info" variant="tonal" @click="importRules('custom')">
<VIcon icon="mdi-import" />
</VBtn>
<VBtn color="info" variant="tonal" @click="shareRules(customRules)">
<VIcon icon="mdi-share" />
</VBtn>
</VBtnGroup>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" class="me-2" @click="saveCustomRules"> 保存 </VBtn>
<VBtnGroup density="comfortable">
<VBtn color="success" variant="tonal" @click="addCustomRule">
<VIcon icon="mdi-plus" />
</VBtn>
<VBtn color="info" variant="tonal" @click="importRules('custom')">
<VIcon icon="mdi-import" />
</VBtn>
<VBtn color="info" variant="tonal" @click="shareRules(customRules)">
<VIcon icon="mdi-share" />
</VBtn>
</VBtnGroup>
</div>
</VForm>
</VCardText>
</VCard>
</VCol>
</VRow>
<VRow>
<VCol cols="12">
<VCard>
<VCardItem>
@@ -311,24 +405,30 @@ onMounted(() => {
</draggable>
</VCardText>
<VCardText>
<VBtn type="submit" class="me-2" @click="saveFilterRuleGroups"> 保存 </VBtn>
<VBtnGroup density="comfortable">
<VBtn color="success" variant="tonal" @click="addFilterRuleGroup">
<VIcon icon="mdi-plus" />
</VBtn>
<VBtn color="info" variant="tonal" @click="importRules('group')">
<VIcon icon="mdi-import" />
</VBtn>
<VBtn color="info" variant="tonal" @click="shareRules(filterRuleGroups)">
<VIcon icon="mdi-share" />
</VBtn>
</VBtnGroup>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" class="me-2" @click="saveFilterRuleGroups"> 保存 </VBtn>
<VBtnGroup density="comfortable">
<VBtn color="success" variant="tonal" @click="addFilterRuleGroup">
<VIcon icon="mdi-plus" />
</VBtn>
<VBtn color="info" variant="tonal" @click="importRules('group')">
<VIcon icon="mdi-import" />
</VBtn>
<VBtn color="info" variant="tonal" @click="shareRules(filterRuleGroups)">
<VIcon icon="mdi-share" />
</VBtn>
</VBtnGroup>
</div>
</VForm>
</VCardText>
<VDialog v-model="importCodeDialog" width="60rem" scrollable>
<ImportCodeDialog v-model="importCodeString" title="导入规则" @close="importCodeDialog = false" />
</VDialog>
</VCard>
</VCol>
</VRow>
<VRow>
<VCol cols="12">
<VCard>
<VCardItem>
@@ -354,7 +454,11 @@ onMounted(() => {
</VForm>
</VCardText>
<VCardText>
<VBtn type="submit" @click="saveTorrentPriority"> 保存 </VBtn>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveTorrentPriority"> 保存 </VBtn>
</div>
</VForm>
</VCardText>
</VCard>
</VCol>

View File

@@ -0,0 +1,124 @@
<script lang="ts" setup>
import { useToast } from 'vue-toast-notification'
import api from '@/api'
import type { ScheduleInfo } from '@/api/types'
// 提示框
const $toast = useToast()
// 定时服务列表
const schedulerList = ref<ScheduleInfo[]>([])
// 定时器
let refreshTimer: NodeJS.Timeout | null = null
// 调用API加载定时服务列表
async function loadSchedulerList() {
try {
const res: ScheduleInfo[] = await api.get('dashboard/schedule')
schedulerList.value = res
} catch (e) {
console.log(e)
}
}
// 任务状态颜色
function getSchedulerColor(status: string) {
switch (status) {
case '正在运行':
return 'success'
case '已停止':
return 'error'
case '等待':
return ''
default:
return ''
}
}
// 执行命令
function runCommand(id: string) {
try {
// 异步提交
api.get('system/runscheduler', {
params: {
jobid: id,
},
})
$toast.success('定时作业执行请求提交成功!')
// 1秒后刷新数据
setTimeout(() => {
loadSchedulerList()
}, 1000)
} catch (e) {
console.log(e)
}
}
onMounted(() => {
loadSchedulerList()
// 启动定时器
refreshTimer = setInterval(() => {
loadSchedulerList()
}, 5000)
})
// 组件卸载时停止定时器
onUnmounted(() => {
if (refreshTimer) {
clearInterval(refreshTimer)
refreshTimer = null
}
})
</script>
<template>
<VCard>
<VCardItem>
<VCardTitle>定时作业</VCardTitle>
<VCardSubtitle>包含系统内置服务以及插件提供的服务手动执行不会影响作业正常的时间表</VCardSubtitle>
</VCardItem>
<VTable class="text-no-wrap">
<thead>
<tr>
<th scope="col">提供者</th>
<th scope="col">任务名称</th>
<th scope="col">任务状态</th>
<th scope="col">下一次执行时间</th>
<th scope="col" />
</tr>
</thead>
<tbody>
<tr v-for="scheduler in schedulerList" :key="scheduler.id">
<td>
{{ scheduler.provider }}
</td>
<td>
{{ scheduler.name }}
</td>
<td>
<VChip :color="getSchedulerColor(scheduler.status)">
{{ scheduler.status }}
</VChip>
</td>
<td>
{{ scheduler.next_run }}
</td>
<td>
<VBtn size="small" :disabled="scheduler.status === '正在运行'" @click="runCommand(scheduler.id)">
<template #prepend>
<VIcon>mdi-play</VIcon>
</template>
执行
</VBtn>
</td>
</tr>
<tr v-if="schedulerList.length === 0">
<td colspan="4" class="text-center">没有后台服务</td>
</tr>
</tbody>
</VTable>
</VCard>
</template>

View File

@@ -2,6 +2,10 @@
import { useToast } from 'vue-toast-notification'
import api from '@/api'
import type { FilterRuleGroup, Site } from '@/api/types'
import debounce from 'lodash/debounce'
// 防抖时间
const debounceTime = 500
// 提示框
const $toast = useToast()
@@ -12,6 +16,18 @@ const allSites = ref<Site[]>([])
// 选中订阅站点
const selectedSites = ref<number[]>([])
// 系统设置
const SystemSettings = ref<any>({
Basis: {
},
Advanced: {
SEARCH_MULTIPLE_NAME: false,
DOWNLOAD_SUBTITLE: false,
AUTO_DOWNLOAD_USER: '',
},
})
// 媒体信息数据源字典
const mediaSourcesDict = [
{
@@ -79,7 +95,7 @@ async function querySelectedSites() {
}
// 保存用户选中的站点
async function saveSelectedSites() {
const saveSelectedSites = debounce(async () => {
try {
// 用户名密码
const result: { [key: string]: any } = await api.post('system/setting/IndexerSites', selectedSites.value)
@@ -89,9 +105,9 @@ async function saveSelectedSites() {
} catch (error) {
console.log(error)
}
}
}, debounceTime)
// 调用API查询下载器设置
// 调用API查询设置
async function loadSearchSetting() {
try {
const result1: { [key: string]: any } = await api.get('system/setting/SEARCH_SOURCE')
@@ -104,7 +120,7 @@ async function loadSearchSetting() {
}
// 调用API保存设置
async function saveSearchSetting() {
const saveSearchSetting = debounce(async () => {
try {
const result1: { [key: string]: any } = await api.post(
'system/setting/SEARCH_SOURCE',
@@ -117,13 +133,68 @@ async function saveSearchSetting() {
)
if (result1.success && result2.success) {
$toast.success('保存媒体数据源设置成功')
$toast.success('保存设置成功')
await reloadSystem()
} else {
$toast.error('保存媒体数据源设置失败!')
$toast.error('保存设置失败!')
}
} catch (error) {
console.log(error)
}
}, debounceTime)
// 加载系统设置
async function loadSystemSettings() {
try {
const result: { [key: string]: any } = await api.get('system/env')
if (result.success) {
// 将API返回的值赋值给SystemSettings
for (const sectionKey of Object.keys(SystemSettings.value) as Array<keyof typeof SystemSettings.value>) {
Object.keys(SystemSettings.value[sectionKey]).forEach((key: string) => {
let v: any
if (result.data.hasOwnProperty(key)) {
v = result.data[key]
// 空字符串转为null避免空字符串导致前端显示问题
if (v === '') {
v = null
}
(SystemSettings.value[sectionKey] as any)[key] = v
}
})
}
} else $toast.error('加载设置失败!')
} catch (error) {
console.log(error)
}
}
// 保存设置
const saveSystemSettings = debounce(async (value: any) => {
try {
const result: { [key: string]: any } = await api.post('system/env', value)
if (result.success) {
$toast.success('保存设置成功')
await reloadSystem()
await loadSystemSettings()
} else {
$toast.error('保存设置失败!')
}
} catch (error) {
console.log(error)
}
}, debounceTime)
// 重载系统生效配置
async function reloadSystem() {
try {
const result: { [key: string]: any } = await api.get('system/reload')
if (result.success) {
$toast.success('系统配置已生效')
await loadSystemSettings()
} else $toast.error('重载系统失败!')
} catch (error) {
console.log(error)
}
}
onMounted(() => {
@@ -131,6 +202,7 @@ onMounted(() => {
queryFilterRuleGroups()
querySelectedSites()
loadSearchSetting()
loadSystemSettings()
})
</script>
@@ -151,7 +223,7 @@ onMounted(() => {
clearable
chips
:items="mediaSourcesDict"
label="媒体数据源"
label="媒体搜索数据源"
hint="搜索媒体信息时使用的数据源以及排序"
persistent-hint
/>
@@ -171,10 +243,16 @@ onMounted(() => {
</VRow>
</VCardText>
<VCardText>
<VBtn type="submit" @click="saveSearchSetting"> 保存 </VBtn>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveSearchSetting"> 保存 </VBtn>
</div>
</VForm>
</VCardText>
</VCard>
</VCol>
</VRow>
<VRow>
<VCol cols="12">
<VCard>
<VCardItem>
@@ -196,7 +274,56 @@ onMounted(() => {
</VChipGroup>
</VCardText>
<VCardText>
<VBtn type="submit" @click="saveSelectedSites"> 保存 </VBtn>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveSelectedSites"> 保存 </VBtn>
</div>
</VForm>
</VCardText>
</VCard>
</VCol>
</VRow>
<VRow>
<VCol cols="12">
<VCard>
<VCardItem>
<VCardTitle>高级设置</VCardTitle>
<VCardSubtitle>设置交互搜索自动下载用户ID字幕</VCardSubtitle>
</VCardItem>
<VCardText>
<VRow>
<VCol cols="12" md="6">
<VSwitch
v-model="SystemSettings.Advanced.SEARCH_MULTIPLE_NAME"
label="整合多名称资源搜索结果"
hint="搜索多个名称的资源时,整合多名称的结果"
persistent-hint
/>
</VCol>
<VCol cols="12" md="6">
<VSwitch
v-model="SystemSettings.Advanced.DOWNLOAD_SUBTITLE"
label="下载站点字幕"
hint="当选定的资源所在站点中,存在字幕文件时,同步自动下载"
persistent-hint
/>
</VCol>
<VCol cols="12">
<VCombobox
v-model="SystemSettings.Advanced.AUTO_DOWNLOAD_USER"
label="交互式搜索自动下载用户"
hint="针对使用tg、微信等第三方交互的特化功能。使用逗号分割设置为 all 代表所有用户自动择优下载,未设置时,需要用户手动选择资源 或 回复 ` 0 ` 才自动择优下载"
persistent-hint
/>
</VCol>
</VRow>
</VCardText>
<VCardText>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveSystemSettings(SystemSettings.Advanced)"> 保存 </VBtn>
</div>
</VForm>
</VCardText>
</VCard>
</VCol>

View File

@@ -1,124 +1,353 @@
<!-- eslint-disable sonarjs/no-duplicate-string -->
<script lang="ts" setup>
import { useToast } from 'vue-toast-notification'
import { VRow } from 'vuetify/lib/components/index.mjs'
import draggable from 'vuedraggable'
import api from '@/api'
import type { ScheduleInfo } from '@/api/types'
import { DownloaderConf, MediaServerConf } from '@/api/types'
import DownloaderCard from '@/components/cards/DownloaderCard.vue'
import MediaServerCard from '@/components/cards/MediaServerCard.vue'
import debounce from 'lodash/debounce'
// 防抖时间
const debounceTime = 500
// 系统设置项
const SystemSettings = ref<any>({
MEDIASERVER_SYNC_INTERVAL: 6,
})
// 是否发送请求的总开关
const isRequest = ref(true)
// 选中的媒体服务器
const mediaServers = ref<MediaServerConf[]>([])
// 下载器
const downloaders = ref<DownloaderConf[]>([])
// 提示框
const $toast = useToast()
// 定时服务列表
const schedulerList = ref<ScheduleInfo[]>([])
// 定时器
let refreshTimer: NodeJS.Timeout | null = null
// 调用API加载定时服务列表
async function loadSchedulerList() {
// 调用API查询下载器设置
async function loadDownloaderSetting() {
try {
const res: ScheduleInfo[] = await api.get('dashboard/schedule')
schedulerList.value = res
} catch (e) {
console.log(e)
const result: { [key: string]: any } = await api.get('system/setting/Downloaders')
downloaders.value = result.data?.value ?? []
} catch (error) {
console.log(error)
}
}
// 任务状态颜色
function getSchedulerColor(status: string) {
switch (status) {
case '正在运行':
return 'success'
case '已停止':
return 'error'
case '等待':
return ''
default:
return ''
}
}
// 执行命令
function runCommand(id: string) {
// 重载系统生效配置
async function reloadSystem() {
try {
// 异步提交
api.get('system/runscheduler', {
params: {
jobid: id,
},
})
$toast.success('定时作业执行请求提交成功!')
// 1秒后刷新数据
setTimeout(() => {
loadSchedulerList()
}, 1000)
} catch (e) {
console.log(e)
const result: { [key: string]: any } = await api.get('system/reload')
if (result.success) $toast.success('系统配置已生效')
else $toast.error('重载系统失败!')
} catch (error) {
console.log(error)
}
}
// 调用API保存下载器设置
const saveDownloaderSetting = debounce(async () => {
try {
// 提取启用的下载器
const enabledDownloaders = downloaders.value.filter(item => item.enabled);
// 有启动的下载器时
if (enabledDownloaders.length > 0) {
downloaders.value = handleDefaultDownloaders(enabledDownloaders, downloaders.value);
}
const result: { [key: string]: any } = await api.post('system/setting/Downloaders', downloaders.value)
if (result.success) $toast.success('下载器设置保存成功')
else $toast.error('下载器设置保存失败!')
await loadDownloaderSetting()
await reloadSystem()
} catch (error) {
console.log(error)
}
}, debounceTime)
// 处理默认下载器状态
function handleDefaultDownloaders(enabledDownloaders: any[], downloaders: any[]) {
const enabledDefaultDownloader = enabledDownloaders.find(item => item.default);
if (enabledDownloaders.length > 0 && !enabledDefaultDownloader) {
downloaders = downloaders.map(item => {
if (item === enabledDownloaders[0]) {
$toast.info(`未设置默认下载器,已将【${item.name}】作为默认下载器`);
return {...item, default: true };
}
// 清除其他下载器的默认下载器状态
return {...item, default: false };
});
}
return downloaders;
}
// 调用API查询媒体服务器设置
async function loadMediaServerSetting() {
try {
const result: { [key: string]: any } = await api.get('system/setting/MediaServers')
mediaServers.value = result.data?.value ?? []
} catch (error) {
console.log(error)
}
}
// 调用API保存媒体服务器设置
const saveMediaServerSetting = debounce(async () => {
try {
const result: { [key: string]: any } = await api.post('system/setting/MediaServers', mediaServers.value)
if (result.success) $toast.success('媒体服务器设置保存成功')
else $toast.error('媒体服务器设置保存失败!')
await loadMediaServerSetting()
await reloadSystem()
} catch (error) {
console.log(error)
}
}, debounceTime)
// 加载系统设置
async function loadSystemSettings() {
try {
const result: { [key: string]: any } = await api.get('system/env')
if (result.success) {
const {
MEDIASERVER_SYNC_INTERVAL,
} = result.data
SystemSettings.value = {
MEDIASERVER_SYNC_INTERVAL,
}
}
} catch (error) {
console.log(error)
}
}
// 调用API保存系统设置
const saveSystemSetting = debounce(async () => {
try {
const result: { [key: string]: any } = await api.post('system/env', SystemSettings.value)
if (result.success) $toast.success('保存设置成功')
else $toast.error('保存设置失败!')
} catch (error) {
console.log(error)
}
}, debounceTime)
// 添加下载器
function addDownloader(downloader: string) {
let name = `下载器${downloaders.value.length + 1}`;
while (downloaders.value.some(item => item.name === name)) {
name = `下载器${parseInt(name.split('下载器')[1]) + 1}`;
}
downloaders.value.push({
name: name,
type: downloader,
default: false,
enabled: false,
config: {},
})
}
// 删除下载器
const removeDownloader = debounce((ele: DownloaderConf) => {
const index = downloaders.value.indexOf(ele)
downloaders.value.splice(index, 1)
}, debounceTime)
// 下载器变化
function onDownloaderChange(downloader: DownloaderConf, name: string) {
const index = downloaders.value.findIndex(item => item.name === name)
if (index !== -1) downloaders.value[index] = downloader
}
// 添加媒体服务器
const addMediaServer = debounce( (mediaserver: string) => {
let name = `服务器${mediaServers.value.length + 1}`;
while (mediaServers.value.some(item => item.name === name)) {
name = `服务器${parseInt(name.split('服务器')[1]) + 1}`;
}
mediaServers.value.push({
name: name,
type: mediaserver,
enabled: false,
config: {},
})
}, debounceTime)
// 删除媒体服务器
const removeMediaServer = debounce((ele: MediaServerConf) => {
const index = mediaServers.value.indexOf(ele)
if (index !== -1) mediaServers.value.splice(index, 1)
}, debounceTime)
// 变更媒体服务器
function onMediaServerChange(mediaserver: MediaServerConf, name: string) {
const index = mediaServers.value.findIndex(item => item.name === name)
if (index !== -1) mediaServers.value[index] = mediaserver
}
// 加载数据
onMounted(() => {
loadSchedulerList()
// 启动定时器
refreshTimer = setInterval(() => {
loadSchedulerList()
}, 5000)
loadDownloaderSetting()
loadMediaServerSetting()
loadSystemSettings()
})
// 组件卸载时停止定时器
onUnmounted(() => {
if (refreshTimer) {
clearInterval(refreshTimer)
refreshTimer = null
}
onActivated(async () => {
isRequest.value = true
})
onDeactivated(() => {
isRequest.value = false
})
</script>
<template>
<VCard>
<VCardItem>
<VCardTitle>定时作业</VCardTitle>
<VCardSubtitle>包含系统内置服务以及插件提供的服务手动执行不会影响作业正常的时间表</VCardSubtitle>
</VCardItem>
<VTable class="text-no-wrap">
<thead>
<tr>
<th scope="col">提供者</th>
<th scope="col">任务名称</th>
<th scope="col">任务状态</th>
<th scope="col">下一次执行时间</th>
<th scope="col" />
</tr>
</thead>
<tbody>
<tr v-for="scheduler in schedulerList" :key="scheduler.id">
<td>
{{ scheduler.provider }}
</td>
<td>
{{ scheduler.name }}
</td>
<td>
<VChip :color="getSchedulerColor(scheduler.status)">
{{ scheduler.status }}
</VChip>
</td>
<td>
{{ scheduler.next_run }}
</td>
<td>
<VBtn size="small" :disabled="scheduler.status === '正在运行'" @click="runCommand(scheduler.id)">
<template #prepend>
<VIcon>mdi-play</VIcon>
</template>
执行
</VBtn>
</td>
</tr>
<tr v-if="schedulerList.length === 0">
<td colspan="4" class="text-center">没有后台服务</td>
</tr>
</tbody>
</VTable>
</VCard>
<VRow>
<VCol cols="12">
<VCard>
<VCardItem>
<VCardTitle>基础设置</VCardTitle>
<VCardSubtitle>设置服务器的全局功能</VCardSubtitle>
</VCardItem>
<VCardText>
<VForm>
<VRow>
<VCol cols="12" md="6">
<VTextField
v-model="SystemSettings.MEDIASERVER_SYNC_INTERVAL"
label="媒体服务器同步间隔"
hint="不宜设置间隔过短的时间,这会导致服务器性能占用增高。"
persistent-hint
clearable
suffix="小时"
type="number"
min="1"
style="width: fit-content"
:rules="[
v => !!v || '必选项,请勿留空',
v => !isNaN(v) || '仅支持输入数字,请勿输入其他字符',
v => v >= 1 || '间隔不能小于1个小时',
]"
/>
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardText>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveSystemSetting"> 保存 </VBtn>
</div>
</VForm>
</VCardText>
</VCard>
</VCol>
</VRow>
<VRow>
<VCol cols="12">
<VCard>
<VCardItem>
<VCardTitle>下载器</VCardTitle>
<VCardSubtitle>只有默认下载器才会被默认使用</VCardSubtitle>
</VCardItem>
<VCardText>
<draggable
v-model="downloaders"
handle=".cursor-move"
item-key="name"
tag="div"
:component-data="{ 'class': 'grid gap-3 grid-app-card' }"
>
<template #item="{ element }">
<DownloaderCard
:downloader="element"
:downloaders="downloaders"
@close="removeDownloader(element)"
@change="onDownloaderChange"
:allow-refresh="isRequest"
/>
</template>
</draggable>
</VCardText>
<VCardText>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveDownloaderSetting"> 保存 </VBtn>
<VBtn color="success" variant="tonal">
<VIcon icon="mdi-plus" />
<VMenu activator="parent" close-on-content-click>
<VList>
<VListItem variant="plain" @click="addDownloader('qbittorrent')">
<VListItemTitle>Qbittorrent</VListItemTitle>
</VListItem>
<VListItem variant="plain" @click="addDownloader('transmission')">
<VListItemTitle>Transmission</VListItemTitle>
</VListItem>
</VList>
</VMenu>
</VBtn>
</div>
</VForm>
</VCardText>
</VCard>
</VCol>
</VRow>
<VRow>
<VCol cols="12">
<VCard>
<VCardItem>
<VCardTitle>媒体服务器</VCardTitle>
<VCardSubtitle>所有启用的媒体服务器都会被使用</VCardSubtitle>
</VCardItem>
<VCardText>
<draggable
v-model="mediaServers"
handle=".cursor-move"
item-key="name"
tag="div"
:component-data="{ 'class': 'grid gap-3 grid-app-card' }"
>
<template #item="{ element }">
<MediaServerCard
:mediaserver="element"
:mediaservers="mediaServers"
@close="removeMediaServer(element)"
@change="onMediaServerChange"
/>
</template>
</draggable>
</VCardText>
<VCardText>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveMediaServerSetting"> 保存 </VBtn>
<VBtn color="success" variant="tonal">
<VIcon icon="mdi-plus" />
<VMenu activator="parent" close-on-content-click>
<VList>
<VListItem variant="plain" @click="addMediaServer('emby')">
<VListItemTitle>Emby</VListItemTitle>
</VListItem>
<VListItem variant="plain" @click="addMediaServer('jellyfin')">
<VListItemTitle>Jellyfin</VListItemTitle>
</VListItem>
<VListItem variant="plain" @click="addMediaServer('plex')">
<VListItemTitle>Plex</VListItemTitle>
</VListItem>
</VList>
</VMenu>
</VBtn>
</div>
</VForm>
</VCardText>
</VCard>
</VCol>
</VRow>
</template>

View File

@@ -1,6 +1,10 @@
<script lang="ts" setup>
import { useToast } from 'vue-toast-notification'
import api from '@/api'
import debounce from 'lodash/debounce'
// 防抖时间
const debounceTime = 500
// 提示框
const $toast = useToast()
@@ -16,16 +20,20 @@ const resetSitesDisabled = ref(false)
const isPasswordVisible = ref(false)
// CookieCloud设置项
const siteSetting = ref({
COOKIECLOUD_HOST: '',
COOKIECLOUD_KEY: '',
COOKIECLOUD_PASSWORD: '',
COOKIECLOUD_INTERVAL: 0,
USER_AGENT: '',
COOKIECLOUD_ENABLE_LOCAL: false,
COOKIECLOUD_BLACKLIST: '',
SITEDATA_REFRESH_INTERVAL: 0,
// 站点设置默认值
const siteSetting = ref<any>({
CookieCloud: {
COOKIECLOUD_HOST: '',
COOKIECLOUD_KEY: '',
COOKIECLOUD_PASSWORD: '',
COOKIECLOUD_INTERVAL: 0,
USER_AGENT: '',
COOKIECLOUD_ENABLE_LOCAL: false,
COOKIECLOUD_BLACKLIST: '',
},
Site:{
SITEDATA_REFRESH_INTERVAL: 0,
}
})
// 同步间隔下拉框
@@ -50,7 +58,7 @@ const SiteDataRefreshIntervalItems = [
]
// 重置站点
async function resetSites() {
const resetSites = debounce(async () => {
try {
resetSitesDisabled.value = true
resetSitesText.value = '正在重置...'
@@ -64,32 +72,24 @@ async function resetSites() {
} catch (error) {
console.log(error)
}
}
}, debounceTime)
// 加载站点设置
async function loadSiteSettings() {
try {
const result: { [key: string]: any } = await api.get('system/env')
if (result.success) {
const {
COOKIECLOUD_HOST,
COOKIECLOUD_KEY,
COOKIECLOUD_PASSWORD,
COOKIECLOUD_INTERVAL,
USER_AGENT,
COOKIECLOUD_ENABLE_LOCAL,
COOKIECLOUD_BLACKLIST,
SITEDATA_REFRESH_INTERVAL,
} = result.data
siteSetting.value = {
COOKIECLOUD_HOST,
COOKIECLOUD_KEY,
COOKIECLOUD_PASSWORD,
COOKIECLOUD_INTERVAL,
USER_AGENT,
COOKIECLOUD_ENABLE_LOCAL,
COOKIECLOUD_BLACKLIST,
SITEDATA_REFRESH_INTERVAL,
// 将API返回的值赋值给SystemSettings
for (const sectionKey of Object.keys(siteSetting.value) as Array<keyof typeof siteSetting.value>) {
Object.keys(siteSetting.value[sectionKey]).forEach((key: string) => {
let v: any
if (result.data.hasOwnProperty(key)) {
v = result.data[key]
// 空字符串转为null避免空字符串导致前端显示问题
if (v === '') { v = null }
(siteSetting.value[sectionKey] as any)[key] = v
}
})
}
}
} catch (error) {
@@ -97,17 +97,19 @@ async function loadSiteSettings() {
}
}
// 调用API保存CookieCloud设置
async function saveSiteSetting() {
// 调用API保存设置
const saveSiteSetting = debounce(async (value: { [key: string]: any }) => {
console.log(`正在保存设置:${JSON.stringify(value)}`)
try {
const result: { [key: string]: any } = await api.post('system/env', siteSetting.value)
if (result.success) $toast.success('保存站点设置成功')
else $toast.error('保存站点设置失败!')
const result: { [key: string]: any } = await api.post('system/env', value)
if (result.success) {
$toast.success('保存设置成功')
}
} catch (error) {
console.log(error)
$toast.error('保存设置失败!')
}
}
}, debounceTime)
// 加载数据
onMounted(() => {
@@ -128,7 +130,7 @@ onMounted(() => {
<VRow>
<VCol cols="12" md="6">
<VCheckbox
v-model="siteSetting.COOKIECLOUD_ENABLE_LOCAL"
v-model="siteSetting.CookieCloud.COOKIECLOUD_ENABLE_LOCAL"
label="启用本地CookieCloud服务器"
hint="使用内建CookieCloud服务同步站点数据服务地址为http://localhost:3000/cookiecloud"
persistent-hint
@@ -138,17 +140,17 @@ onMounted(() => {
<VRow>
<VCol cols="12" md="6">
<VTextField
v-model="siteSetting.COOKIECLOUD_HOST"
v-model="siteSetting.CookieCloud.COOKIECLOUD_HOST"
label="服务地址"
placeholder="https://movie-pilot.org/cookiecloud"
:disabled="!!siteSetting.COOKIECLOUD_ENABLE_LOCAL"
:disabled="siteSetting.CookieCloud.COOKIECLOUD_ENABLE_LOCAL"
hint="远端CookieCloud服务地址格式https://movie-pilot.org/cookiecloud"
persistent-hint
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="siteSetting.COOKIECLOUD_KEY"
v-model="siteSetting.CookieCloud.COOKIECLOUD_KEY"
label="用户KEY"
hint="CookieCloud浏览器插件生成的用户KEY"
persistent-hint
@@ -156,7 +158,7 @@ onMounted(() => {
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="siteSetting.COOKIECLOUD_PASSWORD"
v-model="siteSetting.CookieCloud.COOKIECLOUD_PASSWORD"
:type="isPasswordVisible ? 'text' : 'password'"
:append-inner-icon="isPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
@click:append-inner="isPasswordVisible = !isPasswordVisible"
@@ -167,7 +169,7 @@ onMounted(() => {
</VCol>
<VCol cols="12" md="6">
<VSelect
v-model="siteSetting.COOKIECLOUD_INTERVAL"
v-model="siteSetting.CookieCloud.COOKIECLOUD_INTERVAL"
label="自动同步间隔"
:items="CookieCloudIntervalItems"
hint="从CookieCloud服务器自动同步站点Cookie到MoviePilot的时间间隔"
@@ -176,7 +178,7 @@ onMounted(() => {
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="siteSetting.COOKIECLOUD_BLACKLIST"
v-model="siteSetting.CookieCloud.COOKIECLOUD_BLACKLIST"
label="同步域名黑名单"
placeholder="多个域名,分割"
hint="CookieCloud同步域名黑名单多个域名,分割"
@@ -185,7 +187,7 @@ onMounted(() => {
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="siteSetting.USER_AGENT"
v-model="siteSetting.CookieCloud.USER_AGENT"
label="浏览器User-Agent"
hint="CookieCloud插件所在的浏览器的User-Agent"
persistent-hint
@@ -195,11 +197,16 @@ onMounted(() => {
</VForm>
</VCardText>
<VCardText>
<VBtn type="submit" @click="saveSiteSetting"> 保存 </VBtn>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveSiteSetting(siteSetting.CookieCloud)"> 保存 </VBtn>
</div>
</VForm>
</VCardText>
</VCard>
</VCol>
</VRow>
<VRow>
<VCol cols="12">
<VCard title="站点数据刷新">
<VCardText>
@@ -207,7 +214,7 @@ onMounted(() => {
<VRow>
<VCol cols="12" md="6">
<VSelect
v-model="siteSetting.SITEDATA_REFRESH_INTERVAL"
v-model="siteSetting.Site.SITEDATA_REFRESH_INTERVAL"
label="站点数据刷新间隔"
:items="SiteDataRefreshIntervalItems"
hint="刷新站点用户上传下载等数据的时间间隔"
@@ -218,11 +225,16 @@ onMounted(() => {
</VForm>
</VCardText>
<VCardText>
<VBtn type="submit" @click="saveSiteSetting"> 保存 </VBtn>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveSiteSetting(siteSetting.Site)"> 保存 </VBtn>
</div>
</VForm>
</VCardText>
</VCard>
</VCol>
</VRow>
<VRow>
<VCol cols="12">
<VCard title="站点重置">
<VCardText>

View File

@@ -250,10 +250,16 @@ onMounted(() => {
</VForm>
</VCardText>
<VCardText>
<VBtn type="submit" @click="saveSubscribeSetting"> 保存 </VBtn>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveSubscribeSetting"> 保存 </VBtn>
</div>
</VForm>
</VCardText>
</VCard>
</VCol>
</VRow>
<VRow>
<VCol cols="12">
<VCard>
<VCardItem>
@@ -275,7 +281,11 @@ onMounted(() => {
</VChipGroup>
</VCardText>
<VCardText>
<VBtn type="submit" @click="saveSelectedRssSites"> 保存 </VBtn>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveSelectedRssSites"> 保存 </VBtn>
</div>
</VForm>
</VCardText>
</VCard>
</VCol>

View File

@@ -1,106 +1,68 @@
<!-- eslint-disable sonarjs/no-duplicate-string -->
<script lang="ts" setup>
import { useToast } from 'vue-toast-notification'
import { VRow } from 'vuetify/lib/components/index.mjs'
import draggable from 'vuedraggable'
import api from '@/api'
import { DownloaderConf, MediaServerConf } from '@/api/types'
import DownloaderCard from '@/components/cards/DownloaderCard.vue'
import MediaServerCard from '@/components/cards/MediaServerCard.vue'
import { copyToClipboard } from '@/@core/utils/navigator'
import debounce from 'lodash/debounce'
import AdvancedNetworkSettingsDialog from '@/components/dialog/AdvancedNetworkSettingsDialog.vue'
import AdvancedSystemSettingsDialog from '@/components/dialog/AdvancedSystemSettingsDialog.vue'
// 系统设置
const SystemSettings = ref({
APP_DOMAIN: '',
// 系统设置默认值
const SystemSettings = ref<any>({
// 系统设置
Basis: {
// 基础设置
AUXILIARY_AUTH_ENABLE: false,
GLOBAL_IMAGE_CACHE: false,
APP_DOMAIN: '',
API_TOKEN: '',
WALLPAPER: 'tmdb',
PLUGIN_MARKET: '',
},
// 高级系统设置
Advanced: {
DEV: false,
DEBUG: false,
PLUGIN_AUTO_RELOAD: false,
REPO_GITHUB_TOKEN: '',
},
// 网络设置
Network: {
// 基础网络设置
TMDB_API_DOMAIN: '',
TMDB_IMAGE_DOMAIN: '',
DOH_ENABLE: true,
GITHUB_PROXY: '',
GITHUB_TOKEN: null,
PIP_PROXY: null,
// 隐藏设置,不在页面显示
PROXY_HOST: '',
},
// 高级网络设置
AdvancedNetwork: {
DOH_RESOLVERS: '',
DOH_DOMAINS: '',
OCR_HOST: '',
},
})
// 是否发送请求的总开关
const isRequest = ref(true)
// 防抖时间
const debounceTime = 500
// 选中的媒体服务器
const mediaServers = ref<MediaServerConf[]>([])
// 下载器
const downloaders = ref<DownloaderConf[]>([])
// 高级设置弹窗
const isAdvancedSystemSettingsDialogOpen = ref(false)
const isAdvancedNetworkSettingsDialogOpen = ref(false)
// 提示框
const $toast = useToast()
// 调用API查询下载器设置
async function loadDownloaderSetting() {
try {
const result: { [key: string]: any } = await api.get('system/setting/Downloaders')
downloaders.value = result.data?.value ?? []
} catch (error) {
console.log(error)
}
}
// 重载系统生效配置
async function reloadSystem() {
try {
const result: { [key: string]: any } = await api.get('system/reload')
if (result.success) $toast.success('系统配置已生效')
else $toast.error('重载系统失败!')
} catch (error) {
console.log(error)
}
}
// 调用API保存下载器设置
async function saveDownloaderSetting() {
try {
// 提取启用的下载器
const enabledDownloaders = downloaders.value.filter(item => item.enabled);
// 有启动的下载器时
if (enabledDownloaders.length > 0) {
downloaders.value = handleDefaultDownloaders(enabledDownloaders, downloaders.value);
}
const result: { [key: string]: any } = await api.post('system/setting/Downloaders', downloaders.value)
if (result.success) $toast.success('下载器设置保存成功')
else $toast.error('下载器设置保存失败!')
loadDownloaderSetting()
await reloadSystem()
} catch (error) {
console.log(error)
}
}
// 处理默认下载器状态
function handleDefaultDownloaders(enabledDownloaders: any[], downloaders: any[]) {
const enabledDefaultDownloader = enabledDownloaders.find(item => item.default);
if (enabledDownloaders.length > 0 && !enabledDefaultDownloader) {
downloaders = downloaders.map(item => {
if (item === enabledDownloaders[0]) {
$toast.info(`未设置默认下载器,已将【${item.name}】作为默认下载器`);
return {...item, default: true };
}
// 清除其他下载器的默认下载器状态
return {...item, default: false };
});
}
return downloaders;
}
// 调用API查询媒体服务器设置
async function loadMediaServerSetting() {
try {
const result: { [key: string]: any } = await api.get('system/setting/MediaServers')
mediaServers.value = result.data?.value ?? []
} catch (error) {
console.log(error)
}
}
// 调用API保存媒体服务器设置
async function saveMediaServerSetting() {
try {
const result: { [key: string]: any } = await api.post('system/setting/MediaServers', mediaServers.value)
if (result.success) $toast.success('媒体服务器设置保存成功')
else $toast.error('媒体服务器设置保存失败!')
loadMediaServerSetting()
await reloadSystem()
if (result.success) {
$toast.success('系统配置已生效')
await loadSystemSettings()
} else $toast.error('重载系统失败!')
} catch (error) {
console.log(error)
}
@@ -111,114 +73,197 @@ async function loadSystemSettings() {
try {
const result: { [key: string]: any } = await api.get('system/env')
if (result.success) {
const { APP_DOMAIN } = result.data
SystemSettings.value = {
APP_DOMAIN,
// 将API返回的值赋值给SystemSettings
for (const sectionKey of Object.keys(SystemSettings.value) as Array<keyof typeof SystemSettings.value>) {
Object.keys(SystemSettings.value[sectionKey]).forEach((key: string) => {
let v: any
if (result.data.hasOwnProperty(key)) {
v = result.data[key]
// 空字符串转为null避免空字符串导致前端显示问题
if (v === '') { v = null }
(SystemSettings.value[sectionKey] as any)[key] = v
}
})
}
}
} catch (error) {
console.log(error)
$toast.error('系统设置加载失败')
}
}
// 调用API保存系统设置
async function saveSystemSetting() {
// 调用API保存设置
async function saveSystemSetting(value: { [key: string]: any }) {
try {
const result: { [key: string]: any } = await api.post('system/env', SystemSettings.value)
const result: { [key: string]: any } = await api.post('system/env', value)
if (result.success) $toast.success('保存设置成功')
else $toast.error('保存设置失败!')
if (result.success) {
$toast.success('保存设置成功')
await reloadSystem()
await loadSystemSettings()
}
} catch (error) {
console.log(error)
$toast.error('保存设置失败!')
}
}
// 保存系统设置
const saveSystemSettings = debounce(async () => {
const Settings = { ...SystemSettings.value.Basis, ...SystemSettings.value.Advanced }
await saveSystemSetting(Settings)
}, debounceTime)
// 保存网络设置
const saveNetworkSettings = debounce(async () => {
const Settings = { ...SystemSettings.value.Network, ...SystemSettings.value.AdvancedNetwork }
// 查找PROXY_HOST并删除避免意外覆盖
if (Settings.PROXY_HOST) delete Settings.PROXY_HOST
await saveSystemSetting(Settings)
}, debounceTime)
// 高级设置变化,等待保存
function saveAdvancedSettings(Settings: any, key: string) {
if (!Settings) return
if (!key) return
// 检查Settings中的键是否在SystemSettings的[key]中存在有则使用Settings的值替换SystemSettings中的值
for (const settingKey in Settings) {
if (SystemSettings.value[key].hasOwnProperty(settingKey)) {
(SystemSettings.value[key] as any)[settingKey] = Settings[settingKey]
}
}
$toast.info('高级设置已更改,待保存后生效')
}
// 快捷复制到剪贴板
function copyValue(value: string) {
try {
copyToClipboard(value)
$toast.success('已复制到剪贴板')
} catch (error) {
$toast.error('复制失败!')
console.log(error)
}
}
// 添加下载器
function addDownloader(downloader: string) {
let name = `下载器${downloaders.value.length + 1}`;
while (downloaders.value.some(item => item.name === name)) {
name = `下载器${parseInt(name.split('下载器')[1]) + 1}`;
}
downloaders.value.push({
name: name,
type: downloader,
default: false,
enabled: false,
config: {},
})
// 登录首页壁纸来源
const wallpaperItems = [
{ title: 'TheMovieDB电影海报', value: 'tmdb' },
{ title: 'Bing每日壁纸', value: 'bing' },
]
// 创建随机字符串
function createRandomString() {
const charset = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_'
const array = new Uint8Array(16)
window.crypto.getRandomValues(array)
SystemSettings.value.Basis.API_TOKEN = Array.from(array, byte => charset[byte % charset.length]).join('')
}
// 删除下载器
function removeDownloader(ele: DownloaderConf) {
const index = downloaders.value.indexOf(ele)
downloaders.value.splice(index, 1)
}
// 预设部分Github加速站
const githubMirrorsItems = [
'https://mirror.ghproxy.com/', // GitHub Proxy
'https://ghp.ci/', // GitHub Proxy 子站
]
// 下载器变化
function onDownloaderChange(downloader: DownloaderConf) {
const index = downloaders.value.findIndex(item => item.name === downloader.name)
downloaders.value[index] = downloader
}
// 添加媒体服务器
function addMediaServer(mediaserver: string) {
let name = `服务器${mediaServers.value.length + 1}`;
while (mediaServers.value.some(item => item.name === name)) {
name = `服务器${parseInt(name.split('服务器')[1]) + 1}`;
}
mediaServers.value.push({
name: name,
type: mediaserver,
enabled: false,
config: {},
})
}
// 删除媒体服务器
function removeMediaServer(ele: MediaServerConf) {
const index = mediaServers.value.indexOf(ele)
mediaServers.value.splice(index, 1)
}
// 变更媒体服务器
function onMediaServerChange(mediaserver: MediaServerConf) {
const index = mediaServers.value.findIndex(item => item.name === mediaserver.name)
mediaServers.value[index] = mediaserver
}
// 预设部分PIP镜像站
const pipMirrorsItems = [
'https://pypi.tuna.tsinghua.edu.cn/simple', // 清华大学
'https://pypi.mirrors.ustc.edu.cn/simple', // 中国科技大学
'https://mirrors.pku.edu.cn/pypi/web/simple', // 北京大学
'https://mirrors.aliyun.com/pypi/simple', // 阿里云
'https://mirrors.cloud.tencent.com/pypi/simple', // 腾讯云
'https://mirrors.163.com/pypi/simple', // 网易云
'https://pypi.doubanio.com/simple', // 豆瓣
'https://mirrors.hust.edu.cn/pypi/web/simple', // 华中理工大学
'https://mirrors.bfsu.edu.cn/pypi/web/simple', // 北京外国语大学
]
// 加载数据
onMounted(() => {
loadDownloaderSetting()
loadMediaServerSetting()
loadSystemSettings()
})
onActivated(async () => {
isRequest.value = true
})
onDeactivated(() => {
isRequest.value = false
})
</script>
<template>
<VRow>
<!-- 系统设置 -->
<VCol cols="12">
<VCard>
<VCardItem>
<VCardTitle>系统</VCardTitle>
<VCardSubtitle>设置服务使用的域名等基础信息</VCardSubtitle>
<VCardTitle>基础系统设置</VCardTitle>
<VCardSubtitle>设置用户辅助认证登录首页壁纸访问域名插件市场等基础化设置</VCardSubtitle>
</VCardItem>
<VCardText>
<VForm>
<VRow>
<VCol cols="12" md="3">
<VSwitch
v-model="SystemSettings.Basis.AUXILIARY_AUTH_ENABLE"
label="用户辅助认证"
hint="允许通过外部服务,进行认证、单点登录以及自动创建用户"
persistent-hint
/>
</VCol>
<VCol cols="12" md="3">
<VSwitch
v-model="SystemSettings.Basis.GLOBAL_IMAGE_CACHE"
label="全局图片缓存"
hint="将媒体图片缓存到本地,增强用户体验"
persistent-hint
/>
</VCol>
<VCol cols="12" md="3">
<VSwitch
v-model="isAdvancedSystemSettingsDialogOpen"
label="高级系统设置"
hint="进入高级系统设置页面"
persistent-hint
/>
</VCol>
<VCol cols="12" md="3">
<VSelect
v-model="SystemSettings.Basis.WALLPAPER"
label="登录首页壁纸"
hint="选择登陆页面背景来源"
persistent-hint
:items="wallpaperItems"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="SystemSettings.APP_DOMAIN"
v-model="SystemSettings.Basis.APP_DOMAIN"
label="访问域名"
hint="用于通知跳转,格式http(s)://domain:port"
placeholder="格式http(s)://domain:port"
hint="用于发送通知时,添加快捷跳转地址"
persistent-hint
clearable
:appendInnerIcon="SystemSettings.Basis.APP_DOMAIN ? 'mdi-content-copy' : ''"
@click:appendInner="SystemSettings.Basis.APP_DOMAIN && copyValue(SystemSettings.Basis.APP_DOMAIN)"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="SystemSettings.Basis.API_TOKEN"
label="API Token"
hint="不得低于16位用于Jellyseerr/Overseerr、媒体服务器Webhook等配置以及部分支持API_TOKEN的API请求"
persistent-hint
clearable
prependInnerIcon="mdi-reload"
:appendInnerIcon="SystemSettings.Basis.API_TOKEN ? 'mdi-content-copy' : ''"
@click:prependInner="createRandomString"
@click:appendInner="SystemSettings.Basis.API_TOKEN && copyValue(SystemSettings.Basis.API_TOKEN)"
:rules="[(v: string) => !!v || '必填项请输入API Token', (v: string) => v.length >= 16 || 'API Token不得低于16位']"
/>
</VCol>
<VCol cols="12">
<VTextarea
v-model="SystemSettings.Basis.PLUGIN_MARKET"
label="插件市场"
placeholder="格式https://github.com/jxxghp/MoviePilot-Plugins/,https://github.com/xxxx/xxxxxx/"
hint="插件市场仓库地址,多个地址使用逗号分隔,确保每个地址以/结尾仅支持Github仓库"
persistent-hint
clearable
/>
</VCol>
</VRow>
@@ -227,109 +272,162 @@ onDeactivated(() => {
<VCardText>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn mtype="submit" @click="saveSystemSetting"> 保存 </VBtn>
</div>
</VForm>
</VCardText>
</VCard>
</VCol>
<VCol cols="12">
<VCard>
<VCardItem>
<VCardTitle>下载器</VCardTitle>
<VCardSubtitle>只有默认下载器才会被默认使用</VCardSubtitle>
</VCardItem>
<VCardText>
<draggable
v-model="downloaders"
handle=".cursor-move"
item-key="name"
tag="div"
:component-data="{ 'class': 'grid gap-3 grid-app-card' }"
>
<template #item="{ element }">
<DownloaderCard
:downloader="element"
:downloaders="downloaders"
@close="removeDownloader(element)"
@change="onDownloaderChange"
:allow-refresh="isRequest"
/>
</template>
</draggable>
</VCardText>
<VCardText>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn mtype="submit" @click="saveDownloaderSetting"> 保存 </VBtn>
<VBtn color="success" variant="tonal">
<VIcon icon="mdi-plus" />
<VMenu activator="parent" close-on-content-click>
<VList>
<VListItem variant="plain" @click="addDownloader('qbittorrent')">
<VListItemTitle>Qbittorrent</VListItemTitle>
</VListItem>
<VListItem variant="plain" @click="addDownloader('transmission')">
<VListItemTitle>Transmission</VListItemTitle>
</VListItem>
</VList>
</VMenu>
</VBtn>
<VBtn type="submit" @click="saveSystemSettings"> 保存 </VBtn>
</div>
</VForm>
</VCardText>
</VCard>
</VCol>
</VRow>
<VRow>
<VCol cols="12">
<VCard>
<VCardItem>
<VCardTitle>媒体服务器</VCardTitle>
<VCardSubtitle>所有启用的媒体服务器都会被使用</VCardSubtitle>
<VCardTitle>基础网络设置</VCardTitle>
<VCardSubtitle>设置DOHPIP加速站Github加速站Github Token等增加网络稳定性保证连通性</VCardSubtitle>
</VCardItem>
<VCardText>
<draggable
v-model="mediaServers"
handle=".cursor-move"
item-key="name"
tag="div"
:component-data="{ 'class': 'grid gap-3 grid-app-card' }"
>
<template #item="{ element }">
<MediaServerCard
:mediaserver="element"
:mediaservers="mediaServers"
@close="removeMediaServer(element)"
@change="onMediaServerChange"
/>
</template>
</draggable>
<VForm>
<VRow>
<VCol cols="12" md="6" class="flex align-center">
<div>
<VSwitch
v-model="SystemSettings.Network.DOH_ENABLE"
label="DNS over HTTPS解析"
hint="使用DOH服务器解析域名"
persistent-hint
/>
</div>
<div class="ml-10">
<VAlert type="info" variant="tonal" class="whitespace-pre-line" style="inline-size: fit-content">
<span v-if="SystemSettings.Network.PROXY_HOST"
>当前已成功配置 PROXY_HOST 建议关闭 DOH 功能</span
>
<span v-else>暂未配置 PROXY_HOST如出现网络连通性问题可考虑开启 DOH 功能 </span>
</VAlert>
</div>
</VCol>
<VCol cols="12" md="6" class="flex align-center">
<div>
<VSwitch
v-model="isAdvancedNetworkSettingsDialogOpen"
label="高级网络设置"
hint="进入高级网络设置页面"
persistent-hint
/>
</div>
<div class="ml-10">
<VAlert
type="info"
variant="tonal"
class="whitespace-pre-line mr-3"
style="inline-size: fit-content"
text="不建议修改,除非你知道它们的使用方法!"
/>
</div>
</VCol>
<VCol cols="12" md="6">
<VCombobox
v-model="SystemSettings.Network.TMDB_API_DOMAIN"
label="TMDB API域名"
placeholder="格式api.themoviedb.org"
hint="可替换为自定义的API域名"
persistent-hint
clearable
active
:items="['api.themoviedb.org']"
:rules="[(v: string) => !!v || '必填项请输入TMDB API域名']"
/>
</VCol>
<VCol cols="12" md="6">
<VCombobox
v-model="SystemSettings.Network.TMDB_IMAGE_DOMAIN"
label="TMDB 图片服务器"
placeholder="格式image.tmdb.org"
hint="可替换为自定义的图片域名"
persistent-hint
clearable
active
:items="['image.tmdb.org', 'static-mdb.v.geilijiasu.com']"
:rules="[(v: string) => !!v || '必填项请输入TMDB API域名']"
/>
</VCol>
<VCol cols="12" md="6">
<VCombobox
v-model="SystemSettings.Network.GITHUB_PROXY"
label="Github加速站"
placeholder="格式https://mirror.ghproxy.com/"
hint="留空则不使用预设部分可选站点也可手动输入自建站点。格式https://mirror.ghproxy.com/ 末尾需要带 /"
persistent-hint
clearable
:items="githubMirrorsItems"
active
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="SystemSettings.Network.GITHUB_TOKEN"
label="Github Token"
placeholder="ghp_**** 或 github_pat_****"
hint="用于提高Github API访问限流阈值"
persistent-hint
clearable
active
:appendInnerIcon="SystemSettings.Network.GITHUB_TOKEN ? 'mdi-content-copy' : ''"
@click:appendInner="
SystemSettings.Network.GITHUB_TOKEN && copyValue(SystemSettings.Network.GITHUB_TOKEN)
"
>
</VTextField>
</VCol>
<VCol cols="12" md="6">
<VCombobox
v-model="SystemSettings.Network.PIP_PROXY"
label="PIP加速站"
placeholder="格式https://pypi.tuna.tsinghua.edu.cn/simple"
hint="留空则不使用;预设部分可选站点,也可手动输入自建站点,格式: https://pypi.tuna.tsinghua.edu.cn/simple"
persistent-hint
clearable
:items="pipMirrorsItems"
active
/>
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardText>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn mtype="submit" @click="saveMediaServerSetting"> 保存 </VBtn>
<VBtn color="success" variant="tonal">
<VIcon icon="mdi-plus" />
<VMenu activator="parent" close-on-content-click>
<VList>
<VListItem variant="plain" @click="addMediaServer('emby')">
<VListItemTitle>Emby</VListItemTitle>
</VListItem>
<VListItem variant="plain" @click="addMediaServer('jellyfin')">
<VListItemTitle>Jellyfin</VListItemTitle>
</VListItem>
<VListItem variant="plain" @click="addMediaServer('plex')">
<VListItemTitle>Plex</VListItemTitle>
</VListItem>
</VList>
</VMenu>
</VBtn>
<VBtn type="submit" @click="saveNetworkSettings"> 保存 </VBtn>
</div>
</VForm>
</VCardText>
</VCard>
</VCol>
</VRow>
<!-- 高级系统设置 -->
<AdvancedSystemSettingsDialog
v-if="isAdvancedSystemSettingsDialogOpen"
v-model="isAdvancedSystemSettingsDialogOpen"
max-width="60rem"
persistent
z-index="1010"
:AdvancedSystemSettings="SystemSettings.Advanced"
@close="isAdvancedSystemSettingsDialogOpen = false"
@change="saveAdvancedSettings"
/>
<!-- 高级网络设置 -->
<AdvancedNetworkSettingsDialog
v-if="isAdvancedNetworkSettingsDialogOpen"
v-model="isAdvancedNetworkSettingsDialogOpen"
max-width="60rem"
persistent
z-index="1010"
:AdvancedNetworkSettings="SystemSettings.AdvancedNetwork"
@close="isAdvancedNetworkSettingsDialogOpen = false"
@change="saveAdvancedSettings"
/>
</template>

View File

@@ -0,0 +1,194 @@
<script lang="ts" setup>
import {useToast} from 'vue-toast-notification'
import api from "@/api"
import debounce from 'lodash/debounce'
// 防抖时间
const debounceTime = 500
// 提示框
const $toast = useToast()
// 系统设置
const SystemSettings = ref<any>({
Basis: {
FANART_ENABLE: false,
RECOGNIZE_SOURCE: 'themoviedb',
SCRAP_SOURCE: 'themoviedb',
META_CACHE_EXPIRE: 0,
MOVIE_RENAME_FORMAT: '',
TV_RENAME_FORMAT: '',
},
})
// 加载系统设置
async function loadSystemSettings() {
try {
const result: { [key: string]: any } = await api.get('system/env')
if (result.success) {
// 将API返回的值赋值给SystemSettings
for (const sectionKey of Object.keys(SystemSettings.value) as Array<keyof typeof SystemSettings.value>) {
Object.keys(SystemSettings.value[sectionKey]).forEach((key: string) => {
let v: any
if (result.data.hasOwnProperty(key)) {
v = result.data[key]
// 空字符串转为null避免空字符串导致前端显示问题
if (v === '') {
v = null
}
(SystemSettings.value[sectionKey] as any)[key] = v
}
})
}
} else $toast.error('加载设置失败!')
} catch (error) {
console.log(error)
}
}
// 保存设置
const saveSystemSettings = debounce(async (value: any) => {
try {
const result: { [key: string]: any } = await api.post('system/env', value)
if (result.success) {
$toast.success('保存设置成功')
await reloadSystem()
await loadSystemSettings()
} else $toast.error('保存设置失败!')
} catch (error) {
console.log(error)
}
}, debounceTime)
// 重载系统生效配置
async function reloadSystem() {
try {
const result: { [key: string]: any } = await api.get('system/reload')
if (result.success) {
$toast.success('系统配置已生效')
await loadSystemSettings()
} else $toast.error('重载系统失败!')
} catch (error) {
console.log(error)
}
}
// 恢复电影设置默认值
const loadDefaultMovieSetting = debounce(async () => {
SystemSettings.value.Basis.MOVIE_RENAME_FORMAT = '{{title}}{% if year %} ({{year}}){% endif %}/{{title}}{% if year %} ({{year}}){% endif %}{% if part %}-{{part}}{% endif %}{% if videoFormat %} - {{videoFormat}}{% endif %}{{fileExt}}'
}, debounceTime)
// 恢复电视剧设置默认值
const loadDefaultTVSetting = debounce(async () => {
SystemSettings.value.Basis.TV_RENAME_FORMAT = '{{title}}{% if year %} ({{year}}){% endif %}/Season {{season}}/{{title}} - {{season_episode}}{% if part %}-{{part}}{% endif %}{% if episode %} - 第 {{episode}} 集{% endif %}{{fileExt}}'
}, debounceTime)
// 数据源
const sourceItems = [
{ "title": "TheMovieDb", "value": "themoviedb"},
{ "title": "豆瓣", "value": "douban" }
]
// 加载数据
onMounted(() => {
loadSystemSettings()
})
</script>
<template>
<VRow>
<VCol cols="12">
<VCard>
<VCardItem>
<VCardTitle>基础设置</VCardTitle>
<VCardSubtitle>设置通用的整理转移功能</VCardSubtitle>
</VCardItem>
<VCardText>
<VRow>
<VCol cols="12" md="3">
<VSwitch
v-model="SystemSettings.Basis.FANART_ENABLE"
label="Fanart图片数据源"
hint="启用Fanart图片数据源"
persistent-hint
/>
</VCol>
<VCol cols="12" md="3">
<VSelect
v-model="SystemSettings.Basis.RECOGNIZE_SOURCE"
:items="sourceItems"
label="媒体信息识别来源"
hint="刮削时的媒体信息识别使用的数据源"
persistent-hint
/>
</VCol>
<VCol cols="12" md="3">
<VSelect
v-model="SystemSettings.Basis.SCRAP_SOURCE"
:items="sourceItems"
label="媒体刮削数据源"
hint="刮削元数据及图片使用的数据源"
persistent-hint
/>
</VCol>
<VCol cols="12" md="3">
<VTextField
v-model="SystemSettings.Basis.META_CACHE_EXPIRE"
label="元数据缓存过期时间"
hint="当缓存过期时间为 0 时,则使用内置默认值"
persistent-hint
min="0"
type="number"
suffix="小时"
:rules="[
v => v === 0 || !!v || '请输入元数据缓存时间',
v => v >= 0 || '元数据缓存时间必须大于等于0'
]"
/>
</VCol>
<VCol cols="12">
<VTextarea
v-model="SystemSettings.Basis.MOVIE_RENAME_FORMAT"
label="电影重命名格式"
hint="使用Jinja2语法"
persistent-hint
clearable
prependInnerIcon="mdi-reload"
@click:prependInner="loadDefaultMovieSetting"
active
/>
</VCol>
<VCol cols="12">
<VTextarea
v-model="SystemSettings.Basis.TV_RENAME_FORMAT"
label="电视剧重命名格式"
hint="使用Jinja2语法"
persistent-hint
clearable
prependInnerIcon="mdi-reload"
@click:prependInner="loadDefaultTVSetting"
active
/>
</VCol>
<VCol cols="12">
<VAlert type="info" variant="tonal" class="whitespace-pre-line" style="inline-size: fit-content">
<span>Jinja2语法参考</span>
<a href="https://jinja.palletsprojects.com/en/3.0.x/templates" target="_blank">
<u>https://jinja.palletsprojects.com/en/3.0.x/templates</u>
</a>
</VAlert>
</VCol>
</VRow>
</VCardText>
<VCardText>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveSystemSettings(SystemSettings.Basis)"> 保存</VBtn>
</div>
</VForm>
</VCardText>
</VCard>
</VCol>
</VRow>
</template>

View File

@@ -162,10 +162,16 @@ onMounted(() => {
</VAlert>
</VCardText>
<VCardText>
<VBtn type="submit" @click="saveCustomIdentifiers"> 保存 </VBtn>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveCustomIdentifiers"> 保存 </VBtn>
</div>
</VForm>
</VCardText>
</VCard>
</VCol>
</VRow>
<VRow>
<VCol cols="12">
<VCard>
<VCardItem>
@@ -182,10 +188,16 @@ onMounted(() => {
/>
</VCardText>
<VCardText>
<VBtn type="submit" @click="saveCustomReleaseGroups"> 保存 </VBtn>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveCustomReleaseGroups"> 保存 </VBtn>
</div>
</VForm>
</VCardText>
</VCard>
</VCol>
</VRow>
<VRow>
<VCol cols="12">
<VCard>
<VCardItem>
@@ -202,10 +214,16 @@ onMounted(() => {
/>
</VCardText>
<VCardText>
<VBtn type="submit" @click="saveCustomization"> 保存 </VBtn>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveCustomization"> 保存 </VBtn>
</div>
</VForm>
</VCardText>
</VCard>
</VCol>
</VRow>
<VRow>
<VCol cols="12">
<VCard>
<VCardItem>
@@ -222,7 +240,11 @@ onMounted(() => {
/>
</VCardText>
<VCardText>
<VBtn type="submit" @click="saveTransferExcludeWords"> 保存 </VBtn>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveTransferExcludeWords"> 保存 </VBtn>
</div>
</VForm>
</VCardText>
</VCard>
</VCol>

View File

@@ -73,9 +73,21 @@ const qrCode = ref('')
function changeAvatar(file: Event) {
const fileReader = new FileReader()
const { files } = file.target as HTMLInputElement
if (files && files.length > 0) {
fileReader.readAsDataURL(files[0])
const selectedFile = files[0]
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif']
const maxSize = 800 * 1024
// 检查文件是否为图片
if (!allowedTypes.includes(selectedFile.type)) {
$toast.error('上传的文件不符合要求,请重新选择头像');
return;
}
// 检查文件大小
if (selectedFile.size > maxSize) {
$toast.error('文件大小不得大于800KB')
return
}
fileReader.readAsDataURL(selectedFile)
fileReader.onload = () => {
if (typeof fileReader.result === 'string') {
currentAvatar.value = fileReader.result
@@ -285,7 +297,7 @@ watch(
</VBtn>
</div>
<p class="text-body-1 mb-0">允许 JPGPNGGIF 格式 最大尺寸 800K</p>
<p class="text-body-1 mb-0">允许 JPGPNGGIF 格式 最大尺寸 800KB</p>
</form>
</VCardText>