diff --git a/src/components/cards/CustomRuleCard.vue b/src/components/cards/CustomRuleCard.vue index 83ed6863..af74910b 100644 --- a/src/components/cards/CustomRuleCard.vue +++ b/src/components/cards/CustomRuleCard.vue @@ -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({ 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() { - + @@ -116,7 +160,7 @@ function onClose() { ({ 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(() => {
- +
@@ -192,7 +189,7 @@ onUnmounted(() => { { ({ 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() { - + @@ -234,7 +228,7 @@ function onClose() { - 确定 + 确定 diff --git a/src/components/cards/MediaServerCard.vue b/src/components/cards/MediaServerCard.vue index 88b83043..33548fa5 100644 --- a/src/components/cards/MediaServerCard.vue +++ b/src/components/cards/MediaServerCard.vue @@ -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({ name: '', @@ -70,8 +68,8 @@ const mediaServerInfo = ref({ // 打开详情弹窗 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(() => { - + @@ -202,7 +199,7 @@ onMounted(() => { { { ({ 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() {
{{ notificationTypeNames[notification.type] }}
- + @@ -160,7 +158,7 @@ function onClose() { +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', + } +} + + + diff --git a/src/components/dialog/AdvancedSystemSettingsDialog.vue b/src/components/dialog/AdvancedSystemSettingsDialog.vue new file mode 100644 index 00000000..41d402e4 --- /dev/null +++ b/src/components/dialog/AdvancedSystemSettingsDialog.vue @@ -0,0 +1,93 @@ + + + diff --git a/src/components/dialog/UserAddEditDialog.vue b/src/components/dialog/UserAddEditDialog.vue index e732831c..cf42e778 100644 --- a/src/components/dialog/UserAddEditDialog.vue +++ b/src/components/dialog/UserAddEditDialog.vue @@ -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(() => { -

允许 JPG、PNG、GIF 格式, 最大尺寸 800K。

+

允许 JPG、PNG、GIF 格式, 最大尺寸 800KB。

diff --git a/src/pages/setting.vue b/src/pages/setting.vue index 9fc87a16..4f996d0a 100644 --- a/src/pages/setting.vue +++ b/src/pages/setting.vue @@ -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" > -
+
{{ item.title }}
@@ -40,7 +42,7 @@ function jumpTab(tab: string) { - +
@@ -49,6 +51,15 @@ function jumpTab(tab: string) { + + + +
+ +
+
+
+ @@ -83,6 +94,15 @@ function jumpTab(tab: string) { + + + +
+ +
+
+
+ @@ -93,10 +113,10 @@ function jumpTab(tab: string) { - +
- +
diff --git a/src/router/menu.ts b/src/router/menu.ts index 0b8a9d83..7ca7c2ae 100644 --- a/src/router/menu.ts +++ b/src/router/menu.ts @@ -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: '定时作业', }, { diff --git a/src/views/setting/AccountSettingDirectory.vue b/src/views/setting/AccountSettingDirectory.vue index 0e9c06c5..381b973d 100644 --- a/src/views/setting/AccountSettingDirectory.vue +++ b/src/views/setting/AccountSettingDirectory.vue @@ -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([]) @@ -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(() => { - 保存 + +
+ 保存 +
+
+ + @@ -190,10 +200,14 @@ onMounted(() => { - 保存 - - - + +
+ 保存 + + + +
+
diff --git a/src/views/setting/AccountSettingNotification.vue b/src/views/setting/AccountSettingNotification.vue index 07904af6..e3ad3df2 100644 --- a/src/views/setting/AccountSettingNotification.vue +++ b/src/views/setting/AccountSettingNotification.vue @@ -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([]) +// 防抖时间 +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(() => { @@ -161,7 +172,7 @@ onMounted(() => {
保存 - + @@ -225,7 +236,7 @@ onMounted(() => {
- 保存 + 保存
diff --git a/src/views/setting/AccountSettingRule.vue b/src/views/setting/AccountSettingRule.vue index 99a6698a..9e5a2ec1 100644 --- a/src/views/setting/AccountSettingRule.vue +++ b/src/views/setting/AccountSettingRule.vue @@ -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([]) @@ -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(() => { - 保存 - - - - - - - - - - - + +
+ 保存 + + + + + + + + + + + +
+
+ + @@ -311,24 +405,30 @@ onMounted(() => { - 保存 - - - - - - - - - - - + +
+ 保存 + + + + + + + + + + + +
+
+
+ @@ -354,7 +454,11 @@ onMounted(() => { - 保存 + +
+ 保存 +
+
diff --git a/src/views/setting/AccountSettingScheduler.vue b/src/views/setting/AccountSettingScheduler.vue new file mode 100644 index 00000000..30b0aa1f --- /dev/null +++ b/src/views/setting/AccountSettingScheduler.vue @@ -0,0 +1,124 @@ + + + diff --git a/src/views/setting/AccountSettingSearch.vue b/src/views/setting/AccountSettingSearch.vue index 9d748c98..5fd0f713 100644 --- a/src/views/setting/AccountSettingSearch.vue +++ b/src/views/setting/AccountSettingSearch.vue @@ -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([]) // 选中订阅站点 const selectedSites = ref([]) +// 系统设置 +const SystemSettings = ref({ + 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) { + 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() }) @@ -151,7 +223,7 @@ onMounted(() => { clearable chips :items="mediaSourcesDict" - label="媒体数据源" + label="媒体搜索数据源" hint="搜索媒体信息时使用的数据源以及排序" persistent-hint /> @@ -171,10 +243,16 @@ onMounted(() => {
- 保存 + +
+ 保存 +
+
+ + @@ -196,7 +274,56 @@ onMounted(() => { - 保存 + +
+ 保存 +
+
+
+
+
+
+ + + + + 高级设置 + 设置交互搜索自动下载用户ID、字幕。 + + + + + + + + + + + + + + + + +
+ 保存 +
+
diff --git a/src/views/setting/AccountSettingService.vue b/src/views/setting/AccountSettingService.vue index 30b0aa1f..dd4391c2 100644 --- a/src/views/setting/AccountSettingService.vue +++ b/src/views/setting/AccountSettingService.vue @@ -1,124 +1,353 @@ + diff --git a/src/views/setting/AccountSettingSite.vue b/src/views/setting/AccountSettingSite.vue index 8df2f888..f6243b94 100644 --- a/src/views/setting/AccountSettingSite.vue +++ b/src/views/setting/AccountSettingSite.vue @@ -1,6 +1,10 @@ diff --git a/src/views/setting/AccountSettingTransfer.vue b/src/views/setting/AccountSettingTransfer.vue new file mode 100644 index 00000000..62028f82 --- /dev/null +++ b/src/views/setting/AccountSettingTransfer.vue @@ -0,0 +1,194 @@ + + + + diff --git a/src/views/setting/AccountSettingWords.vue b/src/views/setting/AccountSettingWords.vue index c9ba15e5..e9103644 100644 --- a/src/views/setting/AccountSettingWords.vue +++ b/src/views/setting/AccountSettingWords.vue @@ -162,10 +162,16 @@ onMounted(() => { - 保存 + +
+ 保存 +
+
+
+ @@ -182,10 +188,16 @@ onMounted(() => { /> - 保存 + +
+ 保存 +
+
+
+ @@ -202,10 +214,16 @@ onMounted(() => { /> - 保存 + +
+ 保存 +
+
+
+ @@ -222,7 +240,11 @@ onMounted(() => { /> - 保存 + +
+ 保存 +
+
diff --git a/src/views/user/UserProfileView.vue b/src/views/user/UserProfileView.vue index 13b42ba8..f65bb5c0 100644 --- a/src/views/user/UserProfileView.vue +++ b/src/views/user/UserProfileView.vue @@ -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(
-

允许 JPG、PNG、GIF 格式, 最大尺寸 800K。

+

允许 JPG、PNG、GIF 格式, 最大尺寸 800KB。