mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-05-21 08:10:16 +08:00
fix settings layout
This commit is contained in:
138
src/api/types.ts
138
src/api/types.ts
@@ -847,22 +847,124 @@ export interface SystemNotification {
|
||||
date: string
|
||||
}
|
||||
|
||||
// 下载目录/媒体库目录
|
||||
export interface MediaDirectory {
|
||||
// 类型 download/library
|
||||
type?: string
|
||||
// 别名
|
||||
name?: string
|
||||
// 路径
|
||||
path?: string
|
||||
// 媒体类型 电影/电视剧
|
||||
media_type?: string
|
||||
// 媒体类别 动画电影/国产剧
|
||||
category?: string
|
||||
// 刮削媒体信息
|
||||
scrape?: boolean
|
||||
// 自动二级分类,未指定类别时自动分类
|
||||
auto_category?: boolean
|
||||
// 优先级
|
||||
priority?: number
|
||||
// 下载器配置
|
||||
export interface DownloaderConf {
|
||||
// 名称
|
||||
name: string
|
||||
// 类型 qbittorrent/transmission
|
||||
type: string
|
||||
// 是否默认
|
||||
default: boolean
|
||||
// 配置
|
||||
config?: { [key: string]: any }
|
||||
// 是否启用
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
// 通知配置
|
||||
export interface NotificationConf {
|
||||
// 名称
|
||||
name: string
|
||||
// 类型 telegram/wechat/vocechat/synologychat
|
||||
type: string
|
||||
// 配置
|
||||
config?: { [key: string]: any }
|
||||
// 场景开关
|
||||
switchs?: string[]
|
||||
// 是否启用
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
// 通知场景开关配置
|
||||
export interface NotificationSwitchConf {
|
||||
// 场景名称
|
||||
type: string
|
||||
// 通知范围 all/user/admin
|
||||
action: string
|
||||
}
|
||||
|
||||
// 存储配置
|
||||
export interface StorageConf {
|
||||
// 类型 local/alipan/u115/rclone
|
||||
type: string
|
||||
// 配置
|
||||
config?: { [key: string]: any }
|
||||
}
|
||||
|
||||
// 媒体服务器配置
|
||||
export interface MediaServerConf {
|
||||
// 名称
|
||||
name: string
|
||||
// 类型 emby/jellyfin/plex
|
||||
type: string
|
||||
// 配置
|
||||
config?: { [key: string]: any }
|
||||
// 是否启用
|
||||
enabled: boolean
|
||||
// 同步媒体体库列表
|
||||
sync_libraries?: string[]
|
||||
}
|
||||
|
||||
// 文件整理目录配置
|
||||
export interface TransferDirectoryConf {
|
||||
// 名称
|
||||
name: string
|
||||
// 优先级
|
||||
priority: number
|
||||
// 存储
|
||||
storage: string
|
||||
// 下载目录
|
||||
download_path?: string
|
||||
// 适用媒体类型
|
||||
media_type?: string
|
||||
// 适用媒体类别
|
||||
media_category?: string
|
||||
// 下载类型子目录
|
||||
download_type_folder?: boolean
|
||||
// 下载类别子目录
|
||||
download_category_folder?: boolean
|
||||
// 监控方式 downloader/monitor,None为不监控
|
||||
monitor_type?: string
|
||||
// 整理方式 move/copy/link/softlink
|
||||
transfer_type?: string
|
||||
// 文件覆盖模式 always/size/never/latest
|
||||
overwrite_mode?: string
|
||||
// 整理到媒体库目录
|
||||
library_path?: string
|
||||
// 媒体库目录存储
|
||||
library_storage?: string
|
||||
// 智能重命名
|
||||
renaming?: boolean
|
||||
// 刮削
|
||||
scraping?: boolean
|
||||
// 媒体库类型子目录
|
||||
library_type_folder?: boolean
|
||||
// 媒体库类别子目录
|
||||
library_category_folder?: boolean
|
||||
}
|
||||
|
||||
// 自定义规则项
|
||||
export interface CustomRule {
|
||||
// 规则ID
|
||||
id: string
|
||||
// 名称
|
||||
name: string
|
||||
// 包含
|
||||
include?: string[]
|
||||
// 排除
|
||||
exclude?: string[]
|
||||
// 大小范围
|
||||
size_range?: string
|
||||
// 最少做种人数
|
||||
seeders?: string
|
||||
}
|
||||
|
||||
// 过滤规则组
|
||||
export interface FilterRuleGroup {
|
||||
// 名称
|
||||
name: string
|
||||
// 规则串
|
||||
rule_string?: string
|
||||
// 适用类媒体类别 None-全部 电影/电视剧
|
||||
media_type?: string
|
||||
}
|
||||
|
||||
2
src/components/cards/CustomRuleCard.vue
Normal file
2
src/components/cards/CustomRuleCard.vue
Normal file
@@ -0,0 +1,2 @@
|
||||
<script setup lang="ts"></script>
|
||||
<template></template>
|
||||
2
src/components/cards/DownloaderCard.vue
Normal file
2
src/components/cards/DownloaderCard.vue
Normal file
@@ -0,0 +1,2 @@
|
||||
<script setup lang="ts"></script>
|
||||
<template></template>
|
||||
2
src/components/cards/FilterRuleGroupCard.vue
Normal file
2
src/components/cards/FilterRuleGroupCard.vue
Normal file
@@ -0,0 +1,2 @@
|
||||
<script setup lang="ts"></script>
|
||||
<template></template>
|
||||
2
src/components/cards/MediaServerCard.vue
Normal file
2
src/components/cards/MediaServerCard.vue
Normal file
@@ -0,0 +1,2 @@
|
||||
<script setup lang="ts"></script>
|
||||
<template></template>
|
||||
2
src/components/cards/NotificationChannelCard.vue
Normal file
2
src/components/cards/NotificationChannelCard.vue
Normal file
@@ -0,0 +1,2 @@
|
||||
<script setup lang="ts"></script>
|
||||
<template></template>
|
||||
2
src/components/cards/StorageCard.vue
Normal file
2
src/components/cards/StorageCard.vue
Normal file
@@ -0,0 +1,2 @@
|
||||
<script setup lang="ts"></script>
|
||||
<template></template>
|
||||
2
src/components/cards/UserCard.vue
Normal file
2
src/components/cards/UserCard.vue
Normal file
@@ -0,0 +1,2 @@
|
||||
<script setup lang="ts"></script>
|
||||
<template></template>
|
||||
2
src/components/dialog/UserAddEditDialog.vue
Normal file
2
src/components/dialog/UserAddEditDialog.vue
Normal file
@@ -0,0 +1,2 @@
|
||||
<script setup lang="ts"></script>
|
||||
<template></template>
|
||||
@@ -79,7 +79,6 @@ watch(isCompactMode, value => {
|
||||
<VAvatar class="cursor-pointer ms-3" color="primary" variant="tonal">
|
||||
<VImg :src="avatar ?? avatar1" />
|
||||
|
||||
<!-- SECTION Menu -->
|
||||
<VMenu activator="parent" width="230" location="bottom end" offset="14px">
|
||||
<VList>
|
||||
<!-- 👉 User Avatar & Name -->
|
||||
|
||||
@@ -10,6 +10,7 @@ import AccountSettingSubscribe from '@/views/setting/AccountSettingSubscribe.vue
|
||||
import AccountSettingService from '@/views/setting/AccountSettingService.vue'
|
||||
import AccountSettingSystem from '@/views/setting/AccountSettingSystem.vue'
|
||||
import AccountSettingDirectory from '@/views/setting/AccountSettingDirectory.vue'
|
||||
import AccountSettingRule from '@/views/setting/AccountSettingRule.vue'
|
||||
import { SettingTabs } from '@/router/menu'
|
||||
|
||||
const route = useRoute()
|
||||
@@ -58,6 +59,13 @@ function jumpTab(tab: string) {
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
|
||||
<!-- 规则 -->
|
||||
<VWindowItem value="rule">
|
||||
<transition name="fade-slide" appear>
|
||||
<AccountSettingRule />
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
|
||||
<!-- 搜索 -->
|
||||
<VWindowItem value="search">
|
||||
<transition name="fade-slide" appear>
|
||||
|
||||
@@ -134,7 +134,7 @@ export const SettingTabs = [
|
||||
description: '下载器(Qbittorrent、Transmission)、媒体服务器(Emby、Jellyfin、Plex)',
|
||||
},
|
||||
{
|
||||
title: '目录',
|
||||
title: '存储 & 目录',
|
||||
icon: 'mdi-folder',
|
||||
tab: 'directory',
|
||||
description: '下载目录、媒体库目录、整理模式',
|
||||
@@ -145,6 +145,12 @@ export const SettingTabs = [
|
||||
tab: 'site',
|
||||
description: '站点同步、下载优先规则、站点重置',
|
||||
},
|
||||
{
|
||||
title: '规则',
|
||||
icon: 'mdi-filter',
|
||||
tab: 'rule',
|
||||
description: '优先级规则组',
|
||||
},
|
||||
{
|
||||
title: '搜索',
|
||||
icon: 'mdi-magnify',
|
||||
|
||||
@@ -181,6 +181,16 @@
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
|
||||
.grid-user-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
|
||||
.grid-app-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
|
||||
.v-tabs:not(.v-tabs-pill).v-tabs--horizontal {
|
||||
border-block-end: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
}
|
||||
|
||||
@@ -125,7 +125,6 @@ onMounted(() => {
|
||||
}
|
||||
})
|
||||
groupedDataList.value = groupMap
|
||||
|
||||
})
|
||||
|
||||
// 只监听filterForm和groupedDataList的变化。因为displayDataList的变化不需要清空列表
|
||||
@@ -265,12 +264,16 @@ function loadMore({ done }: { done: any }) {
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCard>
|
||||
<VInfiniteScroll mode="intersect" side="end" :items="displayDataList" class="overflow-hidden"
|
||||
@load="loadMore">
|
||||
<template #loading />
|
||||
<template #empty />
|
||||
<div class="grid gap-3 grid-torrent-card items-start">
|
||||
<TorrentCard v-for="item in displayDataList" :key="`${item.torrent_info.page_url}`" :torrent="item" :more="item.more" />
|
||||
</div>
|
||||
</VInfiniteScroll>
|
||||
<VInfiniteScroll mode="intersect" side="end" :items="displayDataList" class="overflow-hidden" @load="loadMore">
|
||||
<template #loading />
|
||||
<template #empty />
|
||||
<div class="grid gap-3 grid-torrent-card items-start">
|
||||
<TorrentCard
|
||||
v-for="item in displayDataList"
|
||||
:key="`${item.torrent_info.page_url}`"
|
||||
:torrent="item"
|
||||
:more="item.more"
|
||||
/>
|
||||
</div>
|
||||
</VInfiniteScroll>
|
||||
</template>
|
||||
|
||||
@@ -4,39 +4,14 @@ import { useToast } from 'vue-toast-notification'
|
||||
import draggable from 'vuedraggable'
|
||||
import { VRow } from 'vuetify/lib/components/index.mjs'
|
||||
import api from '@/api'
|
||||
import { MediaDirectory } from '@/api/types'
|
||||
import { TransferDirectoryConf, StorageConf } from '@/api/types'
|
||||
import DirectoryCard from '@/components/cards/DirectoryCard.vue'
|
||||
|
||||
// 媒体库设置项
|
||||
const transferSettings = ref({
|
||||
TRANSFER_TYPE: 'copy',
|
||||
OVERWRITE_MODE: 'size',
|
||||
TRANSFER_SAME_DISK: true,
|
||||
})
|
||||
|
||||
// 转移方式字典
|
||||
const transferTypeItems = [
|
||||
{ title: '硬链接', value: 'link' },
|
||||
{ title: '复制', value: 'copy' },
|
||||
{ title: '移动', value: 'move' },
|
||||
{ title: '软链接', value: 'softlink' },
|
||||
{ title: 'rclone复制', value: 'rclone_copy' },
|
||||
{ title: 'rclone移动', value: 'rclone_move' },
|
||||
]
|
||||
|
||||
// 覆盖模式字典
|
||||
const overwriteModeItems = [
|
||||
{ title: '从不覆盖', value: 'never' },
|
||||
{ title: '按大小覆盖', value: 'size' },
|
||||
{ title: '总是覆盖', value: 'always' },
|
||||
{ title: '仅保留最新版本', value: 'latest' },
|
||||
]
|
||||
|
||||
// 所有下载目录
|
||||
const downloadDirectories = ref<MediaDirectory[]>([])
|
||||
const directories = ref<TransferDirectoryConf[]>([])
|
||||
|
||||
// 所有媒体库目录
|
||||
const libraryDirectories = ref<MediaDirectory[]>([])
|
||||
// 所有存储
|
||||
const storages = ref<StorageConf[]>([])
|
||||
|
||||
// 二级分类策略
|
||||
const mediaCategories = ref<{ [key: string]: any }>({})
|
||||
@@ -44,148 +19,32 @@ const mediaCategories = ref<{ [key: string]: any }>({})
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
// 加载媒体库设置
|
||||
async function loadTransferSettings() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/env')
|
||||
if (result.success) {
|
||||
const { TRANSFER_TYPE, OVERWRITE_MODE, TRANSFER_SAME_DISK } = result.data
|
||||
transferSettings.value = {
|
||||
TRANSFER_TYPE,
|
||||
OVERWRITE_MODE,
|
||||
TRANSFER_SAME_DISK,
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 调用API保存媒体设置
|
||||
async function saveTransferSetting() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post('system/env', transferSettings.value)
|
||||
|
||||
if (result.success) $toast.success('保存媒体库设置成功')
|
||||
else $toast.error('保存媒体库设置失败!')
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 移动结束
|
||||
function orderDownloadCards() {
|
||||
function orderDirectoryCards() {
|
||||
// 更新所有目录的优先级
|
||||
downloadDirectories.value.forEach((item, index) => {
|
||||
directories.value.forEach((item, index) => {
|
||||
item.priority = index
|
||||
})
|
||||
}
|
||||
|
||||
// 移动结束
|
||||
function orderLibraryCards() {
|
||||
// 更新所有目录的优先级
|
||||
libraryDirectories.value.forEach((item, index) => {
|
||||
item.priority = index
|
||||
})
|
||||
}
|
||||
// 查询存储
|
||||
async function loadStorages() {}
|
||||
|
||||
// 关闭目录卡片
|
||||
function libraryCardClose(name: string) {
|
||||
libraryDirectories.value = libraryDirectories.value.filter(item => item.name !== name)
|
||||
}
|
||||
// 保存存储
|
||||
async function saveStorages() {}
|
||||
// 添加存储
|
||||
function addStorage() {}
|
||||
|
||||
// 关闭下载卡片
|
||||
function downloadCardClose(name: string) {
|
||||
downloadDirectories.value = downloadDirectories.value.filter(item => item.name !== name)
|
||||
}
|
||||
// 查询目录
|
||||
async function loadDirectories() {}
|
||||
|
||||
// 查询下载目录
|
||||
async function loadDownloadDirectories() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/setting/DownloadDirectories')
|
||||
if (result.success && result.data?.value) {
|
||||
downloadDirectories.value = result.data.value
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存下载目录
|
||||
async function saveDownloadDirectories() {
|
||||
orderDownloadCards()
|
||||
try {
|
||||
const value = downloadDirectories.value.map(item => {
|
||||
return {
|
||||
name: item.name,
|
||||
path: item.path,
|
||||
media_type: item.media_type,
|
||||
category: item.category,
|
||||
auto_category: item.auto_category,
|
||||
priority: item.priority,
|
||||
}
|
||||
})
|
||||
const result: { [key: string]: any } = await api.post('system/setting/DownloadDirectories', value)
|
||||
if (result.success) $toast.success('下载目录设置保存成功!')
|
||||
} catch (e) {
|
||||
console.error('保存下载目录设置失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 添加下载目录
|
||||
function addDownloadDirectory() {
|
||||
downloadDirectories.value.push({
|
||||
name: `下载目录${downloadDirectories.value.length + 1}`,
|
||||
path: '',
|
||||
media_type: '全部',
|
||||
category: '',
|
||||
})
|
||||
}
|
||||
|
||||
// 查询媒体库目录
|
||||
async function loadLibraryDirectories() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/setting/LibraryDirectories')
|
||||
if (result.success && result.data?.value) {
|
||||
libraryDirectories.value = result.data.value
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存媒体库目录
|
||||
async function saveLibraryDirectories() {
|
||||
orderLibraryCards()
|
||||
try {
|
||||
const value = libraryDirectories.value.map(item => {
|
||||
return {
|
||||
name: item.name,
|
||||
path: item.path,
|
||||
media_type: item.media_type,
|
||||
category: item.category,
|
||||
auto_category: item.auto_category,
|
||||
scrape: item.scrape,
|
||||
priority: item.priority,
|
||||
}
|
||||
})
|
||||
const result: { [key: string]: any } = await api.post('system/setting/LibraryDirectories', value)
|
||||
if (result.success) $toast.success('媒体库目录设置保存成功!')
|
||||
} catch (e) {
|
||||
console.error('保存媒体库目录设置失败')
|
||||
}
|
||||
// 保存目录
|
||||
async function saveDirectories() {
|
||||
orderDirectoryCards()
|
||||
}
|
||||
|
||||
// 添加媒体库目录
|
||||
function addLibraryDirectory() {
|
||||
libraryDirectories.value.push({
|
||||
name: `媒体库目录${libraryDirectories.value.length + 1}`,
|
||||
path: '',
|
||||
media_type: '全部',
|
||||
category: '',
|
||||
scrape: true,
|
||||
})
|
||||
}
|
||||
function addDirectory() {}
|
||||
|
||||
// 调用API查询自动分类配置
|
||||
async function loadMediaCategories() {
|
||||
@@ -198,10 +57,9 @@ async function loadMediaCategories() {
|
||||
|
||||
// 加载数据
|
||||
onMounted(() => {
|
||||
loadTransferSettings()
|
||||
loadDirectories()
|
||||
loadStorages()
|
||||
loadMediaCategories()
|
||||
loadDownloadDirectories()
|
||||
loadLibraryDirectories()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -210,32 +68,25 @@ onMounted(() => {
|
||||
<VCol cols="12">
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>下载目录</VCardTitle>
|
||||
<VCardSubtitle>设置下载目录路径和分类,按顺序依次匹配使用。</VCardSubtitle>
|
||||
<VCardTitle>存储</VCardTitle>
|
||||
<VCardSubtitle>设置本地或网盘存储参数。</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<draggable
|
||||
v-model="downloadDirectories"
|
||||
v-model="storages"
|
||||
handle=".cursor-move"
|
||||
item-key="pri"
|
||||
item-key="name"
|
||||
tag="div"
|
||||
@end="orderDownloadCards"
|
||||
:component-data="{ 'class': 'grid gap-3 grid-directory-card' }"
|
||||
:component-data="{ 'class': 'grid gap-3 grid-app-card' }"
|
||||
>
|
||||
<template #item="{ element }">
|
||||
<DirectoryCard
|
||||
type="download"
|
||||
:directory="element"
|
||||
:categories="mediaCategories"
|
||||
@update:modelValue="(value: string) => (element.path = value)"
|
||||
@close="downloadCardClose(element.name)"
|
||||
/>
|
||||
<StorageCard type="download" :storage="element" />
|
||||
</template>
|
||||
</draggable>
|
||||
</VCardText>
|
||||
<VCardText>
|
||||
<VBtn type="submit" class="me-2" @click="saveDownloadDirectories"> 保存 </VBtn>
|
||||
<VBtn color="success" variant="tonal" @click="addDownloadDirectory">
|
||||
<VBtn type="submit" class="me-2" @click="saveStorages"> 保存 </VBtn>
|
||||
<VBtn color="success" variant="tonal" @click="addStorage">
|
||||
<VIcon icon="mdi-plus" />
|
||||
</VBtn>
|
||||
</VCardText>
|
||||
@@ -244,17 +95,17 @@ onMounted(() => {
|
||||
<VCol cols="12">
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>媒体库目录</VCardTitle>
|
||||
<VCardSubtitle>设置媒体文件整理后存储目录和分类,按顺序依次匹配使用。</VCardSubtitle>
|
||||
<VCardTitle>目录</VCardTitle>
|
||||
<VCardSubtitle>设置媒体文件整理目录结构,按先后顺序依次匹配。</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<draggable
|
||||
v-model="libraryDirectories"
|
||||
v-model="directories"
|
||||
handle=".cursor-move"
|
||||
item-key="pri"
|
||||
tag="div"
|
||||
@end="orderLibraryCards"
|
||||
:component-data="{ 'class': 'grid gap-3 grid-directory-card' }"
|
||||
@end="orderDirectoryCards"
|
||||
:component-data="{ 'class': 'grid gap-3 grid-app-card' }"
|
||||
>
|
||||
<template #item="{ element }">
|
||||
<DirectoryCard
|
||||
@@ -262,65 +113,17 @@ onMounted(() => {
|
||||
:directory="element"
|
||||
:categories="mediaCategories"
|
||||
@update:modelValue="(value: string) => (element.path = value)"
|
||||
@close="libraryCardClose(element.name)"
|
||||
/>
|
||||
</template>
|
||||
</draggable>
|
||||
</VCardText>
|
||||
<VCardText>
|
||||
<VBtn type="submit" class="me-2" @click="saveLibraryDirectories"> 保存 </VBtn>
|
||||
<VBtn color="success" variant="tonal" @click="addLibraryDirectory">
|
||||
<VBtn type="submit" class="me-2" @click="saveDirectories"> 保存 </VBtn>
|
||||
<VBtn color="success" variant="tonal" @click="addDirectory">
|
||||
<VIcon icon="mdi-plus" />
|
||||
</VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>整理模式</VCardTitle>
|
||||
<VCardSubtitle>设置文件整理方式和偏好。</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VForm>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VSelect
|
||||
v-model="transferSettings.TRANSFER_TYPE"
|
||||
:items="transferTypeItems"
|
||||
label="整理方式"
|
||||
hint="文件从下载目录整理到媒体库目录的操作方式"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSelect
|
||||
v-model="transferSettings.OVERWRITE_MODE"
|
||||
:items="overwriteModeItems"
|
||||
label="覆盖模式"
|
||||
hint="媒体库中同名文件已存在时的覆盖方式"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="transferSettings.TRANSFER_SAME_DISK"
|
||||
label="同盘/同根目录优先"
|
||||
hint="优先整理到与下载目录同一磁盘/同一根路径的媒体库目录中"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<div class="d-flex flex-wrap gap-4 mt-4">
|
||||
<VBtn mtype="submit" @click="saveTransferSetting"> 保存 </VBtn>
|
||||
</div>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</template>
|
||||
|
||||
@@ -1,38 +1,12 @@
|
||||
<script lang="ts" setup>
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import api from '@/api'
|
||||
import type { NotificationSwitch } from '@/api/types'
|
||||
import draggable from 'vuedraggable'
|
||||
import type { NotificationConf } from '@/api/types'
|
||||
import NotificationChannelCard from '@/components/cards/NotificationChannelCard.vue'
|
||||
|
||||
const messagemTypes = ref<NotificationSwitch[]>([])
|
||||
|
||||
// 选中的消息渠道
|
||||
const selectedChannels = ref([])
|
||||
|
||||
// 消息渠道标签页
|
||||
const messagerTab = ref('wechat')
|
||||
|
||||
// 消息设置
|
||||
const notificationSettings = ref({
|
||||
WECHAT_CORPID: '',
|
||||
WECHAT_APP_SECRET: '',
|
||||
WECHAT_APP_ID: '',
|
||||
WECHAT_PROXY: '',
|
||||
WECHAT_TOKEN: '',
|
||||
WECHAT_ENCODING_AESKEY: '',
|
||||
WECHAT_ADMINS: '',
|
||||
TELEGRAM_TOKEN: '',
|
||||
TELEGRAM_CHAT_ID: '',
|
||||
TELEGRAM_USERS: '',
|
||||
TELEGRAM_ADMINS: '',
|
||||
SLACK_OAUTH_TOKEN: '',
|
||||
SLACK_APP_TOKEN: '',
|
||||
SLACK_CHANNEL: '',
|
||||
SYNOLOGYCHAT_WEBHOOK: '',
|
||||
SYNOLOGYCHAT_TOKEN: '',
|
||||
VOCECHAT_HOST: '',
|
||||
VOCECHAT_API_KEY: '',
|
||||
VOCECHAT_CHANNEL_ID: '',
|
||||
})
|
||||
// 所有消息渠道
|
||||
const notifications = ref<NotificationConf[]>([])
|
||||
|
||||
// 消息渠道
|
||||
const NotificationChannels = [
|
||||
@@ -65,120 +39,8 @@ const NotificationChannels = [
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
// 调用API查询消息开关
|
||||
async function loadNotificationSwitchs() {
|
||||
try {
|
||||
const result: NotificationSwitch[] = await api.get('message/switchs')
|
||||
|
||||
messagemTypes.value = result
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 调用API保存消息开关
|
||||
async function saveNotificationSwitchs() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post('message/switchs', messagemTypes.value)
|
||||
|
||||
if (result.success) $toast.success('保存通知消息设置成功')
|
||||
else $toast.error('保存通知消息设置失败!')
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 调用API查询消息渠道设置
|
||||
async function loadNotificationSettings() {
|
||||
try {
|
||||
const result1: { [key: string]: any } = await api.get('system/setting/MESSAGER')
|
||||
if (result1.success)
|
||||
selectedChannels.value = result1.data && result1.data.value ? result1.data.value.split(',') : []
|
||||
|
||||
const result2: { [key: string]: any } = await api.get('system/env')
|
||||
if (result2.success) {
|
||||
const {
|
||||
WECHAT_CORPID,
|
||||
WECHAT_APP_SECRET,
|
||||
WECHAT_APP_ID,
|
||||
WECHAT_PROXY,
|
||||
WECHAT_TOKEN,
|
||||
WECHAT_ENCODING_AESKEY,
|
||||
WECHAT_ADMINS,
|
||||
TELEGRAM_TOKEN,
|
||||
TELEGRAM_CHAT_ID,
|
||||
TELEGRAM_USERS,
|
||||
TELEGRAM_ADMINS,
|
||||
SLACK_OAUTH_TOKEN,
|
||||
SLACK_APP_TOKEN,
|
||||
SLACK_CHANNEL,
|
||||
SYNOLOGYCHAT_WEBHOOK,
|
||||
SYNOLOGYCHAT_TOKEN,
|
||||
VOCECHAT_HOST,
|
||||
VOCECHAT_API_KEY,
|
||||
VOCECHAT_CHANNEL_ID,
|
||||
} = result2.data
|
||||
notificationSettings.value = {
|
||||
WECHAT_CORPID,
|
||||
WECHAT_APP_SECRET,
|
||||
WECHAT_APP_ID,
|
||||
WECHAT_PROXY,
|
||||
WECHAT_TOKEN,
|
||||
WECHAT_ENCODING_AESKEY,
|
||||
WECHAT_ADMINS,
|
||||
TELEGRAM_TOKEN,
|
||||
TELEGRAM_CHAT_ID,
|
||||
TELEGRAM_USERS,
|
||||
TELEGRAM_ADMINS,
|
||||
SLACK_OAUTH_TOKEN,
|
||||
SLACK_APP_TOKEN,
|
||||
SLACK_CHANNEL,
|
||||
SYNOLOGYCHAT_WEBHOOK,
|
||||
SYNOLOGYCHAT_TOKEN,
|
||||
VOCECHAT_HOST,
|
||||
VOCECHAT_API_KEY,
|
||||
VOCECHAT_CHANNEL_ID,
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 调用API保存消息渠道设置
|
||||
async function saveNotificationSettings() {
|
||||
try {
|
||||
const result1: { [key: string]: any } = await api.post('system/setting/MESSAGER', selectedChannels.value.join(','))
|
||||
|
||||
const result2: { [key: string]: any } = await api.post('system/env', notificationSettings.value)
|
||||
|
||||
if (result1.success && result2.success) {
|
||||
$toast.success('保存通知渠道设置成功')
|
||||
reloadModule()
|
||||
} else {
|
||||
$toast.error('保存通知渠道设置失败!')
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 调用API接口重新加载模块
|
||||
async function reloadModule() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/reload')
|
||||
if (result.success) $toast.success('重新加载模块成功')
|
||||
else $toast.error('重新加载模块失败!')
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
onMounted(() => {
|
||||
loadNotificationSwitchs()
|
||||
loadNotificationSettings()
|
||||
})
|
||||
onMounted(() => {})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -187,231 +49,28 @@ onMounted(() => {
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>通知渠道</VCardTitle>
|
||||
<VCardSubtitle>只有选中的渠道才会发送消息。</VCardSubtitle>
|
||||
<VCardSubtitle>设置消息发送渠道参数。</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VForm>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VSelect
|
||||
v-model="selectedChannels"
|
||||
multiple
|
||||
chips
|
||||
:items="NotificationChannels"
|
||||
label="当前使用通知渠道"
|
||||
hint="消息通知渠道总开关"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol>
|
||||
<VTabs v-model="messagerTab" stacked>
|
||||
<VTab value="wechat"> 微信 </VTab>
|
||||
<VTab value="telegram"> Telegram </VTab>
|
||||
<VTab value="slack"> Slack </VTab>
|
||||
<VTab value="synologychat"> SynologyChat </VTab>
|
||||
<VTab value="vocechat"> VoceChat </VTab>
|
||||
</VTabs>
|
||||
<VWindow v-model="messagerTab" class="mt-5 disable-tab-transition" :touch="false">
|
||||
<VWindowItem value="wechat">
|
||||
<VForm>
|
||||
<VRow>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="notificationSettings.WECHAT_CORPID"
|
||||
label="企业ID"
|
||||
hint="企业微信后台企业信息中的企业ID"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="notificationSettings.WECHAT_APP_ID"
|
||||
label="应用 AgentId"
|
||||
hint="企业微信自建应用的AgentId"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="notificationSettings.WECHAT_APP_SECRET"
|
||||
label="应用 Secret"
|
||||
hint="企业微信自建应用的Secret"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="notificationSettings.WECHAT_PROXY"
|
||||
label="代理地址"
|
||||
hint="微信消息的转发代理地址,2022年6月20日后创建的自建应用才需要,不使用代理时需要保留默认值"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="notificationSettings.WECHAT_TOKEN"
|
||||
label="Token"
|
||||
hint="微信企业自建应用->API接收消息配置中的Token"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="notificationSettings.WECHAT_ENCODING_AESKEY"
|
||||
label="EncodingAESKey"
|
||||
hint="微信企业自建应用->API接收消息配置中的EncodingAESKey"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="notificationSettings.WECHAT_ADMINS"
|
||||
label="管理员白名单"
|
||||
placeholder="多个用,分隔"
|
||||
hint="可使用管理菜单及命令的用户ID列表,多个ID使用,分隔"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VWindowItem>
|
||||
<VWindowItem value="telegram">
|
||||
<VForm>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="notificationSettings.TELEGRAM_TOKEN"
|
||||
label="Bot Token"
|
||||
hint="Telegram机器人token,格式:123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="notificationSettings.TELEGRAM_CHAT_ID"
|
||||
label="Chat ID"
|
||||
hint="接受消息通知的用户、群组或频道Chat ID"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="notificationSettings.TELEGRAM_USERS"
|
||||
label="用户白名单"
|
||||
placeholder="多个用,分隔"
|
||||
hint="可使用Telegram机器人的用户ID清单,多个用户用,分隔,不填写则所有用户都能使用"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="notificationSettings.TELEGRAM_ADMINS"
|
||||
label="管理员白名单"
|
||||
placeholder="多个用,分隔"
|
||||
hint="可使用管理菜单及命令的用户ID列表,多个ID使用,分隔"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VWindowItem>
|
||||
<VWindowItem value="slack">
|
||||
<VForm>
|
||||
<VRow>
|
||||
<VCol cols="12" md="5">
|
||||
<VTextField
|
||||
v-model="notificationSettings.SLACK_OAUTH_TOKEN"
|
||||
label="Slack Bot User OAuth Token"
|
||||
placeholder="xoxb-xxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
hint="Slack应用`OAuth & Permissions`页面中的`Bot User OAuth Token`"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="5">
|
||||
<VTextField
|
||||
v-model="notificationSettings.SLACK_APP_TOKEN"
|
||||
label="Slack App-Level Token"
|
||||
placeholder="xapp-xxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
hint="Slack应用`OAuth & Permissions`页面中的`App-Level Token`"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="2">
|
||||
<VTextField
|
||||
v-model="notificationSettings.SLACK_CHANNEL"
|
||||
label="频道名称"
|
||||
placeholder="全体"
|
||||
hint="消息发送频道,默认`全体`"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VWindowItem>
|
||||
<VWindowItem value="synologychat">
|
||||
<VForm>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="notificationSettings.SYNOLOGYCHAT_WEBHOOK"
|
||||
label="机器人传入URL"
|
||||
hint="Synology Chat机器人传入URL"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="notificationSettings.SYNOLOGYCHAT_TOKEN"
|
||||
label="令牌"
|
||||
hint="Synology Chat机器人令牌"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VWindowItem>
|
||||
<VWindowItem value="vocechat">
|
||||
<VForm>
|
||||
<VRow>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="notificationSettings.VOCECHAT_HOST"
|
||||
label="地址"
|
||||
hint="VoceChat服务端地址,格式:http(s)://ip:port"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="notificationSettings.VOCECHAT_API_KEY"
|
||||
label="机器人密钥"
|
||||
hint="VoceChat机器人密钥"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="notificationSettings.VOCECHAT_CHANNEL_ID"
|
||||
label="频道ID"
|
||||
placeholder="不包含#号"
|
||||
hint="VoceChat的频道ID,不包含#号"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VWindowItem>
|
||||
</VWindow>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
<draggable
|
||||
v-model="notifications"
|
||||
handle=".cursor-move"
|
||||
item-key="name"
|
||||
tag="div"
|
||||
:component-data="{ 'class': 'grid gap-3 grid-app-card' }"
|
||||
>
|
||||
<template #item="{ element }">
|
||||
<NotificationChannelCard notification="element" />
|
||||
</template>
|
||||
</draggable>
|
||||
</VCardText>
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<div class="d-flex flex-wrap gap-4 mt-4">
|
||||
<VBtn mtype="submit" @click="saveNotificationSettings"> 保存 </VBtn>
|
||||
<VBtn mtype="submit" @click=""> 保存 </VBtn>
|
||||
<VBtn color="success" variant="tonal" @click="">
|
||||
<VIcon icon="mdi-plus" />
|
||||
</VBtn>
|
||||
</div>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
@@ -422,55 +81,14 @@ onMounted(() => {
|
||||
<VCol cols="12">
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>消息类型</VCardTitle>
|
||||
<VCardSubtitle>对应消息类型只会发送给选中的消息渠道。</VCardSubtitle>
|
||||
<VCardTitle>通知发送范围</VCardTitle>
|
||||
<VCardSubtitle>对应消息类型只会发送给设定的用户。</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VTable class="text-no-wrap">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">消息类型</th>
|
||||
<th scope="col">微信</th>
|
||||
<th scope="col">Telegram</th>
|
||||
<th scope="col">Slack</th>
|
||||
<th scope="col">SynologyChat</th>
|
||||
<th scope="col">VoceChat</th>
|
||||
<th scope="col">WebPush</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="message in messagemTypes" :key="message.mtype">
|
||||
<td>
|
||||
{{ message.mtype }}
|
||||
</td>
|
||||
<td>
|
||||
<VCheckbox v-model="message.wechat" />
|
||||
</td>
|
||||
<td>
|
||||
<VCheckbox v-model="message.telegram" />
|
||||
</td>
|
||||
<td>
|
||||
<VCheckbox v-model="message.slack" />
|
||||
</td>
|
||||
<td>
|
||||
<VCheckbox v-model="message.synologychat" />
|
||||
</td>
|
||||
<td>
|
||||
<VCheckbox v-model="message.vocechat" />
|
||||
</td>
|
||||
<td>
|
||||
<VCheckbox v-model="message.webpush" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="messagemTypes.length === 0">
|
||||
<td colspan="6" class="text-center">没有设置任何通知渠道</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</VTable>
|
||||
<VDivider />
|
||||
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<div class="d-flex flex-wrap gap-4 mt-4">
|
||||
<VBtn mtype="submit" @click="saveNotificationSwitchs"> 保存 </VBtn>
|
||||
<VBtn mtype="submit" @click=""> 保存 </VBtn>
|
||||
</div>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
|
||||
157
src/views/setting/AccountSettingRule.vue
Normal file
157
src/views/setting/AccountSettingRule.vue
Normal file
@@ -0,0 +1,157 @@
|
||||
<!-- eslint-disable sonarjs/no-duplicate-string -->
|
||||
<script lang="ts" setup>
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import draggable from 'vuedraggable'
|
||||
import { VRow } from 'vuetify/lib/components/index.mjs'
|
||||
import api from '@/api'
|
||||
import { CustomRule, FilterRuleGroup } from '@/api/types'
|
||||
import CustomerRuleCard from '@/components/cards/CustomRuleCard.vue'
|
||||
import FilterRuleGroupCard from '@/components/cards/FilterRuleGroupCard.vue'
|
||||
|
||||
// 自定义规则列表
|
||||
const customRules = ref<CustomRule[]>([])
|
||||
|
||||
// 所有规则组列表
|
||||
const filterRuleGroups = ref<FilterRuleGroup[]>([])
|
||||
|
||||
// 种子优先规则
|
||||
const selectedTorrentPriority = ref<string>('seeder')
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
// 种子优先规则下拉框
|
||||
const TorrentPriorityItems = [
|
||||
{ title: '站点优先', value: 'site' },
|
||||
{ title: '做种数优先', value: 'seeder' },
|
||||
]
|
||||
|
||||
// 保存自定义规则
|
||||
function saveCustomRules() {}
|
||||
|
||||
// 添加自定义规则
|
||||
function addCustomRule() {}
|
||||
|
||||
// 保存规则组
|
||||
function saveFilterRuleGroups() {}
|
||||
|
||||
// 添加规则组
|
||||
function addFilterRuleGroup() {}
|
||||
|
||||
// 查询种子优先规则
|
||||
async function queryTorrentPriority() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/setting/TorrentsPriority')
|
||||
|
||||
selectedTorrentPriority.value = result.data?.value
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存种子优先规则
|
||||
async function saveTorrentPriority() {
|
||||
try {
|
||||
// 用户名密码
|
||||
const result: { [key: string]: any } = await api.post(
|
||||
'system/setting/TorrentsPriority',
|
||||
selectedTorrentPriority.value,
|
||||
)
|
||||
|
||||
if (result.success) $toast.success('优先规则保存成功')
|
||||
else $toast.error('优先规则保存失败!')
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
onMounted(() => {
|
||||
queryTorrentPriority()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>自定义规则</VCardTitle>
|
||||
<VCardSubtitle>自定义优先级规则项</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<draggable
|
||||
v-model="customRules"
|
||||
handle=".cursor-move"
|
||||
item-key="name"
|
||||
tag="div"
|
||||
:component-data="{ 'class': 'grid gap-3 grid-filterrule-card' }"
|
||||
>
|
||||
<template #item="{ element }">
|
||||
<CustomerRuleCard :rule="element" />
|
||||
</template>
|
||||
</draggable>
|
||||
</VCardText>
|
||||
<VCardText>
|
||||
<VBtn type="submit" class="me-2" @click="saveCustomRules"> 保存 </VBtn>
|
||||
<VBtn color="success" variant="tonal" @click="addCustomRule">
|
||||
<VIcon icon="mdi-plus" />
|
||||
</VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>优先级规则组</VCardTitle>
|
||||
<VCardSubtitle>预设优先级规则组,以便在搜索和订阅中使用。</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<draggable
|
||||
v-model="filterRuleGroups"
|
||||
handle=".cursor-move"
|
||||
item-key="name"
|
||||
tag="div"
|
||||
:component-data="{ 'class': 'grid gap-3 grid-filterrule-card' }"
|
||||
>
|
||||
<template #item="{ element }">
|
||||
<FilterRuleGroupCard type="library" :filterrule="element" />
|
||||
</template>
|
||||
</draggable>
|
||||
</VCardText>
|
||||
<VCardText>
|
||||
<VBtn type="submit" class="me-2" @click="saveFilterRuleGroups"> 保存 </VBtn>
|
||||
<VBtn color="success" variant="tonal" @click="addFilterRuleGroup">
|
||||
<VIcon icon="mdi-plus" />
|
||||
</VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>下载规则</VCardTitle>
|
||||
<VCardSubtitle>按站点或做种数量优先下载。</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VForm>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VSelect
|
||||
v-model="selectedTorrentPriority"
|
||||
:items="TorrentPriorityItems"
|
||||
label="当前使用下载优先规则"
|
||||
hint="同时命中多个站点的多个资源时下载的优先规则"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
<VCardText>
|
||||
<VBtn type="submit" @click="saveTorrentPriority"> 保存 </VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</template>
|
||||
@@ -1,26 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import draggable from 'vuedraggable'
|
||||
import api from '@/api'
|
||||
import FilterRuleCard from '@/components/cards/FilterRuleCard.vue'
|
||||
import type { Site } from '@/api/types'
|
||||
import { copyToClipboard } from '@/@core/utils/navigator'
|
||||
import ImportCodeDialog from '@/components/dialog/ImportCodeDialog.vue'
|
||||
|
||||
// 规则卡片类型
|
||||
interface FilterCard {
|
||||
// 优先级
|
||||
pri: string
|
||||
// 已选规则
|
||||
rules: string[]
|
||||
}
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
// 规则卡片列表
|
||||
const filterCards = ref<FilterCard[]>([])
|
||||
|
||||
// 所有站点
|
||||
const allSites = ref<Site[]>([])
|
||||
|
||||
@@ -54,85 +39,11 @@ const mediaSourcesDict = [
|
||||
// 当前选中的媒体信息数据源
|
||||
const selectedMediaSource = ref([])
|
||||
|
||||
// 导入代码弹窗
|
||||
const importCodeDialog = ref(false)
|
||||
|
||||
// 导入的代码
|
||||
const importCodeString = ref('')
|
||||
|
||||
// 查询已设置优先级规则
|
||||
async function queryCustomFilters() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/setting/SearchFilterRules')
|
||||
if (result.success) {
|
||||
// 保存的是个字符串,需要分割成数组
|
||||
const groups = result.data?.value?.split('>') ?? []
|
||||
|
||||
// 生成规则卡片
|
||||
filterCards.value = groups?.map((group: string, index: number) => {
|
||||
return {
|
||||
pri: (index + 1).toString(),
|
||||
rules: group.split('&'),
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
// 当前选中的过滤规则组
|
||||
const selectedFilterGroup = ref([])
|
||||
|
||||
// 保存用户设置的规则
|
||||
async function saveCustomFilters() {
|
||||
try {
|
||||
// 有值才处理
|
||||
let value = ''
|
||||
if (filterCards.value.length !== 0) {
|
||||
// 将卡片规则接装为字符串
|
||||
value = filterCards.value
|
||||
.filter(card => card.rules.length > 0)
|
||||
.map(card => card.rules.join('&'))
|
||||
.join('>')
|
||||
}
|
||||
// 保存
|
||||
const result: { [key: string]: any } = await api.post('system/setting/SearchFilterRules', value)
|
||||
|
||||
if (result.success) $toast.success('搜索优先级保存成功')
|
||||
else $toast.error('搜索优先级保存失败!')
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 更新规则卡片的值
|
||||
function updateFilterCardValue(pri: string, rules: string[]) {
|
||||
const card = filterCards.value.find(card => card.pri === pri)
|
||||
if (card) card.rules = rules
|
||||
}
|
||||
|
||||
// 移除卡片
|
||||
function filterCardClose(pri: string) {
|
||||
// 将pri对应的卡片从列表中删除,并更新剩余卡片的序号
|
||||
const updatedCards = filterCards.value
|
||||
.filter(card => card.pri !== pri)
|
||||
.map((card, index) => {
|
||||
card.pri = (index + 1).toString()
|
||||
return card
|
||||
})
|
||||
// 更新 filterCards.value
|
||||
filterCards.value = updatedCards
|
||||
}
|
||||
|
||||
// 增加卡片
|
||||
function addFilterCard() {
|
||||
// 优先级
|
||||
const pri = (filterCards.value.length + 1).toString()
|
||||
|
||||
// 新卡片
|
||||
const newCard: FilterCard = { pri, rules: [] }
|
||||
|
||||
// 添加到列表
|
||||
filterCards.value.push(newCard)
|
||||
}
|
||||
async function saveCustomFilters() {}
|
||||
|
||||
// 查询所有站点
|
||||
async function querySites() {
|
||||
@@ -171,78 +82,8 @@ async function saveSelectedSites() {
|
||||
}
|
||||
}
|
||||
|
||||
// 根据列表的拖动顺序更新优先级
|
||||
function dragOrderEnd() {
|
||||
filterCards.value = filterCards.value.map((card, index) => {
|
||||
card.pri = (index + 1).toString()
|
||||
return card
|
||||
})
|
||||
}
|
||||
|
||||
// 查询包含与排除规则
|
||||
async function queryDefaultFilter() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/setting/DefaultSearchFilterRules')
|
||||
if (result.data?.value) defaultFilterRules.value = result.data?.value
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存包含与排除规则
|
||||
async function saveDefaultFilter() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post(
|
||||
'system/setting/DefaultSearchFilterRules',
|
||||
defaultFilterRules.value,
|
||||
)
|
||||
if (result.success) $toast.success('默认包含/排除规则保存成功')
|
||||
else $toast.error('默认包含/排除规则保存失败!')
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 分享规则
|
||||
function shareRules() {
|
||||
// 有值才处理
|
||||
if (filterCards.value.length === 0) return
|
||||
|
||||
// 将卡片规则接装为字符串
|
||||
const value = filterCards.value
|
||||
.filter(card => card.rules.length > 0)
|
||||
.map(card => card.rules.join('&'))
|
||||
.join('>')
|
||||
|
||||
// 复制到剪贴板
|
||||
try {
|
||||
copyToClipboard(value)
|
||||
$toast.success('优先级规则已复制到剪贴板')
|
||||
} catch (error) {
|
||||
$toast.error('优先级规则复制失败!')
|
||||
}
|
||||
}
|
||||
|
||||
// 监听导入代码变化
|
||||
watchEffect(() => {
|
||||
if (!importCodeString.value) return
|
||||
|
||||
// 导入代码需要以空格开头和结束,没有则拼接
|
||||
if (!importCodeString.value.startsWith(' ')) importCodeString.value = ` ${importCodeString.value}`
|
||||
if (!importCodeString.value.endsWith(' ')) importCodeString.value = `${importCodeString.value} `
|
||||
|
||||
// 将导入的代码转换为规则卡片
|
||||
const groups = importCodeString.value.split('>')
|
||||
filterCards.value = groups.map((group: string, index: number) => {
|
||||
return {
|
||||
pri: (index + 1).toString(),
|
||||
rules: group.split('&'),
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 调用API查询下载器设置
|
||||
async function loadMediaSourceSetting() {
|
||||
async function loadSearchSetting() {
|
||||
try {
|
||||
const result1: { [key: string]: any } = await api.get('system/setting/SEARCH_SOURCE')
|
||||
if (result1.success) selectedMediaSource.value = result1.data?.value?.split(',')
|
||||
@@ -251,8 +92,8 @@ async function loadMediaSourceSetting() {
|
||||
}
|
||||
}
|
||||
|
||||
// 调用API保存下载器设置
|
||||
async function saveMediaSourceSetting() {
|
||||
// 调用API保存设置
|
||||
async function saveSearchSetting() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post(
|
||||
'system/setting/SEARCH_SOURCE',
|
||||
@@ -270,10 +111,8 @@ async function saveMediaSourceSetting() {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
queryCustomFilters()
|
||||
querySites()
|
||||
queryDefaultFilter()
|
||||
loadMediaSourceSetting()
|
||||
loadSearchSetting()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -282,8 +121,8 @@ onMounted(() => {
|
||||
<VCol cols="12">
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>媒体数据源</VCardTitle>
|
||||
<VCardSubtitle>设定搜索时展示哪些源的媒体信息。</VCardSubtitle>
|
||||
<VCardTitle>搜索数据源 & 规则组</VCardTitle>
|
||||
<VCardSubtitle>设定数据源、规则组等基础信息。</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VRow>
|
||||
@@ -293,15 +132,26 @@ onMounted(() => {
|
||||
multiple
|
||||
chips
|
||||
:items="mediaSourcesDict"
|
||||
label="当前使用数据源"
|
||||
label="媒体数据源"
|
||||
hint="搜索媒体信息时使用的数据源以及排序"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSelect
|
||||
v-model="selectedFilterGroup"
|
||||
multiple
|
||||
chips
|
||||
:items="[]"
|
||||
label="过滤规则组"
|
||||
hint="搜索媒体信息时按选定的过滤规则组对结果进行过滤"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
<VCardText>
|
||||
<VBtn type="submit" @click="saveMediaSourceSetting"> 保存 </VBtn>
|
||||
<VBtn type="submit" @click="saveSearchSetting"> 保存 </VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
@@ -330,118 +180,5 @@ onMounted(() => {
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<template #append>
|
||||
<IconBtn>
|
||||
<VIcon icon="mdi-dots-vertical" />
|
||||
<VMenu activator="parent" close-on-content-click>
|
||||
<VList>
|
||||
<VListItem variant="plain" @click="shareRules">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-share" />
|
||||
</template>
|
||||
<VListItemTitle>分享</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem variant="plain" @click="importCodeDialog = true">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-import" />
|
||||
</template>
|
||||
<VListItemTitle>导入</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</template>
|
||||
<VCardTitle>搜索优先级</VCardTitle>
|
||||
<VCardSubtitle>设置在搜索时默认使用的优先级排序,未在优先级中的资源将不在搜索结果中显示。</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<draggable
|
||||
v-model="filterCards"
|
||||
handle=".cursor-move"
|
||||
item-key="pri"
|
||||
tag="div"
|
||||
@end="dragOrderEnd"
|
||||
:component-data="{ 'class': 'grid gap-3 grid-filterrule-card' }"
|
||||
>
|
||||
<template #item="{ element }">
|
||||
<FilterRuleCard
|
||||
:pri="element.pri"
|
||||
:maxpri="filterCards.length.toString()"
|
||||
:rules="element.rules"
|
||||
@changed="updateFilterCardValue"
|
||||
@close="filterCardClose(element.pri)"
|
||||
/>
|
||||
</template>
|
||||
</draggable>
|
||||
</VCardText>
|
||||
<VCardText>
|
||||
<VBtn type="submit" class="me-2" @click="saveCustomFilters()"> 保存 </VBtn>
|
||||
<VBtn color="success" variant="tonal" @click="addFilterCard()">
|
||||
<VIcon icon="mdi-plus" />
|
||||
</VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>默认过滤规则</VCardTitle>
|
||||
<VCardSubtitle>设置在搜索时默认使用的过滤规则。</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VForm>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="defaultFilterRules.include"
|
||||
type="text"
|
||||
label="包含(关键字、正则式)"
|
||||
hint="包含规则,支持正式表达式,多个关键字用 | 分隔表示或"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="defaultFilterRules.exclude"
|
||||
type="text"
|
||||
label="排除(关键字、正则式)"
|
||||
hint="排除规则,支持正式表达式,多个关键字用 | 分隔表示或"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="defaultFilterRules.min_seeders"
|
||||
type="text"
|
||||
label="最小做种数"
|
||||
placeholder="0"
|
||||
hint="小于该值的资源将被过滤掉,0表示不过滤"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="defaultFilterRules.min_seeders_time"
|
||||
type="text"
|
||||
label="最少做种数生效发布时间(分钟)"
|
||||
placeholder="0"
|
||||
hint="发布时间距当前时间大于该值的资源将生效最小做种数规则,0表示不生效"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
<VCardText>
|
||||
<VBtn type="submit" @click="saveDefaultFilter"> 保存 </VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VDialog v-model="importCodeDialog" width="60rem" scrollable>
|
||||
<ImportCodeDialog v-model="importCodeString" title="导入优先级规则" @close="importCodeDialog = false" />
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
@@ -14,9 +14,6 @@ const resetSitesText = ref('重置站点数据')
|
||||
// 站点重置按钮可用状态
|
||||
const resetSitesDisabled = ref(false)
|
||||
|
||||
// 种子优先规则
|
||||
const selectedTorrentPriority = ref<string>('seeder')
|
||||
|
||||
// CookieCloud设置项
|
||||
const cookieCloudSetting = ref({
|
||||
COOKIECLOUD_HOST: '',
|
||||
@@ -28,12 +25,6 @@ const cookieCloudSetting = ref({
|
||||
COOKIECLOUD_BLACKLIST: '',
|
||||
})
|
||||
|
||||
// 种子优先规则下拉框
|
||||
const TorrentPriorityItems = [
|
||||
{ title: '站点优先', value: 'site' },
|
||||
{ title: '做种数优先', value: 'seeder' },
|
||||
]
|
||||
|
||||
// 同步间隔下拉框
|
||||
const CookieCloudIntervalItems = [
|
||||
{ title: '每小时', value: 60 },
|
||||
@@ -62,33 +53,6 @@ async function resetSites() {
|
||||
}
|
||||
}
|
||||
|
||||
// 查询种子优先规则
|
||||
async function queryTorrentPriority() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/setting/TorrentsPriority')
|
||||
|
||||
selectedTorrentPriority.value = result.data?.value
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存种子优先规则
|
||||
async function saveTorrentPriority() {
|
||||
try {
|
||||
// 用户名密码
|
||||
const result: { [key: string]: any } = await api.post(
|
||||
'system/setting/TorrentsPriority',
|
||||
selectedTorrentPriority.value,
|
||||
)
|
||||
|
||||
if (result.success) $toast.success('优先规则保存成功')
|
||||
else $toast.error('优先规则保存失败!')
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载CookieCloud设置
|
||||
async function loadCookieCloudSettings() {
|
||||
try {
|
||||
@@ -132,7 +96,6 @@ async function saveCookieCloudetting() {
|
||||
|
||||
// 加载数据
|
||||
onMounted(() => {
|
||||
queryTorrentPriority()
|
||||
loadCookieCloudSettings()
|
||||
})
|
||||
</script>
|
||||
@@ -219,32 +182,7 @@ onMounted(() => {
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>下载优先规则</VCardTitle>
|
||||
<VCardSubtitle>按站点或做种数量优先下载。</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VForm>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VSelect
|
||||
v-model="selectedTorrentPriority"
|
||||
:items="TorrentPriorityItems"
|
||||
label="当前使用下载优先规则"
|
||||
hint="同时命中多个站点的多个资源时下载的优先规则"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
<VCardText>
|
||||
<VBtn type="submit" @click="saveTorrentPriority"> 保存 </VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<VCard title="站点重置">
|
||||
<VCardText>
|
||||
|
||||
@@ -1,37 +1,19 @@
|
||||
<script lang="ts" setup>
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import draggable from 'vuedraggable'
|
||||
import api from '@/api'
|
||||
import FilterRuleCard from '@/components/cards/FilterRuleCard.vue'
|
||||
import type { Site } from '@/api/types'
|
||||
import { copyToClipboard } from '@/@core/utils/navigator'
|
||||
import ImportCodeDialog from '@/components/dialog/ImportCodeDialog.vue'
|
||||
|
||||
// 规则卡片类型
|
||||
interface FilterCard {
|
||||
// 优先级
|
||||
pri: string
|
||||
// 已选规则
|
||||
rules: string[]
|
||||
}
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
// 订阅规则卡片列表
|
||||
const subscribeFilterCards = ref<FilterCard[]>([])
|
||||
|
||||
// 洗版规则卡片列表
|
||||
const bestVersionFilterCards = ref<FilterCard[]>([])
|
||||
|
||||
// 所有站点
|
||||
const allSites = ref<Site[]>([])
|
||||
|
||||
// 选中订阅站点
|
||||
const selectedRssSites = ref<number[]>([])
|
||||
|
||||
// 当前规则类型
|
||||
const currentRuleType = ref('SubscribeFilterRules')
|
||||
// 选中的优先级规则组
|
||||
const selectedFilterRuleGroup = ref([])
|
||||
|
||||
// 是否开启订阅定时搜索
|
||||
const enableIntervalSearch = ref(false)
|
||||
@@ -69,12 +51,6 @@ const rssIntervalItems = [
|
||||
// 选择的RSS运行周期
|
||||
const selectedRssInterval = ref<number>(5)
|
||||
|
||||
// 导入代码弹窗
|
||||
const importCodeDialog = ref(false)
|
||||
|
||||
// 导入的代码
|
||||
const importCodeString = ref('')
|
||||
|
||||
// 查询用户选中的订阅站点
|
||||
async function querySelectedRssSites() {
|
||||
try {
|
||||
@@ -133,178 +109,8 @@ async function querySites() {
|
||||
}
|
||||
}
|
||||
|
||||
// 查询已设置优先级规则
|
||||
async function queryCustomFilters(ruleType: string) {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get(`system/setting/${ruleType}`)
|
||||
if (result.success) {
|
||||
// 保存的是个字符串,需要分割成数组
|
||||
const groups = result.data?.value?.split('>') ?? []
|
||||
|
||||
// 生成规则卡片
|
||||
const cards = ruleType === 'SubscribeFilterRules' ? subscribeFilterCards : bestVersionFilterCards
|
||||
cards.value = groups?.map((group: string, index: number) => {
|
||||
return {
|
||||
pri: (index + 1).toString(),
|
||||
rules: group.split('&'),
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存用户设置的规则
|
||||
async function saveCustomFilters(ruleType: string) {
|
||||
try {
|
||||
// 有值才处理
|
||||
let value = ''
|
||||
const cards = ruleType === 'SubscribeFilterRules' ? subscribeFilterCards : bestVersionFilterCards
|
||||
if (cards.value.length !== 0) {
|
||||
// 将卡片规则接装为字符串
|
||||
value = cards.value
|
||||
.filter(card => card.rules.length > 0)
|
||||
.map(card => card.rules.join('&'))
|
||||
.join('>')
|
||||
}
|
||||
// 保存
|
||||
const result: { [key: string]: any } = await api.post(`system/setting/${ruleType}`, value)
|
||||
|
||||
const msg = ruleType === 'SubscribeFilterRules' ? '订阅优先级' : '洗版优先级'
|
||||
|
||||
if (result.success) $toast.success(`${msg}保存成功`)
|
||||
else $toast.error(`${msg}保存失败!`)
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 更新规则卡片的值
|
||||
function updateSubscribeFilterCardValue(pri: string, rules: string[]) {
|
||||
const card = subscribeFilterCards.value.find(card => card.pri === pri)
|
||||
if (card) card.rules = rules
|
||||
}
|
||||
|
||||
// 更新洗版规则卡片的值
|
||||
function updateBestVersionFilterCardValue(pri: string, rules: string[]) {
|
||||
const card = bestVersionFilterCards.value.find(card => card.pri === pri)
|
||||
if (card) card.rules = rules
|
||||
}
|
||||
|
||||
// 移除卡片
|
||||
function filterCardClose(ruleType: string, pri: string) {
|
||||
// 将pri对应的卡片从列表中删除,并更新剩余卡片的序号
|
||||
const updatedCards = (ruleType === 'SubscribeFilterRules' ? subscribeFilterCards.value : bestVersionFilterCards.value)
|
||||
.filter(card => card.pri !== pri)
|
||||
.map((card, index) => {
|
||||
card.pri = (index + 1).toString()
|
||||
return card
|
||||
})
|
||||
// 更新 subscribeFilterCards.value
|
||||
if (ruleType === 'SubscribeFilterRules') subscribeFilterCards.value = updatedCards
|
||||
else bestVersionFilterCards.value = updatedCards
|
||||
}
|
||||
|
||||
// 增加卡片
|
||||
function addFilterCard(ruleType: string) {
|
||||
const cards = ruleType === 'SubscribeFilterRules' ? subscribeFilterCards : bestVersionFilterCards
|
||||
// 优先级
|
||||
const pri = (cards.value.length + 1).toString()
|
||||
|
||||
// 新卡片
|
||||
const newCard: FilterCard = { pri, rules: [] }
|
||||
|
||||
// 添加到列表
|
||||
cards.value.push(newCard)
|
||||
}
|
||||
|
||||
// 根据列表的拖动顺序更新优先级
|
||||
function dragOrderEnd(ruleType: string) {
|
||||
;(ruleType === 'SubscribeFilterRules' ? subscribeFilterCards.value : bestVersionFilterCards.value).map(
|
||||
(card, index) => {
|
||||
card.pri = (index + 1).toString()
|
||||
return card
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// 查询包含与排除规则
|
||||
async function queryDefaultFilter() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/setting/DefaultFilterRules')
|
||||
if (result.data?.value) defaultFilterRules.value = result.data?.value
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存包含与排除规则
|
||||
async function saveDefaultFilter() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post('system/setting/DefaultFilterRules', defaultFilterRules.value)
|
||||
if (result.success) $toast.success('默认包含/排除规则保存成功')
|
||||
else $toast.error('默认包含/排除规则保存失败!')
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 分享规则
|
||||
function shareRules(ruleType: string) {
|
||||
let filterCards: Ref<FilterCard[]>
|
||||
if (ruleType === 'SubscribeFilterRules') filterCards = subscribeFilterCards
|
||||
else filterCards = bestVersionFilterCards
|
||||
// 有值才处理
|
||||
if (filterCards.value.length === 0) return
|
||||
|
||||
// 将卡片规则接装为字符串
|
||||
const value = filterCards.value
|
||||
.filter(card => card.rules.length > 0)
|
||||
.map(card => card.rules.join('&'))
|
||||
.join('>')
|
||||
|
||||
// 复制到剪贴板
|
||||
try {
|
||||
copyToClipboard(value)
|
||||
$toast.success('优先级规则已复制到剪贴板')
|
||||
} catch (error) {
|
||||
$toast.error('优先级规则复制失败!')
|
||||
}
|
||||
}
|
||||
|
||||
// 导入规则
|
||||
async function importRules(ruleType: string) {
|
||||
currentRuleType.value = ruleType
|
||||
importCodeString.value = ''
|
||||
importCodeDialog.value = true
|
||||
}
|
||||
|
||||
// 监听导入代码变化
|
||||
watchEffect(() => {
|
||||
if (!importCodeString.value) return
|
||||
if (!currentRuleType.value) return
|
||||
// 导入代码需要以空格开头和结束,没有则拼接
|
||||
if (!importCodeString.value.startsWith(' ')) importCodeString.value = ` ${importCodeString.value}`
|
||||
if (!importCodeString.value.endsWith(' ')) importCodeString.value = `${importCodeString.value} `
|
||||
let filterCards: Ref<FilterCard[]>
|
||||
if (currentRuleType.value === 'SubscribeFilterRules') filterCards = subscribeFilterCards
|
||||
else filterCards = bestVersionFilterCards
|
||||
// 将导入的代码转换为规则卡片
|
||||
const groups = importCodeString.value.split('>')
|
||||
filterCards.value = groups.map((group: string, index: number) => {
|
||||
return {
|
||||
pri: (index + 1).toString(),
|
||||
rules: group.split('&'),
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
querySites()
|
||||
queryCustomFilters('SubscribeFilterRules')
|
||||
queryCustomFilters('BestVersionFilterRules')
|
||||
queryDefaultFilter()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -313,23 +119,9 @@ onMounted(() => {
|
||||
<VCol cols="12">
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>订阅站点</VCardTitle>
|
||||
<VCardSubtitle>只有选中的站点才会在订阅中使用。</VCardSubtitle>
|
||||
<VCardTitle>订阅模式 & 规则</VCardTitle>
|
||||
<VCardSubtitle>设定订阅模式、周期等基础设置</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VChipGroup v-model="selectedRssSites" column multiple>
|
||||
<VChip
|
||||
v-for="site in allSites"
|
||||
:key="site.id"
|
||||
:color="selectedRssSites.includes(site.id) ? 'primary' : ''"
|
||||
filter
|
||||
variant="outlined"
|
||||
:value="site.id"
|
||||
>
|
||||
{{ site.name }}
|
||||
</VChip>
|
||||
</VChipGroup>
|
||||
</VCardText>
|
||||
<VCardText>
|
||||
<VForm>
|
||||
<VRow>
|
||||
@@ -351,6 +143,17 @@ onMounted(() => {
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="12">
|
||||
<VSelect
|
||||
v-model="selectedFilterRuleGroup"
|
||||
:items="[]"
|
||||
chips
|
||||
multiple
|
||||
label="优先级规则组"
|
||||
hint="按选定的过滤规则组对订阅进行过滤"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -372,190 +175,27 @@ onMounted(() => {
|
||||
<VCol cols="12">
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<template #append>
|
||||
<IconBtn>
|
||||
<VIcon icon="mdi-dots-vertical" />
|
||||
<VMenu activator="parent" close-on-content-click>
|
||||
<VList>
|
||||
<VListItem variant="plain" @click="shareRules('SubscribeFilterRules')">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-share" />
|
||||
</template>
|
||||
<VListItemTitle>分享</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem variant="plain" @click="importRules('SubscribeFilterRules')">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-import" />
|
||||
</template>
|
||||
<VListItemTitle>导入</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</template>
|
||||
<VCardTitle>订阅优先级</VCardTitle>
|
||||
<VCardSubtitle> 设置在正常订阅时默认使用的优先级,未在优先级中的资源将不会自动下载。</VCardSubtitle>
|
||||
<VCardTitle>订阅站点</VCardTitle>
|
||||
<VCardSubtitle>只有选中的站点才会在订阅中使用。</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<draggable
|
||||
v-model="subscribeFilterCards"
|
||||
handle=".cursor-move"
|
||||
item-key="pri"
|
||||
tag="div"
|
||||
@end="dragOrderEnd('SubscribeFilterRules')"
|
||||
:component-data="{ 'class': 'grid gap-3 grid-filterrule-card' }"
|
||||
>
|
||||
<template #item="{ element }">
|
||||
<FilterRuleCard
|
||||
:pri="element.pri"
|
||||
:maxpri="subscribeFilterCards.length.toString()"
|
||||
:rules="element.rules"
|
||||
@changed="updateSubscribeFilterCardValue"
|
||||
@close="filterCardClose('SubscribeFilterRules', element.pri)"
|
||||
/>
|
||||
</template>
|
||||
</draggable>
|
||||
<VChipGroup v-model="selectedRssSites" column multiple>
|
||||
<VChip
|
||||
v-for="site in allSites"
|
||||
:key="site.id"
|
||||
:color="selectedRssSites.includes(site.id) ? 'primary' : ''"
|
||||
filter
|
||||
variant="outlined"
|
||||
:value="site.id"
|
||||
>
|
||||
{{ site.name }}
|
||||
</VChip>
|
||||
</VChipGroup>
|
||||
</VCardText>
|
||||
<VCardText>
|
||||
<VBtn type="submit" class="me-2" @click="saveCustomFilters('SubscribeFilterRules')"> 保存 </VBtn>
|
||||
<VBtn color="success" variant="tonal" @click="addFilterCard('SubscribeFilterRules')">
|
||||
<VIcon icon="mdi-plus" />
|
||||
</VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>洗版优先级</VCardTitle>
|
||||
<template #append>
|
||||
<IconBtn>
|
||||
<VIcon icon="mdi-dots-vertical" />
|
||||
<VMenu activator="parent" close-on-content-click>
|
||||
<VList>
|
||||
<VListItem variant="plain" @click="shareRules('BestVersionFilterRules')">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-share" />
|
||||
</template>
|
||||
<VListItemTitle>分享</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem variant="plain" @click="importRules('BestVersionFilterRules')">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-import" />
|
||||
</template>
|
||||
<VListItemTitle>导入</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</template>
|
||||
<VCardSubtitle> 设置在订阅洗版时使用的优先级,匹配优先级1时洗版完成。</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<draggable
|
||||
v-model="bestVersionFilterCards"
|
||||
handle=".cursor-move"
|
||||
item-key="pri"
|
||||
tag="div"
|
||||
@end="dragOrderEnd('BestVersionFilterRules')"
|
||||
:component-data="{ 'class': 'grid gap-3 grid-filterrule-card' }"
|
||||
>
|
||||
<template #item="{ element }">
|
||||
<FilterRuleCard
|
||||
:pri="element.pri"
|
||||
:maxpri="bestVersionFilterCards.length.toString()"
|
||||
:rules="element.rules"
|
||||
@changed="updateBestVersionFilterCardValue"
|
||||
@close="filterCardClose('BestVersionFilterRules', element.pri)"
|
||||
/>
|
||||
</template>
|
||||
</draggable>
|
||||
</VCardText>
|
||||
<VCardText>
|
||||
<VBtn type="submit" class="me-2" @click="saveCustomFilters('BestVersionFilterRules')"> 保存 </VBtn>
|
||||
<VBtn color="success" variant="tonal" @click="addFilterCard('BestVersionFilterRules')">
|
||||
<VIcon icon="mdi-plus" />
|
||||
</VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>默认过滤规则</VCardTitle>
|
||||
<VCardSubtitle> 设置在订阅时默认使用的过滤规则。</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VForm>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="defaultFilterRules.include"
|
||||
type="text"
|
||||
label="包含(关键字、正则式)"
|
||||
hint="包含规则,支持正式表达式,多个关键字用 | 分隔表示或"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="defaultFilterRules.exclude"
|
||||
type="text"
|
||||
label="排除(关键字、正则式)"
|
||||
hint="排除规则,支持正式表达式,多个关键字用 | 分隔表示或"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="defaultFilterRules.movie_size"
|
||||
type="text"
|
||||
label="电影文件大小(GB)"
|
||||
placeholder="0-30"
|
||||
hint="文件大小范围,格式:0-30,表示0-30GB之间的资源"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="defaultFilterRules.tv_size"
|
||||
type="text"
|
||||
label="剧集单集文件大小(GB)"
|
||||
placeholder="0-10"
|
||||
hint="单集文件大小范围,格式:0-10,表示0-10GB之间的资源"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="defaultFilterRules.min_seeders"
|
||||
type="text"
|
||||
label="最小做种数"
|
||||
placeholder="0"
|
||||
hint="小于该值的资源将被过滤掉,0表示不过滤"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="defaultFilterRules.min_seeders_time"
|
||||
type="text"
|
||||
label="最少做种数生效发布时间(分钟)"
|
||||
placeholder="0"
|
||||
hint="发布时间距当前时间大于该值的资源将生效最小做种数规则,0表示不生效"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
<VCardText>
|
||||
<VBtn type="submit" @click="saveDefaultFilter"> 保存 </VBtn>
|
||||
<VBtn type="submit" @click="saveSelectedRssSites"> 保存 </VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VDialog v-model="importCodeDialog" width="60rem" scrollable>
|
||||
<ImportCodeDialog v-model="importCodeString" title="导入优先级规则" @close="importCodeDialog = false" />
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
@@ -2,216 +2,37 @@
|
||||
<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'
|
||||
|
||||
// 选中的媒体服务器
|
||||
const selectedMediaServers = ref([])
|
||||
|
||||
// 选中的下载器
|
||||
const selectedDownloaders = ref([])
|
||||
|
||||
// 下载器选中标签页
|
||||
const downloaderTab = ref('qbittorrent')
|
||||
|
||||
// 媒体服务器选中标签页
|
||||
const mediaserverTab = ref('emby')
|
||||
import { DownloaderConf, MediaServerConf } from '@/api/types'
|
||||
import DownloaderCard from '@/components/cards/DownloaderCard.vue'
|
||||
import MediaServerCard from '@/components/cards/MediaServerCard.vue'
|
||||
|
||||
// 系统设置项
|
||||
const SystemSettings = ref({
|
||||
APP_DOMAIN: '',
|
||||
})
|
||||
|
||||
// 下载器设置项
|
||||
const downloaderSettings = ref({
|
||||
DOWNLOADER_MONITOR: true,
|
||||
TORRENT_TAG: '',
|
||||
QB_HOST: '',
|
||||
QB_USER: '',
|
||||
QB_PASSWORD: '',
|
||||
QB_CATEGORY: false,
|
||||
QB_SEQUENTIAL: false,
|
||||
QB_FORCE_RESUME: false,
|
||||
TR_HOST: '',
|
||||
TR_USER: '',
|
||||
TR_PASSWORD: '',
|
||||
})
|
||||
// 选中的媒体服务器
|
||||
const mediaServers = ref<MediaServerConf[]>([])
|
||||
|
||||
// 媒体服务器设置项
|
||||
const mediaServerSettings = ref({
|
||||
MEDIASERVER_SYNC_INTERVAL: 6,
|
||||
MEDIASERVER_SYNC_BLACKLIST: '',
|
||||
EMBY_HOST: '',
|
||||
EMBY_PLAY_HOST: '',
|
||||
EMBY_API_KEY: '',
|
||||
JELLYFIN_HOST: '',
|
||||
JELLYFIN_PLAY_HOST: '',
|
||||
JELLYFIN_API_KEY: '',
|
||||
PLEX_HOST: '',
|
||||
PLEX_PLAY_HOST: '',
|
||||
PLEX_TOKEN: '',
|
||||
})
|
||||
|
||||
// 下载器字典项
|
||||
const Downloaders = [
|
||||
{
|
||||
title: 'Qbittorrent',
|
||||
value: 'qbittorrent',
|
||||
},
|
||||
{
|
||||
title: 'Transmission',
|
||||
value: 'transmission',
|
||||
},
|
||||
]
|
||||
|
||||
// 媒体服务器字典项
|
||||
const MediaServers = [
|
||||
{
|
||||
title: 'Emby',
|
||||
value: 'emby',
|
||||
},
|
||||
{
|
||||
title: 'Jellyfin',
|
||||
value: 'jellyfin',
|
||||
},
|
||||
{
|
||||
title: 'Plex',
|
||||
value: 'plex',
|
||||
},
|
||||
]
|
||||
|
||||
// 媒体库同步周期字典
|
||||
const syncIntervalItems = [
|
||||
{ title: '从不', value: 0 },
|
||||
{ title: '每小时', value: 1 },
|
||||
{ title: '每6小时', value: 6 },
|
||||
{ title: '每12小时', value: 12 },
|
||||
{ title: '每天', value: 24 },
|
||||
{ title: '每周', value: 168 },
|
||||
]
|
||||
// 下载器
|
||||
const downloaders = ref<DownloaderConf[]>([])
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
// 调用API查询下载器设置
|
||||
async function loadDownloaderSetting() {
|
||||
try {
|
||||
const result1: { [key: string]: any } = await api.get('system/setting/DOWNLOADER')
|
||||
if (result1.success) selectedDownloaders.value = result1.data?.value?.split(',')
|
||||
|
||||
const result2: { [key: string]: any } = await api.get('system/env')
|
||||
if (result2.success) {
|
||||
const {
|
||||
DOWNLOADER_MONITOR,
|
||||
TORRENT_TAG,
|
||||
QB_HOST,
|
||||
QB_USER,
|
||||
QB_PASSWORD,
|
||||
QB_CATEGORY,
|
||||
QB_SEQUENTIAL,
|
||||
QB_FORCE_RESUME,
|
||||
TR_HOST,
|
||||
TR_USER,
|
||||
TR_PASSWORD,
|
||||
} = result2.data
|
||||
downloaderSettings.value = {
|
||||
DOWNLOADER_MONITOR,
|
||||
TORRENT_TAG,
|
||||
QB_HOST,
|
||||
QB_USER,
|
||||
QB_PASSWORD,
|
||||
QB_CATEGORY,
|
||||
QB_SEQUENTIAL,
|
||||
QB_FORCE_RESUME,
|
||||
TR_HOST,
|
||||
TR_USER,
|
||||
TR_PASSWORD,
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
async function loadDownloaderSetting() {}
|
||||
|
||||
// 调用API保存下载器设置
|
||||
async function saveDownloaderSetting() {
|
||||
try {
|
||||
const result1: { [key: string]: any } = await api.post(
|
||||
'system/setting/DOWNLOADER',
|
||||
selectedDownloaders.value.join(','),
|
||||
)
|
||||
const result2: { [key: string]: any } = await api.post('system/env', downloaderSettings.value)
|
||||
|
||||
if (result1.success && result2.success) {
|
||||
$toast.success('保存下载器设置成功')
|
||||
reloadModule()
|
||||
} else {
|
||||
$toast.error('保存下载器设置失败!')
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
async function saveDownloaderSetting() {}
|
||||
|
||||
// 调用API查询媒体服务器设置
|
||||
async function loadMediaServerSetting() {
|
||||
try {
|
||||
const result1: { [key: string]: any } = await api.get('system/setting/MEDIASERVER')
|
||||
if (result1.success) selectedMediaServers.value = result1.data?.value?.split(',')
|
||||
|
||||
const result2: { [key: string]: any } = await api.get('system/env')
|
||||
if (result2.success) {
|
||||
const {
|
||||
MEDIASERVER_SYNC_INTERVAL,
|
||||
MEDIASERVER_SYNC_BLACKLIST,
|
||||
EMBY_HOST,
|
||||
EMBY_PLAY_HOST,
|
||||
EMBY_API_KEY,
|
||||
JELLYFIN_HOST,
|
||||
JELLYFIN_PLAY_HOST,
|
||||
JELLYFIN_API_KEY,
|
||||
PLEX_HOST,
|
||||
PLEX_PLAY_HOST,
|
||||
PLEX_TOKEN,
|
||||
} = result2.data
|
||||
mediaServerSettings.value = {
|
||||
MEDIASERVER_SYNC_INTERVAL,
|
||||
MEDIASERVER_SYNC_BLACKLIST,
|
||||
EMBY_HOST,
|
||||
EMBY_PLAY_HOST,
|
||||
EMBY_API_KEY,
|
||||
JELLYFIN_HOST,
|
||||
JELLYFIN_PLAY_HOST,
|
||||
JELLYFIN_API_KEY,
|
||||
PLEX_HOST,
|
||||
PLEX_PLAY_HOST,
|
||||
PLEX_TOKEN,
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
async function loadMediaServerSetting() {}
|
||||
|
||||
// 调用API保存媒体服务器设置
|
||||
async function saveMediaServerSetting() {
|
||||
try {
|
||||
const result1: { [key: string]: any } = await api.post(
|
||||
'system/setting/MEDIASERVER',
|
||||
selectedMediaServers.value.join(','),
|
||||
)
|
||||
|
||||
const result2: { [key: string]: any } = await api.post('system/env', mediaServerSettings.value)
|
||||
|
||||
if (result1.success && result2.success) {
|
||||
$toast.success('保存媒体服务器设置成功')
|
||||
reloadModule()
|
||||
} else {
|
||||
$toast.error('保存媒体服务器设置失败!')
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
async function saveMediaServerSetting() {}
|
||||
|
||||
// 加载系统设置
|
||||
async function loadSystemSettings() {
|
||||
@@ -240,16 +61,11 @@ async function saveSystemSetting() {
|
||||
}
|
||||
}
|
||||
|
||||
// 调用API接口重新加载模块
|
||||
async function reloadModule() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/reload')
|
||||
if (result.success) $toast.success('重新加载模块成功')
|
||||
else $toast.error('重新加载模块失败!')
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
// 添加下载器
|
||||
function addDownloader() {}
|
||||
|
||||
// 添加媒体服务器
|
||||
function addMediaServer() {}
|
||||
|
||||
// 加载数据
|
||||
onMounted(() => {
|
||||
@@ -265,7 +81,7 @@ onMounted(() => {
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>系统</VCardTitle>
|
||||
<VCardSubtitle>设置服务使用的域名等信息。</VCardSubtitle>
|
||||
<VCardSubtitle>设置服务使用的域名等基础信息。</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VForm>
|
||||
@@ -294,147 +110,28 @@ onMounted(() => {
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>下载器</VCardTitle>
|
||||
<VCardSubtitle>只有选中的第1个下载器才会被默认使用。</VCardSubtitle>
|
||||
<VCardSubtitle>只有第1个启用的下载器才会被默认使用。</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VForm>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VSelect
|
||||
v-model="selectedDownloaders"
|
||||
multiple
|
||||
chips
|
||||
:items="Downloaders"
|
||||
label="当前使用下载器"
|
||||
hint="启用下载器,只有第1个会被默认下载使用"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="downloaderSettings.TORRENT_TAG"
|
||||
label="下载器种子标签"
|
||||
hint="MoviePilot添加的下载任务标签"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="downloaderSettings.DOWNLOADER_MONITOR"
|
||||
label="下载文件自动整理"
|
||||
hint="任务下载完成时自动整理文件到媒体库"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol>
|
||||
<VTabs v-model="downloaderTab" stacked>
|
||||
<VTab value="qbittorrent"> Qbittorrent </VTab>
|
||||
<VTab value="transmission"> Transmission </VTab>
|
||||
</VTabs>
|
||||
<VWindow v-model="downloaderTab" class="mt-5 disable-tab-transition" :touch="false">
|
||||
<VWindowItem value="qbittorrent">
|
||||
<VForm>
|
||||
<VRow>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="downloaderSettings.QB_HOST"
|
||||
label="地址"
|
||||
placeholder="http(s)://ip:port"
|
||||
hint="服务端地址,格式:http(s)://ip:port"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="downloaderSettings.QB_USER"
|
||||
label="用户名"
|
||||
placeholder="admin"
|
||||
hint="登录使用的用户名"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="downloaderSettings.QB_PASSWORD"
|
||||
type="password"
|
||||
label="密码"
|
||||
hint="登录使用的密码"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VSwitch
|
||||
v-model="downloaderSettings.QB_CATEGORY"
|
||||
label="自动分类管理"
|
||||
hint="由下载器自动管理分类和下载目录"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VSwitch
|
||||
v-model="downloaderSettings.QB_SEQUENTIAL"
|
||||
label="顺序下载"
|
||||
hint="按顺序依次下载文件"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VSwitch
|
||||
v-model="downloaderSettings.QB_FORCE_RESUME"
|
||||
label="强制继续"
|
||||
hint="强制继续、强制上传模式"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VWindowItem>
|
||||
<VWindowItem value="transmission">
|
||||
<VForm>
|
||||
<VRow>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="downloaderSettings.TR_HOST"
|
||||
label="地址"
|
||||
placeholder="http(s)://ip:port"
|
||||
hint="服务端地址,格式:http(s)://ip:port"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="downloaderSettings.TR_USER"
|
||||
label="用户名"
|
||||
placeholder="admin"
|
||||
hint="登录使用的用户名"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="downloaderSettings.TR_PASSWORD"
|
||||
type="password"
|
||||
label="密码"
|
||||
hint="登录使用的密码"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VWindowItem>
|
||||
</VWindow>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
<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" />
|
||||
</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" @click="addDownloader">
|
||||
<VIcon icon="mdi-plus" />
|
||||
</VBtn>
|
||||
</div>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
@@ -446,154 +143,28 @@ onMounted(() => {
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>媒体服务器</VCardTitle>
|
||||
<VCardSubtitle>只有选中的媒体服务器才会被默认使用。</VCardSubtitle>
|
||||
<VCardSubtitle>所有启用的媒体服务器都会被使用。</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VForm>
|
||||
<VRow>
|
||||
<VCol cols="12" md="4">
|
||||
<VSelect
|
||||
v-model="selectedMediaServers"
|
||||
multiple
|
||||
chips
|
||||
:items="MediaServers"
|
||||
label="当前使用媒体服务器"
|
||||
hint="启用媒体服务器,入库展示、下载控重等将使用"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VSelect
|
||||
v-model="mediaServerSettings.MEDIASERVER_SYNC_INTERVAL"
|
||||
:items="syncIntervalItems"
|
||||
label="同步周期"
|
||||
hint="同步媒体库数据到MoviePilot的时间间隔"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="mediaServerSettings.MEDIASERVER_SYNC_BLACKLIST"
|
||||
label="媒体库同步黑名单"
|
||||
placeholder="使用,分隔"
|
||||
hint="不同步数据的媒体库名称,多个使用,分隔"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol>
|
||||
<VTabs v-model="mediaserverTab" stacked>
|
||||
<VTab value="emby"> Emby </VTab>
|
||||
<VTab value="jellyfin"> Jellyfin </VTab>
|
||||
<VTab value="plex"> Plex </VTab>
|
||||
</VTabs>
|
||||
<VWindow v-model="mediaserverTab" class="mt-5 disable-tab-transition" :touch="false">
|
||||
<VWindowItem value="emby">
|
||||
<VForm>
|
||||
<VRow>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="mediaServerSettings.EMBY_HOST"
|
||||
label="地址"
|
||||
placeholder="http(s)://ip:port"
|
||||
hint="服务端地址,格式:http(s)://ip:port"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="mediaServerSettings.EMBY_PLAY_HOST"
|
||||
label="外网播放地址"
|
||||
placeholder="http(s)://domain:port"
|
||||
hint="跳转播放页面使用的地址,格式:http(s)://domain:port"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="mediaServerSettings.EMBY_API_KEY"
|
||||
label="API密钥"
|
||||
hint="Emby设置->高级->API密钥中生成的密钥"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VWindowItem>
|
||||
<VWindowItem value="jellyfin">
|
||||
<VForm>
|
||||
<VRow>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="mediaServerSettings.JELLYFIN_HOST"
|
||||
label="地址"
|
||||
placeholder="http(s)://ip:port"
|
||||
hint="服务端地址,格式:http(s)://ip:port"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="mediaServerSettings.JELLYFIN_PLAY_HOST"
|
||||
label="外网播放地址"
|
||||
placeholder="http(s)://domain:port"
|
||||
hint="跳转播放页面使用的地址,格式:http(s)://domain:port"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="mediaServerSettings.JELLYFIN_API_KEY"
|
||||
label="API密钥"
|
||||
hint="Jellyfin设置->高级->API密钥中生成的密钥"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VWindowItem>
|
||||
<VWindowItem value="plex">
|
||||
<VForm>
|
||||
<VRow>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="mediaServerSettings.PLEX_HOST"
|
||||
label="地址"
|
||||
placeholder="http(s)://ip:port"
|
||||
hint="服务端地址,格式:http(s)://ip:port"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="mediaServerSettings.PLEX_PLAY_HOST"
|
||||
label="外网播放地址"
|
||||
placeholder="http(s)://domain:port"
|
||||
hint="跳转播放页面使用的地址,格式:http(s)://domain:port"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="mediaServerSettings.PLEX_TOKEN"
|
||||
label="X-Plex-Token"
|
||||
hint="浏览器F12->网络,从Plex请求URL中获取的X-Plex-Token"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VWindowItem>
|
||||
</VWindow>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
<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 :downloader="element" />
|
||||
</template>
|
||||
</draggable>
|
||||
</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" @click="addMediaServer">
|
||||
<VIcon icon="mdi-plus" />
|
||||
</VBtn>
|
||||
</div>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
|
||||
@@ -6,7 +6,6 @@ import SiteCard from '@/components/cards/SiteCard.vue'
|
||||
import NoDataFound from '@/components/NoDataFound.vue'
|
||||
import SiteAddEditDialog from '@/components/dialog/SiteAddEditDialog.vue'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import { isLength } from 'lodash'
|
||||
|
||||
// 显示器宽度
|
||||
const display = useDisplay()
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
<script lang="ts" setup>
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import { VForm } from 'vuetify/lib/components/index.mjs'
|
||||
import { requiredValidator } from '@/@validators'
|
||||
import api from '@/api'
|
||||
import type { User } from '@/api/types'
|
||||
import avatar1 from '@images/avatars/avatar-1.png'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import NoDataFound from '@/components/NoDataFound.vue'
|
||||
import UserCard from '@/components/cards/UserCard.vue'
|
||||
import UserAddEditDialog from '@/components/dialog/UserAddEditDialog.vue'
|
||||
|
||||
// APP
|
||||
const display = useDisplay()
|
||||
@@ -13,108 +12,26 @@ const appMode = computed(() => {
|
||||
return localStorage.getItem('MP_APPMODE') != '0' && display.mdAndDown.value
|
||||
})
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
// 是否刷新过
|
||||
const isRefreshed = ref(false)
|
||||
|
||||
// 是否加载中
|
||||
const loading = ref(false)
|
||||
|
||||
// 新增用户窗口
|
||||
const addUserDialog = ref(false)
|
||||
|
||||
const isPasswordVisible = ref(false)
|
||||
|
||||
// 新增用户表单
|
||||
const userForm = reactive({
|
||||
name: '',
|
||||
password: '',
|
||||
email: '',
|
||||
})
|
||||
|
||||
// 当前用户信息
|
||||
const accountInfo = ref<User>({
|
||||
id: 0,
|
||||
name: '',
|
||||
password: '',
|
||||
email: '',
|
||||
is_active: false,
|
||||
is_superuser: false,
|
||||
avatar: '',
|
||||
is_otp: false,
|
||||
permissions: {},
|
||||
settings: {},
|
||||
})
|
||||
|
||||
// 所有用户信息
|
||||
const allUsers = ref<User[]>([])
|
||||
|
||||
// 调用API,加载当前用户数据
|
||||
async function loadAccountInfo() {
|
||||
try {
|
||||
const user: User = await api.get('user/current')
|
||||
console.log(user)
|
||||
accountInfo.value = user
|
||||
if (!accountInfo.value.avatar) accountInfo.value.avatar = avatar1
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 调用API,查询所有用户
|
||||
async function loadAllUsers() {
|
||||
try {
|
||||
loading.value = true
|
||||
const result: User[] = await api.get('/user/')
|
||||
|
||||
allUsers.value = result
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 删除用户
|
||||
async function deleteUser(user: User) {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.delete(`user/${user.name}`)
|
||||
if (result.success) {
|
||||
$toast.success('用户删除成功!')
|
||||
loadAllUsers()
|
||||
} else {
|
||||
$toast.error(`用户删除失败:${result.message}!`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 冻结用户
|
||||
async function deactivateUser(user: User) {
|
||||
try {
|
||||
user.is_active = !user.is_active
|
||||
|
||||
const result: { [key: string]: any } = await api.put('user/', user)
|
||||
if (result.success) {
|
||||
$toast.success('用户冻结成功!')
|
||||
loadAllUsers()
|
||||
} else {
|
||||
$toast.error(`用户冻结失败:${result.message}!`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 新增用户
|
||||
async function addUser() {
|
||||
if (!userForm.name || !userForm.password || !userForm.email) {
|
||||
$toast.error('请填写完整信息!')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post('user/', userForm)
|
||||
if (result.success) {
|
||||
$toast.success('用户新增成功!')
|
||||
loadAllUsers()
|
||||
addUserDialog.value = false
|
||||
} else {
|
||||
$toast.error(`用户新增失败:${result.message}!`)
|
||||
}
|
||||
loading.value = false
|
||||
isRefreshed.value = true
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
@@ -122,107 +39,30 @@ async function addUser() {
|
||||
|
||||
// 加载当前用户数据
|
||||
onMounted(() => {
|
||||
loadAccountInfo()
|
||||
loadAllUsers()
|
||||
})
|
||||
|
||||
onActivated(() => {
|
||||
if (!loading.value) {
|
||||
loadAllUsers()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<VRow>
|
||||
<VCol v-if="accountInfo.is_superuser" cols="12">
|
||||
<!-- 👉 Accounts -->
|
||||
<VCard title="所有用户">
|
||||
<template #append>
|
||||
<IconBtn @click.stop="addUserDialog = true">
|
||||
<VIcon icon="mdi-plus" />
|
||||
</IconBtn>
|
||||
</template>
|
||||
<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" class="w-5" />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="user in allUsers" :key="user.name">
|
||||
<td>
|
||||
{{ user.name }}
|
||||
</td>
|
||||
<td>{{ user.email }}</td>
|
||||
<td>
|
||||
<VChip v-if="user.is_active" color="success" text-color="white"> 激活 </VChip>
|
||||
<VChip v-else color="error" text-color="white"> 冻结 </VChip>
|
||||
</td>
|
||||
<td>{{ user.is_superuser ? '是' : '否' }}</td>
|
||||
<td>
|
||||
<IconBtn v-show="accountInfo.is_superuser && accountInfo.name !== user.name">
|
||||
<VIcon icon="mdi-dots-vertical" />
|
||||
<VMenu activator="parent" close-on-content-click>
|
||||
<VList>
|
||||
<VListItem variant="plain" @click="deactivateUser(user)">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-lock" />
|
||||
</template>
|
||||
<VListItemTitle>
|
||||
{{ user.is_active ? '冻结' : '解冻' }}
|
||||
</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem variant="plain" base-color="error" @click="deleteUser(user)">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-delete" />
|
||||
</template>
|
||||
<VListItemTitle>删除</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</VTable>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<!-- =弹窗 -->
|
||||
<VDialog v-model="addUserDialog" max-width="50rem" persistent z-index="1010">
|
||||
<!-- Dialog Content -->
|
||||
<VCard title="新增用户">
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField v-model="userForm.name" label="用户名" :rules="[requiredValidator]" />
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="userForm.password"
|
||||
label="密码"
|
||||
:rules="[requiredValidator]"
|
||||
:type="isPasswordVisible ? 'text' : 'password'"
|
||||
:append-inner-icon="isPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
|
||||
@click:append-inner="isPasswordVisible = !isPasswordVisible"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField v-model="userForm.email" :rules="[requiredValidator]" label="邮箱" />
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VBtn @click="addUserDialog = false"> 取消 </VBtn>
|
||||
<VSpacer />
|
||||
<VBtn @click="addUser"> 确定 </VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
<LoadingBanner v-if="!isRefreshed" class="mt-12" />
|
||||
|
||||
<div v-if="allUsers.length > 0" class="grid gap-3 grid-user-card items-start">
|
||||
<UserCard v-for="user in allUsers" :user="user" />
|
||||
</div>
|
||||
|
||||
<NoDataFound
|
||||
v-if="allUsers.length === 0 && isRefreshed"
|
||||
error-code="404"
|
||||
error-title="没有用户"
|
||||
error-description="点击右下角按钮添加用户"
|
||||
/>
|
||||
|
||||
<VFab
|
||||
icon="mdi-plus"
|
||||
location="bottom"
|
||||
@@ -233,4 +73,7 @@ onMounted(() => {
|
||||
@click="addUserDialog = true"
|
||||
:class="{ 'mb-12': appMode }"
|
||||
/>
|
||||
|
||||
<!-- 弹窗 -->
|
||||
<UserAddEditDialog v-model="addUserDialog" max-width="50rem" persistent z-index="1010" />
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user