Compare commits

...

34 Commits

Author SHA1 Message Date
jxxghp
a738d4a3b9 feat:系统设置面板 2024-02-15 15:04:59 +08:00
jxxghp
e9866a04df v1.6.4 2024-02-14 21:07:09 +08:00
jxxghp
4f5193d602 feat:更新Cookie支持两步验证 2024-02-14 21:06:42 +08:00
jxxghp
37b92c55ba feat:文件管理手动刮削 2024-02-10 19:32:49 +08:00
jxxghp
9299f1bcb6 release 2024-02-10 09:48:45 +08:00
jxxghp
7fe12192df add apexcharts 2024-02-10 09:36:54 +08:00
jxxghp
1169644ab3 fix render 2024-02-09 08:43:36 +08:00
jxxghp
6f7770ed43 fix 2024-02-08 20:58:41 +08:00
jxxghp
8059fd6f90 fix 2024-02-08 20:57:09 +08:00
jxxghp
556dbd8d78 fix bug 2024-02-08 20:07:23 +08:00
jxxghp
6695fd8c14 add ace-editor 2024-02-08 19:56:28 +08:00
jxxghp
3ab0229275 fix ui 2024-02-08 13:43:37 +08:00
jxxghp
99467127a0 更新 package.json 2024-02-08 13:28:11 +08:00
jxxghp
90d73b7bd5 Merge pull request #73 from cikezhu/main 2024-02-08 13:27:40 +08:00
叮叮当
2e326e1798 fix 全部日志url反向代理时被serviceWorker拦截 2024-02-08 12:45:52 +08:00
jxxghp
251eac93c7 更新 package.json 2024-02-08 07:11:49 +08:00
jxxghp
c74d70808c Merge pull request #72 from cikezhu/main 2024-02-08 07:10:48 +08:00
叮叮当
e63b2d7152 新窗口打开全部日志 2024-02-08 00:06:52 +08:00
jxxghp
16b29b56a5 更新 package.json 2024-01-29 23:11:57 +08:00
jxxghp
6d79c4fe2f Merge pull request #71 from cikezhu/main 2024-01-29 23:11:34 +08:00
叮叮当
4b1fb60ee3 fix 媒体信息页面person跳转问题 2024-01-29 23:01:22 +08:00
jxxghp
1d2be54f9e 更新 package.json 2024-01-29 11:05:38 +08:00
jxxghp
83547e32db Merge pull request #70 from cikezhu/main 2024-01-29 11:05:10 +08:00
叮叮当
70ddb929f2 fix plugin_icon 2024-01-28 00:47:37 +08:00
叮叮当
8b22961394 支持基于路径的反向代理 2024-01-27 14:47:00 +08:00
jxxghp
c15d42c179 Merge pull request #69 from falling/main 2024-01-24 18:43:37 +08:00
falling
098e473cab 从qbt后台的返回值来更新下载状态 2024-01-21 19:48:47 +08:00
jxxghp
f6f3d9368a fix bug 2024-01-08 13:22:42 +08:00
jxxghp
9558a420e9 fix image proxy 2024-01-08 12:25:06 +08:00
jxxghp
4d3b69ca34 更新 package.json 2024-01-08 11:33:32 +08:00
jxxghp
fdcc4a44c8 Merge pull request #68 from jjjokin/feat-auto-switch-theme 2024-01-08 11:01:26 +08:00
jokin
5de0494538 增加主题自适应选项 2024-01-07 22:17:11 +08:00
jxxghp
2045f833e4 fix bug 2024-01-06 11:07:23 +08:00
jxxghp
cc4f89aac1 fix 2024-01-06 11:02:03 +08:00
27 changed files with 730 additions and 205 deletions

View File

@@ -1 +1 @@
VITE_API_BASE_URL=/api/v1/
VITE_API_BASE_URL=api/v1/

View File

@@ -1,6 +1,6 @@
{
"name": "moviepilot",
"version": "1.5.7-3",
"version": "1.6.4-1",
"private": true,
"bin": "dist/service.js",
"scripts": {
@@ -24,6 +24,7 @@
"@floating-ui/dom": "1.2.8",
"@vueuse/core": "^10.1.2",
"@vueuse/math": "^10.1.2",
"ace-builds": "^1.32.6",
"apexcharts-clevision": "^3.28.5",
"axios": "1.4.0",
"axios-mock-adapter": "^1.21.4",
@@ -48,6 +49,7 @@
"vue-prism-component": "^2.0.0",
"vue-router": "^4.2.0",
"vue-toast-notification": "^3",
"vue3-ace-editor": "^2.2.4",
"vue3-apexcharts": "^1.4.1",
"vue3-perfect-scrollbar": "^1.6.0",
"vuetify": "3.3.5",

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, watch } from 'vue'
import { ref } from 'vue'
import { useTheme } from 'vuetify'
import type { ThemeSwitcherTheme } from '@layouts/types'
@@ -20,26 +20,30 @@ const {
{ initialValue: savedTheme.value },
)
function changeTheme() {
const nextTheme = getNextThemeName()
globalTheme.name.value = nextTheme
savedTheme.value = nextTheme
localStorage.setItem('theme', nextTheme)
function updateTheme() {
const autoTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
const theme = currentThemeName.value === 'auto' ? autoTheme : currentThemeName.value
globalTheme.name.value = theme
savedTheme.value = theme
// 修改载入时背景色
localStorage.setItem('materio-initial-loader-bg', globalTheme.current.value.colors.background)
themeTransition()
}
// Update icon if theme is changed from other sources
// 监听系统主题变化
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', updateTheme)
watch(
() => globalTheme.name.value,
(val) => {
currentThemeName.value = val
},
() => currentThemeName.value,
() => updateTheme(),
)
function changeTheme() {
const nextTheme = getNextThemeName()
currentThemeName.value = nextTheme
localStorage.setItem('theme', nextTheme)
}
// Apply saved theme on page load
// onMounted(() => {
// globalTheme.name.value = savedTheme.value

View File

@@ -3,9 +3,15 @@ import { useToast } from 'vue-toast-notification'
import { useTheme } from 'vuetify'
import store from './store'
function setTheme() {
const { global: globalTheme } = useTheme()
let theme = localStorage.getItem('theme') || 'light'
if (theme === 'auto')
theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
globalTheme.name.value = theme
}
// 第一时间应用主题
const { global: globalTheme } = useTheme()
globalTheme.name.value = localStorage.getItem('theme') || 'light'
setTheme()
// 提示框
const $toast = useToast()

54
src/ace-config.ts Normal file
View File

@@ -0,0 +1,54 @@
import ace from 'ace-builds'
import modeJsonUrl from 'ace-builds/src-noconflict/mode-json?url'
import modeJavascriptUrl from 'ace-builds/src-noconflict/mode-javascript?url'
import modeHtmlUrl from 'ace-builds/src-noconflict/mode-html?url'
import modeYamlUrl from 'ace-builds/src-noconflict/mode-yaml?url'
import themeGithubUrl from 'ace-builds/src-noconflict/theme-github?url'
import themeChromeUrl from 'ace-builds/src-noconflict/theme-chrome?url'
import themeMonokaiUrl from 'ace-builds/src-noconflict/theme-monokai?url'
import workerBaseUrl from 'ace-builds/src-noconflict/worker-base?url'
import workerJsonUrl from 'ace-builds/src-noconflict/worker-json?url'
import workerJavascriptUrl from 'ace-builds/src-noconflict/worker-javascript?url'
import workerHtmlUrl from 'ace-builds/src-noconflict/worker-html?url'
import workerYamlUrl from 'ace-builds/src-noconflict/worker-yaml?url'
import snippetsHtmlUrl from 'ace-builds/src-noconflict/snippets/html?url'
import snippetsJsUrl from 'ace-builds/src-noconflict/snippets/javascript?url'
import snippetsYamlUrl from 'ace-builds/src-noconflict/snippets/yaml?url'
import snippetsJsonUrl from 'ace-builds/src-noconflict/snippets/json?url'
import 'ace-builds/src-noconflict/ext-language_tools'
ace.config.setModuleUrl('ace/mode/json', modeJsonUrl)
ace.config.setModuleUrl('ace/mode/javascript', modeJavascriptUrl)
ace.config.setModuleUrl('ace/mode/html', modeHtmlUrl)
ace.config.setModuleUrl('ace/mode/yaml', modeYamlUrl)
ace.config.setModuleUrl('ace/theme/github', themeGithubUrl)
ace.config.setModuleUrl('ace/theme/chrome', themeChromeUrl)
ace.config.setModuleUrl('ace/theme/monokai', themeMonokaiUrl)
ace.config.setModuleUrl('ace/mode/base', workerBaseUrl)
ace.config.setModuleUrl('ace/mode/json_worker', workerJsonUrl)
ace.config.setModuleUrl('ace/mode/javascript_worker', workerJavascriptUrl)
ace.config.setModuleUrl('ace/mode/html_worker', workerHtmlUrl)
ace.config.setModuleUrl('ace/mode/yaml_worker', workerYamlUrl)
ace.config.setModuleUrl('ace/snippets/html', snippetsHtmlUrl)
ace.config.setModuleUrl('ace/snippets/javascript', snippetsJsUrl)
ace.config.setModuleUrl('ace/snippets/javascript', snippetsYamlUrl)
ace.config.setModuleUrl('ace/snippets/json', snippetsJsonUrl)
ace.require('ace/ext/language_tools')

View File

@@ -25,7 +25,7 @@ function goPlay() {
// 计算图片地址
const getImgUrl = computed(() => {
const image = props.media?.image || ''
return `${import.meta.env.VITE_API_BASE_URL}system/img/${encodeURIComponent(image)}`
return `${import.meta.env.VITE_API_BASE_URL}system/img/${encodeURIComponent(image)}/0`
})
</script>

View File

@@ -23,6 +23,11 @@ function getSpeedText() {
// 下载状态
const isDownloading = ref(props.info?.state === 'downloading')
// 监听props.info?.state的变化
watch(() => props.info?.state, (newValue) => {
isDownloading.value = newValue === 'downloading';
});
// 图片是否加载完成
const imageLoaded = ref(false)

View File

@@ -56,7 +56,7 @@ function getImgUrl(url: string) {
if (!url)
return getDefaultImage()
else
return `${import.meta.env.VITE_API_BASE_URL}system/proxy?url=${encodeURIComponent(url)}`
return `${import.meta.env.VITE_API_BASE_URL}system/img/${encodeURIComponent(url)}/0`
}
// 根据多张图片生成媒体库封面
@@ -68,7 +68,7 @@ async function drawImages(imageList: string[]) {
// 为所有图片添加system/img前缀
for (let i = 0; i < IMAGES.length; i++)
IMAGES[i] = `${import.meta.env.VITE_API_BASE_URL}system/img/${encodeURIComponent(IMAGES[i])}`
IMAGES[i] = `${import.meta.env.VITE_API_BASE_URL}system/img/${encodeURIComponent(IMAGES[i])}/0`
// canvas
const canvas = canvasRef.value

View File

@@ -85,9 +85,9 @@ const iconPath: Ref<string> = computed(() => {
return noImage
// 如果是网络图片则使用代理后返回
if (props.plugin?.plugin_icon?.startsWith('http'))
return `${import.meta.env.VITE_API_BASE_URL}system/img/${encodeURIComponent(props.plugin?.plugin_icon)}`
return `${import.meta.env.VITE_API_BASE_URL}system/img/${encodeURIComponent(props.plugin?.plugin_icon)}/1`
return `/plugin_icon/${props.plugin?.plugin_icon}`
return `./plugin_icon/${props.plugin?.plugin_icon}`
})
// 访问插件页面

View File

@@ -181,9 +181,9 @@ const iconPath: Ref<string> = computed(() => {
return noImage
// 如果是网络图片则使用代理后返回
if (props.plugin?.plugin_icon?.startsWith('http'))
return `${import.meta.env.VITE_API_BASE_URL}system/img/${encodeURIComponent(props.plugin?.plugin_icon)}`
return `${import.meta.env.VITE_API_BASE_URL}system/img/${encodeURIComponent(props.plugin?.plugin_icon)}/1`
return `/plugin_icon/${props.plugin?.plugin_icon}`
return `./plugin_icon/${props.plugin?.plugin_icon}`
})
// 重置插件

View File

@@ -31,7 +31,7 @@ const getImgUrl = computed(() => {
if (imageLoadError.value)
return noImage
const image = props.media?.image || ''
return `${import.meta.env.VITE_API_BASE_URL}system/img/${encodeURIComponent(image)}`
return `${import.meta.env.VITE_API_BASE_URL}system/img/${encodeURIComponent(image)}/0`
})
// 跳转播放

View File

@@ -80,6 +80,7 @@ const resourceItemsPerPage = ref(25)
const userPwForm = ref({
username: '',
password: '',
code: '',
})
// 打开种子详情页面
@@ -152,6 +153,7 @@ async function updateSiteCookie() {
params: {
username: userPwForm.value.username,
password: userPwForm.value.password,
code: userPwForm.value.code,
},
},
)
@@ -335,7 +337,7 @@ onMounted(() => {
<VRow>
<VCol
cols="12"
md="6"
md="4"
>
<VTextField
v-model="userPwForm.username"
@@ -345,7 +347,7 @@ onMounted(() => {
</VCol>
<VCol
cols="12"
md="6"
md="4"
>
<VTextField
v-model="userPwForm.password"
@@ -359,6 +361,15 @@ onMounted(() => {
@keydown.enter="updateSiteCookie"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VTextField
v-model="userPwForm.code"
label="两步验证"
/>
</VCol>
</VRow>
</VForm>
</VCardText>

View File

@@ -267,6 +267,28 @@ async function recognize(path: string) {
}
}
// 调用API刮削
async function scrape(path: string) {
try {
// 显示进度条
progressDialog.value = true
progressText.value = `正在刮削 ${path} ...`
const result: { [key: string]: any } = await api.get('media/scrape', {
params: {
path,
},
})
// 关闭进度条
progressDialog.value = false
if (!result.success)
$toast.error(result.message)
else
$toast.success(`${path}削刮完成!`)
}
catch (error) {
console.error(error)
}
}
// 弹出菜单
const dropdownItems = ref([
{
@@ -279,8 +301,17 @@ const dropdownItems = ref([
},
},
}, {
title: '重命名',
title: '刮削',
value: 2,
props: {
prependIcon: 'mdi-auto-fix',
click: (_item: FileItem) => {
scrape(_item.path || '')
},
},
}, {
title: '重命名',
value: 3,
props: {
prependIcon: 'mdi-rename',
click: showRenmae,
@@ -288,7 +319,7 @@ const dropdownItems = ref([
},
{
title: '整理',
value: 3,
value: 4,
props: {
prependIcon: 'mdi-folder-arrow-right',
click: showTransfer,
@@ -296,7 +327,7 @@ const dropdownItems = ref([
},
{
title: '删除',
value: 4,
value: 5,
props: {
prependIcon: 'mdi-delete-outline',
color: 'error',
@@ -345,111 +376,133 @@ onMounted(() => {
<VCardText v-else-if="dirs.length || files.length" class="p-0">
<VList v-if="dirs.length" subheader>
<VListSubheader>目录</VListSubheader>
<VListItem
<VHover
v-for="(item, index) in dirs"
:key="index"
class="px-3 pe-1"
@click="changePath(item.path)"
>
<template #prepend>
<VIcon icon="mdi-folder-outline" />
</template>
<VListItemTitle v-text="item.name" />
<template #append>
<IconBtn class="d-sm-none">
<VIcon
icon="mdi-dots-vertical"
/>
<VMenu
activator="parent"
close-on-content-click
>
<VList>
<VListItem
v-for="(menu, i) in dropdownItems"
:key="i"
variant="plain"
:base-color="menu.props.color"
@click="menu.props.click(item)"
<template #default="hover">
<VListItem
v-bind="hover.props"
class="px-3 pe-1"
@click="changePath(item.path)"
>
<template #prepend>
<VIcon icon="mdi-folder-outline" />
</template>
<VListItemTitle v-text="item.name" />
<template #append>
<IconBtn class="d-sm-none">
<VIcon
icon="mdi-dots-vertical"
/>
<VMenu
activator="parent"
close-on-content-click
>
<template #prepend>
<VIcon :icon="menu.props.prependIcon" />
</template>
<VListItemTitle v-text="menu.title" />
</VListItem>
</VList>
</VMenu>
</IconBtn>
<IconBtn class="d-none d-sm-block" @click.stop="recognize(item.path)">
<VIcon icon="mdi-text-recognition" />
</IconBtn>
<IconBtn class="d-none d-sm-block" @click.stop="showRenmae(item)">
<VIcon icon="mdi-rename" />
</IconBtn>
<IconBtn class="d-none d-sm-block" @click.stop="showTransfer(item)">
<VIcon icon="mdi-folder-arrow-right" />
</IconBtn>
<IconBtn class="d-none d-sm-block" @click.stop="deleteItem(item)">
<VIcon icon="mdi-delete-outline" />
</IconBtn>
<VList>
<VListItem
v-for="(menu, i) in dropdownItems"
:key="i"
variant="plain"
:base-color="menu.props.color"
@click="menu.props.click(item)"
>
<template #prepend>
<VIcon :icon="menu.props.prependIcon" />
</template>
<VListItemTitle v-text="menu.title" />
</VListItem>
</VList>
</VMenu>
</IconBtn>
<span v-show="hover.isHovering" class="flex">
<IconBtn class="d-none d-sm-block" @click.stop="recognize(item.path)">
<VIcon icon="mdi-text-recognition" />
</IconBtn>
<IconBtn class="d-none d-sm-block" @click.stop="scrape(item.path)">
<VIcon icon="mdi-auto-fix" />
</IconBtn>
<IconBtn class="d-none d-sm-block" @click.stop="showRenmae(item)">
<VIcon icon="mdi-rename" />
</IconBtn>
<IconBtn class="d-none d-sm-block" @click.stop="showTransfer(item)">
<VIcon icon="mdi-folder-arrow-right" />
</IconBtn>
<IconBtn class="d-none d-sm-block" @click.stop="deleteItem(item)">
<VIcon icon="mdi-delete-outline" />
</IconBtn>
</span>
</template>
</VListItem>
</template>
</VListItem>
</VHover>
</VList>
<VDivider v-if="dirs.length && files.length" />
<VList v-if="files.length" subheader>
<VListSubheader>文件</VListSubheader>
<VListItem
<VHover
v-for="(item, index) in files"
:key="index"
class="pl-3 pe-1"
@click="changePath(item.path)"
>
<template #prepend>
<VIcon v-if="inProps.icons" :icon="inProps.icons[item.extension.toLowerCase()] || inProps.icons?.other" />
</template>
<template #default="hover">
<VListItem
v-bind="hover.props"
class="pl-3 pe-1"
@click="changePath(item.path)"
>
<template #prepend>
<VIcon v-if="inProps.icons" :icon="inProps.icons[item.extension.toLowerCase()] || inProps.icons?.other" />
</template>
<VListItemTitle v-text="item.name" />
<VListItemSubtitle> {{ formatBytes(item.size) }}</VListItemSubtitle>
<VListItemTitle v-text="item.name" />
<VListItemSubtitle> {{ formatBytes(item.size) }}</VListItemSubtitle>
<template #append>
<IconBtn class="d-sm-none">
<VIcon
icon="mdi-dots-vertical"
/>
<VMenu
activator="parent"
close-on-content-click
>
<VList>
<VListItem
v-for="(menu, i) in dropdownItems"
:key="i"
variant="plain"
:base-color="menu.props.color"
@click="menu.props.click(item)"
<template #append>
<IconBtn class="d-sm-none">
<VIcon
icon="mdi-dots-vertical"
/>
<VMenu
activator="parent"
close-on-content-click
>
<template #prepend>
<VIcon :icon="menu.props.prependIcon" />
</template>
<VListItemTitle v-text="menu.title" />
</VListItem>
</VList>
</VMenu>
</IconBtn>
<IconBtn class="d-none d-sm-block" @click.stop="recognize(item.path)">
<VIcon icon="mdi-text-recognition" />
</IconBtn>
<IconBtn class="d-none d-sm-block" @click.stop="showRenmae(item)">
<VIcon icon="mdi-rename" />
</IconBtn>
<IconBtn class="d-none d-sm-block" @click.stop="showTransfer(item)">
<VIcon icon="mdi-folder-arrow-right" />
</IconBtn>
<IconBtn class="d-none d-sm-block" @click.stop="deleteItem(item)">
<VIcon icon="mdi-delete-outline" />
</IconBtn>
<VList>
<VListItem
v-for="(menu, i) in dropdownItems"
:key="i"
variant="plain"
:base-color="menu.props.color"
@click="menu.props.click(item)"
>
<template #prepend>
<VIcon :icon="menu.props.prependIcon" />
</template>
<VListItemTitle v-text="menu.title" />
</VListItem>
</VList>
</VMenu>
</IconBtn>
<span v-show="hover.isHovering" class="flex">
<IconBtn class="d-none d-sm-block" @click.stop="recognize(item.path)">
<VIcon icon="mdi-text-recognition" />
</IconBtn>
<IconBtn class="d-none d-sm-block" @click.stop="scrape(item.path)">
<VIcon icon="mdi-auto-fix" />
</IconBtn>
<IconBtn class="d-none d-sm-block" @click.stop="showRenmae(item)">
<VIcon icon="mdi-rename" />
</IconBtn>
<IconBtn class="d-none d-sm-block" @click.stop="showTransfer(item)">
<VIcon icon="mdi-folder-arrow-right" />
</IconBtn>
<IconBtn class="d-none d-sm-block" @click.stop="deleteItem(item)">
<VIcon icon="mdi-delete-outline" />
</IconBtn>
</span>
</template>
</VListItem>
</template>
</VListItem>
</VHover>
</VList>
</VCardText>
<VCardText

View File

@@ -326,7 +326,6 @@ watchEffect(() => {
<VRow>
<VCol
cols="12"
md="4"
>
<VTextField
v-model="subscribeForm.save_path"

View File

@@ -32,23 +32,58 @@ const formData = ref<any>(elementProps.form || {})
<template>
<Component
:is="formItem.component"
v-if="!formItem.html"
v-if="!formItem.html && !!formItem.props?.modelvalue"
v-bind="formItem.props"
v-model="formData[formItem.props?.model || '']"
v-model:value="formData[formItem.props?.modelvalue]"
>
{{ formItem.text }}
<FormRender
<template
v-for="(innerItem, innerIndex) in (formItem.content || [])"
:key="innerIndex"
v-model="formData[innerItem.props?.model || '']"
:config="innerItem"
:form="formData"
/>
>
<FormRender
v-if="!!innerItem.props?.modelvalue"
v-model:value="formData[innerItem.props?.modelvalue]"
:config="innerItem"
:form="formData"
/>
<FormRender
v-else
v-model="formData[innerItem.props?.model]"
:config="innerItem"
:form="formData"
/>
</template>
</Component>
<Component
:is="formItem.component"
v-if="formItem.html"
v-else-if="formItem.html"
v-bind="formItem.props"
v-html="formItem.html"
/>
<Component
:is="formItem.component"
v-else
v-bind="formItem.props"
v-model="formData[formItem.props?.model]"
>
{{ formItem.text }}
<template
v-for="(innerItem, innerIndex) in (formItem.content || [])"
:key="innerIndex"
>
<FormRender
v-if="!!innerItem.props?.modelvalue"
v-model:value="formData[innerItem.props?.modelvalue]"
:config="innerItem"
:form="formData"
/>
<FormRender
v-else
v-model="formData[innerItem.props?.model]"
:config="innerItem"
:form="formData"
/>
</template>
</Component>
</template>

View File

@@ -14,6 +14,10 @@ const themes: ThemeSwitcherTheme[] = [
name: 'purple',
icon: 'mdi-brightness-4',
},
{
name: 'auto',
icon: 'mdi-brightness-auto',
},
]
</script>

View File

@@ -3,6 +3,7 @@ import NameTestView from '@/views/system/NameTestView.vue'
import NetTestView from '@/views/system/NetTestView.vue'
import LoggingView from '@/views/system/LoggingView.vue'
import RuleTestView from '@/views/system/RuleTestView.vue'
import store from '@/store'
// App捷径
const appsMenu = ref(false)
@@ -18,6 +19,12 @@ const loggingDialog = ref(false)
// 过滤规则弹窗
const ruleTestDialog = ref(false)
// 拼接全部日志url
function allLoggingUrl() {
const token = store.state.auth.token
return `${import.meta.env.VITE_API_BASE_URL}system/logging?token=${token}&length=-1`
}
</script>
<template>
@@ -171,8 +178,19 @@ const ruleTestDialog = ref(false)
class="w-full lg:w-4/5"
scrollable
>
<VCard title="实时日志">
<VCard>
<DialogCloseBtn @click="loggingDialog = false" />
<VCardItem>
<VCardTitle class="inline-flex">
实时日志
<a class="mx-2 inline-flex items-center justify-center" :href="allLoggingUrl()" target="_blank">
<div class="inline-flex cursor-pointer items-center rounded-full bg-gray-600 px-2 text-sm text-gray-200 ring-1 ring-gray-500 transition hover:bg-gray-700">
<VIcon icon="mdi-open-in-new" />
<span class="ms-1">在新窗口中打开</span>
</div>
</a>
</VCardTitle>
</VCardItem>
<VCardText>
<LoggingView />
</VCardText>

View File

@@ -1,7 +1,10 @@
import { VAceEditor } from 'vue3-ace-editor'
import { createApp } from 'vue'
import '@/@iconify/icons-bundle'
import ToastPlugin from 'vue-toast-notification'
import VuetifyUseDialog from 'vuetify-use-dialog'
import './ace-config'
import VueApexCharts from 'vue3-apexcharts'
import { removeEl } from './@core/utils/dom'
import App from '@/App.vue'
import vuetify from '@/plugins/vuetify'
@@ -15,10 +18,14 @@ import 'vue-toast-notification/dist/theme-bootstrap.css'
loadFonts()
// Create vue app
// 创建Vue实例
const app = createApp(App)
// Use plugins Mount vue app
// 注册全局组件
app.component('VAceEditor', VAceEditor)
.component('VApexChart', VueApexCharts)
// 注册插件
app
.use(vuetify)
.use(router)

View File

@@ -8,6 +8,7 @@ import AccountSettingAbout from '@/views/setting/AccountSettingAbout.vue'
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'
const route = useRoute()
@@ -20,6 +21,11 @@ const tabs = [
icon: 'mdi-account',
tab: 'account',
},
{
title: '系统',
icon: 'mdi-cog',
tab: 'system',
},
{
title: '站点',
icon: 'mdi-web',
@@ -83,6 +89,13 @@ const tabs = [
</transition>
</VWindowItem>
<!-- 系统 -->
<VWindowItem value="system">
<transition name="fade-slide" appear>
<AccountSettingSystem />
</transition>
</VWindowItem>
<!-- 站点 -->
<VWindowItem value="site">
<transition name="fade-slide" appear>

View File

@@ -1,4 +1,4 @@
import { createRouter, createWebHistory } from 'vue-router'
import { createRouter, createWebHashHistory } from 'vue-router'
import { configureNProgress, doneNProgress, startNProgress } from '@/api/nprogress'
import store from '@/store'
@@ -7,7 +7,7 @@ configureNProgress()
// Router
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
history: createWebHashHistory(import.meta.env.BASE_URL),
scrollBehavior(to, from, savedPosition) {
// 如果页面有缓存那么恢复其位置, 否则始终滚动到顶部
if (to.meta.keepAlive && savedPosition)

View File

@@ -544,7 +544,9 @@ onBeforeMount(() => {
<ul v-if="mediaDetail.tmdb_id" class="media-crew">
<li v-for="director in mediaDetail.directors" :key="director.id">
<span>{{ director.job }}</span>
<a class="crew-name" :href="`person?personid=${director.id}`" target="_blank">{{ director.name }}</a>
<RouterLink :to="`/person?personid=${director.id}`" class="crew-name" target="_blank">
{{ director.name }}
</RouterLink>
</li>
</ul>
<ul v-if="!mediaDetail.tmdb_id && mediaDetail.douban_id" class="media-crew">

View File

@@ -5,6 +5,29 @@ import type { NotificationSwitch } from '@/api/types'
const messagemTypes = ref<NotificationSwitch[]>([])
// 选中的消息渠道
const selectedChannels = ref([])
// 消息渠道
const NotificationChannels = [
{
title: '微信',
value: 'wechat',
},
{
title: 'Telegram',
value: 'telegram',
},
{
title: 'Slack',
value: 'slack',
},
{
title: 'SynologyChat',
value: 'synologychat',
},
]
// 提示框
const $toast = useToast()
@@ -40,79 +63,147 @@ async function saveNotificationSwitchs() {
}
}
// 调用API查询消息渠道设置
async function loadNotificationChannels() {
try {
const result: { [key: string]: any } = await api.get('system/setting/MESSAGER')
if (result.success)
selectedChannels.value = result.data?.value?.split(',')
}
catch (error) {
console.log(error)
}
}
// 调用API保存消息渠道设置
async function saveNotificationChannels() {
try {
const result: { [key: string]: any } = await api.post(
'system/setting/MESSAGER',
selectedChannels.value.join(','),
)
if (result.success)
$toast.success('保存通知渠道设置成功')
else
$toast.error('保存通知渠道设置失败!')
}
catch (error) {
console.log(error)
}
}
onMounted(() => {
loadNotificationSwitchs()
loadNotificationChannels()
})
</script>
<template>
<VCard title="消息通知">
<VCardText> 对应消息类型只会发送给选中的消息渠道 </VCardText>
<VRow>
<VCol cols="12">
<VCard title="通知渠道">
<VCardSubtitle>只有选中的渠道才会发送消息</VCardSubtitle>
<VCardText>
<VForm>
<VRow>
<VCol cols="12" md="6">
<VSelect
v-model="selectedChannels"
multiple
chips
:items="NotificationChannels"
label="当前使用通知渠道"
/>
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardText>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn
mtype="submit"
@click="saveNotificationChannels"
>
保存
</VBtn>
</div>
</VForm>
</VCardText>
</VCard>
</VCol>
</VRow>
<VRow>
<VCol cols="12">
<VCard title="消息类型">
<VCardSubtitle> 对应消息类型只会发送给选中的消息渠道 </VCardSubtitle>
<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>
</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>
</tr>
<tr v-if="messagemTypes.length === 0">
<td
colspan="4"
class="text-center"
>
没有设置任何通知渠道
</td>
</tr>
</tbody>
</VTable>
<VDivider />
<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>
</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>
</tr>
<tr v-if="messagemTypes.length === 0">
<td
colspan="4"
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>
</div>
</VForm>
</VCardText>
</VCard>
<VCardText>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn
mtype="submit"
@click="saveNotificationSwitchs"
>
保存
</VBtn>
</div>
</VForm>
</VCardText>
</VCard>
</VCol>
</VRow>
</template>

View File

@@ -78,7 +78,7 @@ onUnmounted(() => {
<template>
<VCard title="定时作业">
<VCardText> 手动执行不会影响作业正常的时间表 </VCardText>
<VCardSubtitle> 手动执行不会影响作业正常的时间表 </VCardSubtitle>
<VTable class="text-no-wrap">
<thead>

View File

@@ -94,7 +94,7 @@ onMounted(() => {
<VSelect
v-model="selectedTorrentPriority"
:items="TorrentPriorityItems"
label="优先规则"
label="当前使用下载优先规则"
outlined
/>
</VCol>

View File

@@ -0,0 +1,194 @@
<script lang="ts" setup>
import { useToast } from 'vue-toast-notification'
import api from '@/api'
// 选中的下载器
const selectedDownloader = ref('')
// 选中的媒体服务器
const selectedMediaServers = ref([])
// 是否下载器监控
const downloaderMonitor = ref<boolean>(false)
// 下载器
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 $toast = useToast()
// 调用API查询下载器设置
async function loadDownladerSetting() {
try {
const result1: { [key: string]: any } = await api.get('system/setting/DOWNLOADER')
if (result1.success)
selectedDownloader.value = result1.data?.value
const result2: { [key: string]: any } = await api.get('system/setting/DOWNLOADER_MONITOR')
if (result2.success)
downloaderMonitor.value = result2.data?.value
}
catch (error) {
console.log(error)
}
}
// 调用API保存下载器设置
async function saveDownloaderSetting() {
try {
const result1: { [key: string]: any } = await api.post(
'system/setting/DOWNLOADER',
selectedDownloader.value,
)
const result2: { [key: string]: any } = await api.post(
'system/setting/DOWNLOADER_MONITOR',
downloaderMonitor.value,
)
if (result1.success && result2.success)
$toast.success('保存下载器设置成功')
else
$toast.error('保存下载器设置失败!')
}
catch (error) {
console.log(error)
}
}
// 调用API查询媒体服务器设置
async function loadMediaServerSetting() {
try {
const result: { [key: string]: any } = await api.get('system/setting/MEDIASERVER')
if (result.success)
selectedMediaServers.value = result.data?.value?.split(',')
}
catch (error) {
console.log(error)
}
}
// 调用API保存媒体服务器设置
async function saveMediaServerSetting() {
try {
const result: { [key: string]: any } = await api.post(
'system/setting/MEDIASERVER',
selectedMediaServers.value.join(','),
)
if (result.success)
$toast.success('保存媒体服务器设置成功')
else
$toast.error('保存媒体服务器设置失败!')
}
catch (error) {
console.log(error)
}
}
onMounted(() => {
loadDownladerSetting()
loadMediaServerSetting()
})
</script>
<template>
<VRow>
<VCol cols="12">
<VCard title="下载器">
<VCardSubtitle>只有选中的下载器才会被默认使用</VCardSubtitle>
<VCardText>
<VForm>
<VRow>
<VCol cols="12" md="6">
<VSelect
v-model="selectedDownloader"
:items="Downloaders"
label="当前使用下载器"
/>
</VCol>
</VRow>
<VRow>
<VCol cols="12" md="6">
<VSwitch
v-model="downloaderMonitor"
label="监控下载器"
/>
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardText>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn
mtype="submit"
@click="saveDownloaderSetting"
>
保存
</VBtn>
</div>
</VForm>
</VCardText>
</VCard>
</VCol>
</VRow>
<VRow>
<VCol cols="12">
<VCard title="媒体服务器">
<VCardSubtitle>只有选中的媒体服务器才会被默认使用</VCardSubtitle>
<VCardText>
<VForm>
<VRow>
<VCol cols="12" md="6">
<VSelect
v-model="selectedMediaServers"
multiple
chips
:items="MediaServers"
label="当前使用媒体服务器"
/>
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardText>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn
mtype="submit"
@click="saveMediaServerSetting"
>
保存
</VBtn>
</div>
</VForm>
</VCardText>
</VCard>
</VCol>
</VRow>
</template>

View File

@@ -9,6 +9,7 @@ import vuetify from 'vite-plugin-vuetify'
// https://vitejs.dev/config/
export default defineConfig({
base: './',
plugins: [
vue(),
vueJsx(),
@@ -27,7 +28,16 @@ export default defineConfig({
imports: ['vue', 'vue-router', '@vueuse/core', '@vueuse/math', 'vuex'],
vueTemplate: true,
}),
VitePWA({ registerType: 'autoUpdate', injectRegister: 'script', manifest: false }),
VitePWA({
registerType: 'autoUpdate',
injectRegister: 'script',
manifest: false,
workbox: {
navigateFallbackDenylist: [
/.*\/api\/v\d+\/system\/logging.*/,
],
},
}),
],
define: { 'process.env': {} },
resolve: {

View File

@@ -2422,6 +2422,11 @@ accepts@~1.3.8:
mime-types "~2.1.34"
negotiator "0.6.3"
ace-builds@^1.32.6:
version "1.32.6"
resolved "https://registry.yarnpkg.com/ace-builds/-/ace-builds-1.32.6.tgz#454ec8bc9235fbb960b8d8b86e698f941c104de2"
integrity sha512-dO5BnyDOhCnznhOpILzXq4jqkbhRXxNkf3BuVTmyxGyRLrhddfdyk6xXgy+7A8LENrcYoFi/sIxMuH3qjNUN4w==
acorn-jsx@^5.2.0, acorn-jsx@^5.3.2:
version "5.3.2"
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
@@ -6651,6 +6656,11 @@ require-from-string@^2.0.2:
resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909"
integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==
resize-observer-polyfill@^1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==
resolve-from@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
@@ -7912,6 +7922,13 @@ vue-tsc@^1.6.5:
"@volar/vue-typescript" "1.6.5"
semver "^7.3.8"
vue3-ace-editor@^2.2.4:
version "2.2.4"
resolved "https://registry.yarnpkg.com/vue3-ace-editor/-/vue3-ace-editor-2.2.4.tgz#1f2a787f91cf7979f27fab29e0e0604bb3ee1c17"
integrity sha512-FZkEyfpbH068BwjhMyNROxfEI8135Sc+x8ouxkMdCNkuj/Tuw83VP/gStFQqZHqljyX9/VfMTCdTqtOnJZGN8g==
dependencies:
resize-observer-polyfill "^1.5.1"
vue3-apexcharts@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/vue3-apexcharts/-/vue3-apexcharts-1.4.1.tgz#ea561308430a1c5213b7f17c44ba3c845f6c490d"