mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-05-30 04:39:44 +08:00
Merge pull request #225 from Aqr-K/v2-settings
feat(settings): 配置中心基本功能内置化,修复部分bug
This commit is contained in:
@@ -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="使用别名便于区分规则"
|
||||
|
||||
@@ -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="下载器的别名"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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="媒体服务器的别名"
|
||||
|
||||
@@ -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="通知渠道的别名"
|
||||
|
||||
98
src/components/dialog/AdvancedNetworkSettingsDialog.vue
Normal file
98
src/components/dialog/AdvancedNetworkSettingsDialog.vue
Normal 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>
|
||||
93
src/components/dialog/AdvancedSystemSettingsDialog.vue
Normal file
93
src/components/dialog/AdvancedSystemSettingsDialog.vue
Normal 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>
|
||||
@@ -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">允许 JPG、PNG、GIF 格式, 最大尺寸 800K。</p>
|
||||
<p class="text-body-1 mb-0">允许 JPG、PNG、GIF 格式, 最大尺寸 800KB。</p>
|
||||
</form>
|
||||
</VCardText>
|
||||
<VCardText>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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: '定时作业',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
124
src/views/setting/AccountSettingScheduler.vue
Normal file
124
src/views/setting/AccountSettingScheduler.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>设置DOH、PIP加速站、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>
|
||||
|
||||
194
src/views/setting/AccountSettingTransfer.vue
Normal file
194
src/views/setting/AccountSettingTransfer.vue
Normal 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">允许 JPG、PNG、GIF 格式, 最大尺寸 800K。</p>
|
||||
<p class="text-body-1 mb-0">允许 JPG、PNG、GIF 格式, 最大尺寸 800KB。</p>
|
||||
</form>
|
||||
</VCardText>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user