mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-05-11 01:50:10 +08:00
Compare commits
92 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3cb5f4bdfe | ||
|
|
d355e4575d | ||
|
|
bdbb118e55 | ||
|
|
9a174d99db | ||
|
|
9c8725066c | ||
|
|
9f0f3de864 | ||
|
|
ac84ed2d6a | ||
|
|
9d7e15f4df | ||
|
|
c3563f4501 | ||
|
|
a543202edc | ||
|
|
52cf517a91 | ||
|
|
11b649dc8c | ||
|
|
19663bacb1 | ||
|
|
41c276d0e0 | ||
|
|
6bb73add28 | ||
|
|
2c16b6c078 | ||
|
|
5ddc955805 | ||
|
|
6a3afa4240 | ||
|
|
deabd7b83c | ||
|
|
422e5858ef | ||
|
|
3c019d1376 | ||
|
|
f676e8423e | ||
|
|
f687d1de01 | ||
|
|
6fe28bc2ef | ||
|
|
86b5af3423 | ||
|
|
8f3dce058c | ||
|
|
825b8bb4a5 | ||
|
|
05320d1070 | ||
|
|
33d2a396ce | ||
|
|
ae4cce8abf | ||
|
|
b85950e4ca | ||
|
|
aecf52551b | ||
|
|
fc877ed836 | ||
|
|
5580921b7d | ||
|
|
6b7d0a0fe2 | ||
|
|
f55efbe1e2 | ||
|
|
8e6fc3c417 | ||
|
|
7943ab6017 | ||
|
|
81725a58cf | ||
|
|
5cbcf46aaa | ||
|
|
49dd3f726a | ||
|
|
73f9ebc709 | ||
|
|
f6884ba4f9 | ||
|
|
5d39d0e139 | ||
|
|
6a1463ef17 | ||
|
|
5d00f23cb3 | ||
|
|
6ea106b25d | ||
|
|
d501bf7506 | ||
|
|
1408060053 | ||
|
|
0c37c01496 | ||
|
|
d2049f7839 | ||
|
|
33cdf672b3 | ||
|
|
145c89acc3 | ||
|
|
706d7d6dc1 | ||
|
|
2c35d0f897 | ||
|
|
f227ae89ec | ||
|
|
ac43d53884 | ||
|
|
4b70549bcb | ||
|
|
ea601ae404 | ||
|
|
201411841c | ||
|
|
d857acc58e | ||
|
|
d005252f13 | ||
|
|
2065992b17 | ||
|
|
74e96980e6 | ||
|
|
09110d1ef7 | ||
|
|
bcf55e63f1 | ||
|
|
dd22b2580e | ||
|
|
62a0e46698 | ||
|
|
14b68135fb | ||
|
|
d44b62e489 | ||
|
|
b0f5c2a493 | ||
|
|
d6cfbc60a8 | ||
|
|
fe51f5ced4 | ||
|
|
b257b0453e | ||
|
|
a88105a086 | ||
|
|
2dc792690e | ||
|
|
aa146b1cdf | ||
|
|
c44b20bae3 | ||
|
|
cad8964841 | ||
|
|
ec9a989214 | ||
|
|
7f05932fb9 | ||
|
|
d51694e1cb | ||
|
|
3079483e6b | ||
|
|
bee4264a39 | ||
|
|
c949ea2667 | ||
|
|
2bcb28d0c0 | ||
|
|
bd257554cd | ||
|
|
68a27e0b61 | ||
|
|
8b589bdb9c | ||
|
|
1a25710aac | ||
|
|
271d59ca51 | ||
|
|
37e5e57d5b |
@@ -30,6 +30,7 @@
|
||||
<meta name="HandheldFriendly" content="True" />
|
||||
<meta name="MobileOptimized" content="320" />
|
||||
<link rel="stylesheet" type="text/css" href="/loader.css" />
|
||||
<link rel="preload" href="index.js" as="script">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@@ -159,4 +160,4 @@
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
||||
35
package.json
35
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "moviepilot",
|
||||
"version": "1.8.5-2",
|
||||
"version": "1.9.2-1",
|
||||
"private": true,
|
||||
"bin": "dist/service.js",
|
||||
"scripts": {
|
||||
@@ -19,42 +19,37 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@casl/ability": "^6.2.0",
|
||||
"@casl/vue": "^2.2.0",
|
||||
"@floating-ui/dom": "1.6.3",
|
||||
"@fullcalendar/core": "^6.1.8",
|
||||
"@fullcalendar/daygrid": "^6.1.8",
|
||||
"@fullcalendar/interaction": "^6.1.7",
|
||||
"@fullcalendar/list": "^6.1.7",
|
||||
"@fullcalendar/timegrid": "^6.1.7",
|
||||
"@fullcalendar/vue3": "^6.1.8",
|
||||
"@iconify/utils": "^2.1.22",
|
||||
"@vueuse/core": "^10.1.2",
|
||||
"@vueuse/math": "^10.1.2",
|
||||
"ace-builds": "^1.32.6",
|
||||
"apexcharts-clevision": "^3.28.5",
|
||||
"axios": "1.6.8",
|
||||
"axios-mock-adapter": "^1.21.4",
|
||||
"chart.js": "^4.1.2",
|
||||
"colorthief": "^2.4.0",
|
||||
"dayjs": "^1.11.10",
|
||||
"express": "^4.18.2",
|
||||
"express-http-proxy": "^2.0.0",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"nprogress": "^0.2.0",
|
||||
"postcss-purgecss": "^5.0.0",
|
||||
"prismjs": "^1.29.0",
|
||||
"pull-refresh-vue3": "^0.3.1",
|
||||
"qrcode.vue": "^3.4.1",
|
||||
"roboto-fontface": "^0.10.0",
|
||||
"sass": "^1.59.3",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"unplugin-vue-define-options": "^1.3.5",
|
||||
"vite-plugin-pwa": "^0.19.8",
|
||||
"vue": "^3.3.2",
|
||||
"vue-chartjs": "^5.2.0",
|
||||
"vue-flatpickr-component": "11.0.5",
|
||||
"vue-i18n": "^9.2.2",
|
||||
"vue-prism-component": "^2.0.0",
|
||||
"vue-router": "^4.2.0",
|
||||
"vue-toast-notification": "^3",
|
||||
"vue-virtual-scroll-grid": "^1.11.0",
|
||||
"vue3-ace-editor": "^2.2.4",
|
||||
"vue3-apexcharts": "^1.4.1",
|
||||
"vue3-perfect-scrollbar": "^2.0.0",
|
||||
"vuedraggable": "^4.1.0",
|
||||
"vuetify": "3.5.14",
|
||||
"vuetify-use-dialog": "^0.6.11",
|
||||
"vuex": "^4.1.0",
|
||||
@@ -63,12 +58,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config-vue": "^0.43.1",
|
||||
"@fullcalendar/core": "^6.1.8",
|
||||
"@fullcalendar/daygrid": "^6.1.8",
|
||||
"@fullcalendar/interaction": "^6.1.7",
|
||||
"@fullcalendar/list": "^6.1.7",
|
||||
"@fullcalendar/timegrid": "^6.1.7",
|
||||
"@fullcalendar/vue3": "^6.1.8",
|
||||
"@iconify-json/mdi": "^1.1.52",
|
||||
"@iconify/tools": "^4.0.4",
|
||||
"@iconify/vue": "4.1.1",
|
||||
@@ -82,7 +71,6 @@
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"@vitejs/plugin-vue-jsx": "^3.0.0",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"dayjs": "^1.11.10",
|
||||
"eslint": "^9.0.0",
|
||||
"eslint-config-airbnb-base": "^15.0.0",
|
||||
"eslint-import-resolver-typescript": "^3.5.1",
|
||||
@@ -92,7 +80,6 @@
|
||||
"eslint-plugin-sonarjs": "^0.25.1",
|
||||
"eslint-plugin-unicorn": "^52.0.0",
|
||||
"eslint-plugin-vue": "^9.12.0",
|
||||
"lodash": "^4.17.21",
|
||||
"postcss": "8",
|
||||
"postcss-html": "^1.5.0",
|
||||
"stylelint": "16.3.1",
|
||||
@@ -114,4 +101,4 @@
|
||||
"resolutions": {
|
||||
"postcss": "8"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
"name": "MoviePilot",
|
||||
"short_name": "MoviePilot",
|
||||
"start_url": "./",
|
||||
"display": "standalone",
|
||||
"icons": [
|
||||
{
|
||||
"src": "./android-chrome-192x192.png",
|
||||
@@ -30,7 +31,6 @@
|
||||
],
|
||||
"theme_color": "#28243D",
|
||||
"background_color": "#28243D",
|
||||
"display": "standalone",
|
||||
"shortcuts": [
|
||||
{
|
||||
"name": "推荐",
|
||||
@@ -77,4 +77,4 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,13 @@ http {
|
||||
|
||||
keepalive_timeout 3600;
|
||||
|
||||
gzip on;
|
||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
|
||||
gzip_proxied any;
|
||||
gzip_min_length 256;
|
||||
gzip_vary on;
|
||||
gzip_comp_level 6;
|
||||
|
||||
server {
|
||||
|
||||
include mime.types;
|
||||
@@ -28,9 +35,16 @@ http {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
|
||||
# 静态资源
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
root html;
|
||||
}
|
||||
|
||||
location /assets {
|
||||
# 静态资源
|
||||
expires 7d;
|
||||
expires 1y;
|
||||
add_header Cache-Control "public";
|
||||
root html;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useTheme } from 'vuetify'
|
||||
import { useDisplay, useTheme } from 'vuetify'
|
||||
import type { ThemeSwitcherTheme } from '@layouts/types'
|
||||
import api from '@/api'
|
||||
import { checkPrefersColorSchemeIsDark } from '@/@core/utils'
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import { VAceEditor } from 'vue3-ace-editor'
|
||||
|
||||
// 显示器宽度
|
||||
const display = useDisplay()
|
||||
|
||||
const props = defineProps<{
|
||||
themes: ThemeSwitcherTheme[]
|
||||
@@ -13,15 +18,22 @@ const { name: themeName, global: globalTheme } = useTheme()
|
||||
|
||||
const savedTheme = ref(localStorage.getItem('theme') ?? themeName)
|
||||
|
||||
const {
|
||||
state: currentThemeName,
|
||||
next: getNextThemeName,
|
||||
index: currentThemeIndex,
|
||||
} = useCycleList(
|
||||
const { state: currentThemeName, next: getNextThemeName } = useCycleList(
|
||||
props.themes.map(t => t.name),
|
||||
{ initialValue: savedTheme.value },
|
||||
)
|
||||
|
||||
const $toast = useToast()
|
||||
|
||||
// 自定义CSS弹窗
|
||||
const cssDialog = ref(false)
|
||||
|
||||
// 自定义 CSS
|
||||
const customCSS = ref('')
|
||||
|
||||
// 编辑器主题
|
||||
const editorTheme = computed(() => (currentThemeName.value === 'light' ? 'github' : 'monokai'))
|
||||
|
||||
// 主题切换动画
|
||||
function themeTransition() {
|
||||
const x = performance.now()
|
||||
@@ -90,15 +102,16 @@ function updateTheme() {
|
||||
globalTheme.name.value = theme
|
||||
savedTheme.value = theme
|
||||
themeTransition()
|
||||
// 保存主题到本地
|
||||
localStorage.setItem('theme', theme)
|
||||
localStorage.setItem('materio-initial-loader-bg', globalTheme.current.value.colors.background)
|
||||
}
|
||||
|
||||
// 切换主题
|
||||
function changeTheme() {
|
||||
const nextTheme = getNextThemeName()
|
||||
function changeTheme(theme: string) {
|
||||
let nextTheme = theme
|
||||
if (!theme) nextTheme = getNextThemeName()
|
||||
currentThemeName.value = nextTheme
|
||||
// 保存主题到本地
|
||||
localStorage.setItem('theme', nextTheme)
|
||||
localStorage.setItem('materio-initial-loader-bg', globalTheme.current.value.colors.background)
|
||||
// 保存主题到服务端
|
||||
try {
|
||||
api.post('/user/config/theme', nextTheme, {
|
||||
@@ -126,17 +139,100 @@ try {
|
||||
console.error('当前设备不支持监听系统主题变化')
|
||||
}
|
||||
|
||||
// 查询当前主题的图标
|
||||
const getThemeIcon = computed(() => {
|
||||
const theme = props.themes.find(t => t.name === currentThemeName.value)
|
||||
return theme?.icon ?? 'mdi-circle'
|
||||
})
|
||||
|
||||
// 监听设置主题变化
|
||||
watch(
|
||||
() => currentThemeName.value,
|
||||
() => updateTheme(),
|
||||
)
|
||||
|
||||
// 获取自定义 CSS
|
||||
async function getCustomCSS() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/setting/UserCustomCSS')
|
||||
if (result && result.success && result.data?.value) {
|
||||
customCSS.value = result.data?.value ?? ''
|
||||
if (customCSS.value) {
|
||||
const style = document.createElement('style')
|
||||
style.innerHTML = result.data?.value ?? ''
|
||||
document.head.appendChild(style)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存自定义 CSS
|
||||
async function saveCustomCSS() {
|
||||
cssDialog.value = false
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post('system/setting/UserCustomCSS', customCSS.value, {
|
||||
headers: {
|
||||
'Content-Type': 'text/plain',
|
||||
},
|
||||
})
|
||||
|
||||
if (result.success) $toast.success('自定义CSS保存成功!')
|
||||
} catch (e) {
|
||||
console.error('保存自定义 CSS 到服务端失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getCustomCSS()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<IconBtn @click="changeTheme">
|
||||
<VIcon :icon="props.themes[currentThemeIndex].icon" />
|
||||
</IconBtn>
|
||||
<VMenu v-if="props.themes">
|
||||
<template v-slot:activator="{ props }">
|
||||
<IconBtn v-bind="props">
|
||||
<VIcon :icon="getThemeIcon" />
|
||||
</IconBtn>
|
||||
</template>
|
||||
<VList>
|
||||
<VListItem v-for="theme in props.themes" :key="theme.name" @click="changeTheme(theme.name)">
|
||||
<template #prepend>
|
||||
<VIcon :icon="theme.icon" />
|
||||
</template>
|
||||
<VListItemTitle>{{ theme.title }}</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem @click="cssDialog = true">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-palette" />
|
||||
</template>
|
||||
<VListItemTitle>自定义</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
<!-- 自定义 CSS -- -->
|
||||
<VDialog v-model="cssDialog" persistent max-width="50rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VCard title="自定义主题风格">
|
||||
<DialogCloseBtn @click="cssDialog = false" />
|
||||
<VDivider />
|
||||
<VAceEditor
|
||||
v-model:value="customCSS"
|
||||
lang="css"
|
||||
:theme="editorTheme"
|
||||
style="block-size: 100%; min-block-size: 30rem"
|
||||
/>
|
||||
<VDivider />
|
||||
<VCardText class="text-center">
|
||||
<VBtn @click="saveCustomCSS" class="w-1/2">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-content-save" />
|
||||
</template>
|
||||
保存
|
||||
</VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style lang="sass">
|
||||
|
||||
11
src/@layouts/types.d.ts
vendored
11
src/@layouts/types.d.ts
vendored
@@ -6,19 +6,19 @@ export interface UserConfig {
|
||||
app: {
|
||||
title: Lowercase<string>
|
||||
logo: VNode
|
||||
contentWidth: typeof ContentWidth[keyof typeof ContentWidth]
|
||||
contentLayoutNav: typeof AppContentLayoutNav[keyof typeof AppContentLayoutNav]
|
||||
contentWidth: (typeof ContentWidth)[keyof typeof ContentWidth]
|
||||
contentLayoutNav: (typeof AppContentLayoutNav)[keyof typeof AppContentLayoutNav]
|
||||
overlayNavFromBreakpoint: number
|
||||
enableI18n: boolean
|
||||
isRtl: boolean
|
||||
iconRenderer?: Component
|
||||
}
|
||||
navbar: {
|
||||
type: typeof NavbarType[keyof typeof NavbarType]
|
||||
type: (typeof NavbarType)[keyof typeof NavbarType]
|
||||
navbarBlur: boolean
|
||||
}
|
||||
footer: {
|
||||
type:typeof FooterType[keyof typeof FooterType]
|
||||
type: (typeof FooterType)[keyof typeof FooterType]
|
||||
}
|
||||
verticalNav: {
|
||||
isVerticalNavCollapsed: boolean
|
||||
@@ -143,7 +143,7 @@ interface I18nLanguage {
|
||||
// avatar | text | icon
|
||||
// Thanks: https://stackoverflow.com/a/60617060/10796681
|
||||
type Notification = {
|
||||
id:number
|
||||
id: number
|
||||
title: string
|
||||
subtitle: string
|
||||
time: string
|
||||
@@ -157,5 +157,6 @@ type Notification = {
|
||||
|
||||
interface ThemeSwitcherTheme {
|
||||
name: string
|
||||
title: string
|
||||
icon: string
|
||||
}
|
||||
|
||||
60
src/App.vue
60
src/App.vue
@@ -1,15 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import { useTheme } from 'vuetify'
|
||||
import api from '@/api'
|
||||
import store from './store'
|
||||
import { checkPrefersColorSchemeIsDark } from '@/@core/utils'
|
||||
|
||||
const { global: globalTheme } = useTheme()
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
// 生效主题
|
||||
async function setTheme() {
|
||||
let themeValue = localStorage.getItem('theme') || 'light'
|
||||
@@ -17,47 +11,39 @@ async function setTheme() {
|
||||
globalTheme.name.value = themeValue === 'auto' ? autoTheme : themeValue
|
||||
}
|
||||
|
||||
// SSE持续接收消息
|
||||
function startSSEMessager() {
|
||||
const token = store.state.auth.token
|
||||
if (token) {
|
||||
const eventSource = new EventSource(`${import.meta.env.VITE_API_BASE_URL}system/message?token=${token}`)
|
||||
|
||||
eventSource.addEventListener('message', event => {
|
||||
const message = event.data
|
||||
if (message) $toast.info(message)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
eventSource.close()
|
||||
})
|
||||
// ApexCharts 全局配置
|
||||
declare global {
|
||||
interface Window {
|
||||
Apex: any
|
||||
}
|
||||
}
|
||||
|
||||
// 加载用户监控面板配置
|
||||
async function loadDashboardConfig() {
|
||||
const response = await api.get('/user/config/Dashboard')
|
||||
if (response && response.data && response.data.value) {
|
||||
const data = JSON.stringify(response.data.value)
|
||||
if (data != localStorage.getItem('MP_DASHBOARD')) {
|
||||
localStorage.setItem('MP_DASHBOARD', data)
|
||||
}
|
||||
if (window.Apex) {
|
||||
// 数据标签
|
||||
window.Apex.dataLabels = {
|
||||
formatter: function (_: number, { seriesIndex, w }: { seriesIndex: number; w: any }) {
|
||||
// 如果有小数点,保留两位小数,否则保留整数
|
||||
const data = w.config.series[seriesIndex]
|
||||
return data.toFixed(data % 1 === 0 ? 0 : 1)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试加载用户监控面板配置(本地无配置时才加载)
|
||||
async function tryLoadDashboardConfig() {
|
||||
if (localStorage.getItem('MP_DASHBOARD')) {
|
||||
return
|
||||
// 图例
|
||||
window.Apex.legend = {
|
||||
labels: {
|
||||
useSeriesColors: true,
|
||||
},
|
||||
}
|
||||
// 标题
|
||||
window.Apex.title = {
|
||||
style: {
|
||||
color: 'rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity))',
|
||||
},
|
||||
}
|
||||
await loadDashboardConfig()
|
||||
}
|
||||
|
||||
// 页面加载时,加载当前用户数据
|
||||
onBeforeMount(async () => {
|
||||
setTheme()
|
||||
startSSEMessager()
|
||||
await tryLoadDashboardConfig()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ import modeHtmlUrl from 'ace-builds/src-noconflict/mode-html?url'
|
||||
|
||||
import modeYamlUrl from 'ace-builds/src-noconflict/mode-yaml?url'
|
||||
|
||||
import modeCssUrl from 'ace-builds/src-noconflict/mode-css?url'
|
||||
|
||||
import themeGithubUrl from 'ace-builds/src-noconflict/theme-github?url'
|
||||
|
||||
import themeChromeUrl from 'ace-builds/src-noconflict/theme-chrome?url'
|
||||
@@ -24,6 +26,8 @@ import workerHtmlUrl from 'ace-builds/src-noconflict/worker-html?url'
|
||||
|
||||
import workerYamlUrl from 'ace-builds/src-noconflict/worker-yaml?url'
|
||||
|
||||
import workerCssUrl from 'ace-builds/src-noconflict/worker-css?url'
|
||||
|
||||
import snippetsHtmlUrl from 'ace-builds/src-noconflict/snippets/html?url'
|
||||
|
||||
import snippetsJsUrl from 'ace-builds/src-noconflict/snippets/javascript?url'
|
||||
@@ -32,12 +36,15 @@ import snippetsYamlUrl from 'ace-builds/src-noconflict/snippets/yaml?url'
|
||||
|
||||
import snippetsJsonUrl from 'ace-builds/src-noconflict/snippets/json?url'
|
||||
|
||||
import snippertsCssUrl from 'ace-builds/src-noconflict/snippets/css?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/mode/css', modeCssUrl)
|
||||
ace.config.setModuleUrl('ace/theme/github', themeGithubUrl)
|
||||
ace.config.setModuleUrl('ace/theme/chrome', themeChromeUrl)
|
||||
ace.config.setModuleUrl('ace/theme/monokai', themeMonokaiUrl)
|
||||
@@ -46,9 +53,11 @@ 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/mode/css_worker', workerCssUrl)
|
||||
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.config.setModuleUrl('ace/snippets/css', snippertsCssUrl)
|
||||
|
||||
ace.require('ace/ext/language_tools')
|
||||
|
||||
@@ -61,6 +61,8 @@ export interface Subscribe {
|
||||
save_path: string
|
||||
// 时间
|
||||
date: string
|
||||
// 编辑框设置项
|
||||
show_edit_dialog: boolean
|
||||
}
|
||||
|
||||
// 历史记录
|
||||
@@ -332,6 +334,8 @@ export interface Site {
|
||||
public?: number
|
||||
// 备注
|
||||
note?: string
|
||||
// 超时时间
|
||||
timeout?: number
|
||||
// 流控单位周期
|
||||
limit_interval?: number
|
||||
// 流控次数
|
||||
@@ -443,6 +447,33 @@ export interface Plugin {
|
||||
add_time?: number
|
||||
}
|
||||
|
||||
// 渲染结构
|
||||
export interface RenderProps {
|
||||
component: string
|
||||
text?: string
|
||||
html?: string
|
||||
content?: any
|
||||
slots?: any
|
||||
props?: any
|
||||
events?: any
|
||||
}
|
||||
|
||||
// 仪表板组件
|
||||
export interface DashboardItem {
|
||||
// ID
|
||||
id: string
|
||||
// 名称
|
||||
name: string
|
||||
// 插件的仪表板key
|
||||
key: string
|
||||
// 全局配置
|
||||
attrs: { [key: string]: any }
|
||||
// col列数
|
||||
cols: { [key: string]: number }
|
||||
// 页面元素
|
||||
elements: RenderProps[]
|
||||
}
|
||||
|
||||
// 种子信息
|
||||
export interface TorrentInfo {
|
||||
// 站点ID
|
||||
@@ -687,12 +718,6 @@ export interface NotificationSwitch {
|
||||
vocechat: boolean
|
||||
}
|
||||
|
||||
// 环境设置
|
||||
export interface Setting {
|
||||
// 下载目录
|
||||
DOWNLOAD_PATH: string
|
||||
}
|
||||
|
||||
// 文件浏览接口
|
||||
export interface EndPoints {
|
||||
// 文件列表
|
||||
@@ -790,3 +815,35 @@ export interface Message {
|
||||
// JSON
|
||||
note?: string
|
||||
}
|
||||
|
||||
// 系统通知
|
||||
export interface SystemNotification {
|
||||
// 通知类型 user/system/plugin
|
||||
type: string
|
||||
// 通知标题
|
||||
title: string
|
||||
// 通知内容
|
||||
text: string
|
||||
// 通知时间
|
||||
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
|
||||
}
|
||||
|
||||
1
src/assets/images/no-data.svg
Normal file
1
src/assets/images/no-data.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 38 KiB |
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import image from '@images/misc/teamwork.png'
|
||||
import image from '@images/no-data.svg'
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
@@ -11,10 +11,7 @@ interface Props {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VEmptyState
|
||||
:image="image"
|
||||
size="250"
|
||||
>
|
||||
<VEmptyState :image="image" size="250">
|
||||
<template #title>
|
||||
<div class="mt-8 text-2xl">
|
||||
{{ props.errorTitle }}
|
||||
@@ -22,7 +19,7 @@ interface Props {
|
||||
</template>
|
||||
|
||||
<template #text>
|
||||
<div class="text-subtitle">
|
||||
<div class="text-subtitle mt-3">
|
||||
{{ props.errorDescription }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -18,8 +18,7 @@ function imageLoadHandler() {
|
||||
|
||||
// 跳转播放
|
||||
function goPlay() {
|
||||
if (props.media?.link)
|
||||
window.open(props.media?.link, '_blank')
|
||||
if (props.media?.link) window.open(props.media?.link, '_blank')
|
||||
}
|
||||
|
||||
// 计算图片地址
|
||||
@@ -30,11 +29,7 @@ const getImgUrl = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VHover
|
||||
v-bind="props"
|
||||
:height="props.height"
|
||||
:width="props.width"
|
||||
>
|
||||
<VHover v-bind="props">
|
||||
<template #default="hover">
|
||||
<VCard
|
||||
v-bind="hover.props"
|
||||
@@ -48,12 +43,7 @@ const getImgUrl = computed(() => {
|
||||
@click="goPlay"
|
||||
>
|
||||
<template #image>
|
||||
<VImg
|
||||
:src="getImgUrl"
|
||||
aspect-ratio="2/3"
|
||||
cover
|
||||
@load="imageLoadHandler"
|
||||
>
|
||||
<VImg :src="getImgUrl" aspect-ratio="2/3" cover @load="imageLoadHandler">
|
||||
<template #placeholder>
|
||||
<div class="w-full h-full">
|
||||
<VSkeletonLoader class="object-cover aspect-w-3 aspect-h-2" />
|
||||
@@ -62,7 +52,9 @@ const getImgUrl = computed(() => {
|
||||
<VCardText
|
||||
class="w-full flex flex-col flex-wrap justify-end align-left text-white absolute bottom-0 cursor-pointer pa-2"
|
||||
>
|
||||
<h1 class="mb-1 text-white text-shadow font-extrabold text-xl line-clamp-2 overflow-hidden text-ellipsis ...">
|
||||
<h1
|
||||
class="mb-1 text-white text-shadow font-extrabold text-xl line-clamp-2 overflow-hidden text-ellipsis ..."
|
||||
>
|
||||
{{ props.media?.title }}
|
||||
</h1>
|
||||
<span class="text-shadow">{{ props.media?.subtitle }}</span>
|
||||
@@ -83,7 +75,7 @@ const getImgUrl = computed(() => {
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.text-shadow{
|
||||
text-shadow:1px 1px #777;
|
||||
.text-shadow {
|
||||
text-shadow: 1px 1px #777;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -9,23 +9,13 @@ const props = defineProps({
|
||||
})
|
||||
|
||||
// 定义触发的自定义事件
|
||||
const emit = defineEmits(['close', 'changed', 'levelup', 'leveldown'])
|
||||
const emit = defineEmits(['close', 'changed'])
|
||||
|
||||
// 按钮点击
|
||||
function onClose() {
|
||||
emit('close')
|
||||
}
|
||||
|
||||
// 上升优先级
|
||||
function onLevelUp() {
|
||||
emit('levelup', props.pri)
|
||||
}
|
||||
|
||||
// 下降优先级
|
||||
function onLevelDown() {
|
||||
emit('leveldown', props.pri)
|
||||
}
|
||||
|
||||
// 选项变化
|
||||
function filtersChanged(value: string[]) {
|
||||
emit('changed', props.pri, value)
|
||||
@@ -76,18 +66,9 @@ const selectFilterOptions = ref<{ [key: string]: string }[]>([
|
||||
|
||||
<template>
|
||||
<VCard variant="tonal" :width="props.width" :height="props.height">
|
||||
<span class="absolute top-3 right-14">
|
||||
<IconBtn
|
||||
v-if="props.pri !== '1'"
|
||||
@click.stop="onLevelUp"
|
||||
>
|
||||
<VIcon icon="mdi-arrow-up" />
|
||||
</IconBtn>
|
||||
<IconBtn
|
||||
v-if="props.pri !== props.maxpri"
|
||||
@click.stop="onLevelDown"
|
||||
>
|
||||
<VIcon icon="mdi-arrow-down" />
|
||||
<span class="absolute top-3 right-12">
|
||||
<IconBtn>
|
||||
<VIcon class="cursor-move" icon="mdi-drag" />
|
||||
</IconBtn>
|
||||
</span>
|
||||
<DialogCloseBtn @click="onClose" />
|
||||
@@ -96,7 +77,6 @@ const selectFilterOptions = ref<{ [key: string]: string }[]>([
|
||||
<VRow>
|
||||
<VCol>
|
||||
<VSelect
|
||||
:key="props.pri"
|
||||
v-model="props.rules"
|
||||
variant="underlined"
|
||||
:items="selectFilterOptions"
|
||||
|
||||
@@ -19,6 +19,8 @@ const props = defineProps({
|
||||
height: String,
|
||||
})
|
||||
|
||||
const store = useStore()
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
@@ -56,7 +58,7 @@ const seasonInfos = ref<TmdbSeason[]>([])
|
||||
const seasonsSelected = ref<TmdbSeason[]>([])
|
||||
|
||||
// 来源角标字典
|
||||
const sourceIconDict = {
|
||||
const sourceIconDict: { [key: string]: any } = {
|
||||
themoviedb: tmdbImage,
|
||||
douban: doubanImage,
|
||||
bangumi: bangumiImage,
|
||||
@@ -64,11 +66,9 @@ const sourceIconDict = {
|
||||
|
||||
// 获得mediaid
|
||||
function getMediaId() {
|
||||
return props.media?.tmdb_id
|
||||
? `tmdb:${props.media?.tmdb_id}`
|
||||
: props.media?.douban_id
|
||||
? `douban:${props.media?.douban_id}`
|
||||
: `bangumi:${props.media?.bangumi_id}`
|
||||
if (props.media?.tmdb_id) return `tmdb:${props.media?.tmdb_id}`
|
||||
else if (props.media?.douban_id) return `douban:${props.media?.douban_id}`
|
||||
else return `bangumi:${props.media?.bangumi_id}`
|
||||
}
|
||||
|
||||
// 订阅弹窗选择的多季
|
||||
@@ -97,7 +97,6 @@ async function handleAddSubscribe() {
|
||||
$toast.error(`${props.media?.title} 查询剧集信息失败!`)
|
||||
return
|
||||
}
|
||||
|
||||
// 检查各季的缺失状态
|
||||
await checkSeasonsNotExists()
|
||||
if (!tmdbFlag.value) return
|
||||
@@ -174,7 +173,7 @@ function showSubscribeAddToast(result: boolean, title: string, season: number, m
|
||||
let subname = '订阅'
|
||||
if (best_version > 0) subname = '洗版订阅'
|
||||
|
||||
if (result && seasonsSelected.value.length > 1) $toast.success(`${title} 添加${subname}成功!`)
|
||||
if (result) $toast.success(`${title} 添加${subname}成功!`)
|
||||
else if (!result) $toast.error(`${title} 添加${subname}失败:${message}!`)
|
||||
}
|
||||
|
||||
@@ -199,8 +198,9 @@ async function removeSubscribe() {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
doneNProgress()
|
||||
}
|
||||
doneNProgress()
|
||||
}
|
||||
|
||||
// 查询当前媒体是否已订阅
|
||||
@@ -252,7 +252,7 @@ async function checkSubscribe(season = 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
// 检查所有季的缺失状态
|
||||
// 检查所有季的缺失状态(数据库)
|
||||
async function checkSeasonsNotExists() {
|
||||
// 开始处理
|
||||
startNProgress()
|
||||
@@ -271,10 +271,10 @@ async function checkSeasonsNotExists() {
|
||||
} catch (error) {
|
||||
$toast.error(`${props.media?.title}无法识别TMDB媒体信息!`)
|
||||
tmdbFlag.value = false
|
||||
} finally {
|
||||
// 处理完成
|
||||
doneNProgress()
|
||||
}
|
||||
|
||||
// 处理完成
|
||||
doneNProgress()
|
||||
}
|
||||
|
||||
// 查询TMDB的所有季信息
|
||||
@@ -288,6 +288,8 @@ async function getMediaSeasons() {
|
||||
|
||||
// 查询订阅弹窗规则
|
||||
async function queryDefaultSubscribeConfig() {
|
||||
// 非管理员不显示
|
||||
if (!store.state.auth.superUser) return false
|
||||
try {
|
||||
let subscribe_config_url = ''
|
||||
if (props.media?.type === '电影') subscribe_config_url = 'system/setting/DefaultMovieSubscribeConfig'
|
||||
@@ -464,7 +466,7 @@ function getYear(airDate: string) {
|
||||
density="compact"
|
||||
class="absolute bottom-1 right-1"
|
||||
tile
|
||||
v-if="!hover.isHovering && isImageLoaded"
|
||||
v-if="!hover.isHovering && isImageLoaded && props.media?.source"
|
||||
>
|
||||
<VImg cover :src="sourceIconDict[props.media?.source]" class="shadow-lg" />
|
||||
</VAvatar>
|
||||
@@ -475,7 +477,10 @@ function getYear(airDate: string) {
|
||||
<VBottomSheet v-if="subscribeSeasonDialog" v-model="subscribeSeasonDialog" inset scrollable>
|
||||
<VCard class="rounded-t">
|
||||
<DialogCloseBtn @click="subscribeSeasonDialog = false" />
|
||||
<VCardTitle class="pe-10"> 订阅 - {{ props.media?.title }} </VCardTitle>
|
||||
<VCardItem>
|
||||
<VCardTitle class="pe-10"> 订阅 - {{ props.media?.title }} </VCardTitle>
|
||||
</VCardItem>
|
||||
<VDivider />
|
||||
<VCardText>
|
||||
<VList v-model:selected="seasonsSelected" lines="three" select-strategy="classic">
|
||||
<VListItem v-for="(item, i) in seasonInfos" :key="i" :value="item">
|
||||
|
||||
91
src/components/cards/MediaDirectoryCard.vue
Normal file
91
src/components/cards/MediaDirectoryCard.vue
Normal file
@@ -0,0 +1,91 @@
|
||||
<script lang="ts" setup>
|
||||
import type { MediaDirectory } from '@/api/types'
|
||||
import { VTextField } from 'vuetify/lib/components/index.mjs'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
type: String, // download/library
|
||||
directory: {
|
||||
type: Object as PropType<MediaDirectory>,
|
||||
required: true, // 必填参数
|
||||
},
|
||||
categories: {
|
||||
type: Object as PropType<{ [key: string]: any }>,
|
||||
required: true,
|
||||
},
|
||||
width: String,
|
||||
height: String,
|
||||
})
|
||||
|
||||
// 类型下拉字典
|
||||
const typeItems = [
|
||||
{ title: '全部', value: '' },
|
||||
{ title: '电影', value: '电影' },
|
||||
{ title: '电视剧', value: '电视剧' },
|
||||
]
|
||||
|
||||
// 定义触发的自定义事件
|
||||
const emit = defineEmits(['close', 'changed'])
|
||||
|
||||
// 按钮点击
|
||||
function onClose() {
|
||||
emit('close')
|
||||
}
|
||||
|
||||
// 根据选中的媒体类型,获取对应的媒体类别
|
||||
const getCategories = computed(() => {
|
||||
const default_value = [{ title: '全部', value: '' }]
|
||||
if (!props.categories || !props.categories[props.directory?.media_type ?? '']) return default_value
|
||||
return default_value.concat(props.categories[props.directory.media_type ?? ''])
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard variant="tonal" :width="props.width" :height="props.height">
|
||||
<DialogCloseBtn @click="onClose" />
|
||||
<VCardItem>
|
||||
<VTextField
|
||||
v-model="props.directory.name"
|
||||
variant="underlined"
|
||||
label="别名"
|
||||
class="me-20 text-high-emphasis font-weight-bold"
|
||||
/>
|
||||
<span class="absolute top-3 right-12">
|
||||
<IconBtn>
|
||||
<VIcon class="cursor-move" icon="mdi-drag" />
|
||||
</IconBtn>
|
||||
</span>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VForm>
|
||||
<VRow>
|
||||
<VCol>
|
||||
<VTextField v-model="props.directory.path" variant="underlined" label="路径" />
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="4">
|
||||
<VSelect
|
||||
v-model="props.directory.media_type"
|
||||
variant="underlined"
|
||||
:items="typeItems"
|
||||
label="媒体类型"
|
||||
@update:modelValue="props.directory.category = ''"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol>
|
||||
<VSelect v-model="props.directory.category" variant="underlined" :items="getCategories" label="媒体类别" />
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol v-if="!props.directory.category || props.directory.category === ''">
|
||||
<VSwitch v-model="props.directory.auto_category" label="自动分类"></VSwitch>
|
||||
</VCol>
|
||||
<VCol v-if="type === 'library'">
|
||||
<VSwitch v-model="props.directory.scrape" label="刮削元数据"></VSwitch>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</template>
|
||||
@@ -149,8 +149,8 @@ const dropdownItems = ref([
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard :width="props.width" :height="props.height" @click="installPlugin">
|
||||
<div class="relative pa-4 text-center card-cover-blurred" :style="{ background: `${backgroundColor}` }">
|
||||
<VCard :width="props.width" :height="props.height" @click="installPlugin" class="flex flex-col">
|
||||
<div class="relative pa-3 text-center card-cover-blurred" :style="{ background: `${backgroundColor}` }">
|
||||
<div class="me-n3 absolute top-0 right-3">
|
||||
<IconBtn>
|
||||
<VIcon icon="mdi-dots-vertical" class="text-white" />
|
||||
@@ -172,7 +172,7 @@ const dropdownItems = ref([
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</div>
|
||||
<VAvatar size="8rem">
|
||||
<VAvatar size="6rem">
|
||||
<VImg
|
||||
ref="imageRef"
|
||||
:src="iconPath"
|
||||
@@ -196,7 +196,7 @@ const dropdownItems = ref([
|
||||
</VChip>
|
||||
</div>
|
||||
</VCardText>
|
||||
<VCardText class="flex items-center justify-start pb-2">
|
||||
<VCardText class="flex align-self-baseline pb-2 w-full align-end">
|
||||
<span>
|
||||
<VIcon icon="mdi-account" class="me-1" />
|
||||
<a :href="props.plugin?.author_url" target="_blank" @click.stop>
|
||||
@@ -213,9 +213,9 @@ const dropdownItems = ref([
|
||||
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" />
|
||||
<!-- 更新日志 -->
|
||||
<VDialog v-if="releaseDialog" v-model="releaseDialog" width="600" scrollable>
|
||||
<VCard>
|
||||
<VCard :title="`${props.plugin?.plugin_name} 更新说明`">
|
||||
<DialogCloseBtn @click="releaseDialog = false" />
|
||||
<VCardTitle>{{ props.plugin?.plugin_name }} 更新说明</VCardTitle>
|
||||
<VDivider />
|
||||
<VersionHistory :history="props.plugin?.history" />
|
||||
</VCard>
|
||||
</VDialog>
|
||||
|
||||
@@ -108,14 +108,6 @@ async function uninstallPlugin() {
|
||||
const isConfirmed = await createConfirm({
|
||||
title: '确认',
|
||||
content: `是否确认卸载插件 ${props.plugin?.plugin_name} ?`,
|
||||
confirmationText: '确认',
|
||||
cancellationText: '取消',
|
||||
dialogProps: {
|
||||
maxWidth: '50rem',
|
||||
},
|
||||
confirmationButtonProps: {
|
||||
variant: 'tonal',
|
||||
},
|
||||
})
|
||||
|
||||
if (!isConfirmed) return
|
||||
@@ -229,14 +221,6 @@ async function resetPlugin() {
|
||||
const isConfirmed = await createConfirm({
|
||||
title: '确认',
|
||||
content: `是否确认重置插件 ${props.plugin?.plugin_name} 的配置数据?`,
|
||||
confirmationText: '确认',
|
||||
cancellationText: '取消',
|
||||
dialogProps: {
|
||||
maxWidth: '50rem',
|
||||
},
|
||||
confirmationButtonProps: {
|
||||
variant: 'tonal',
|
||||
},
|
||||
})
|
||||
|
||||
if (!isConfirmed) return
|
||||
@@ -390,8 +374,8 @@ watch(
|
||||
|
||||
<template>
|
||||
<!-- 插件卡片 -->
|
||||
<VCard v-if="isVisible" :width="props.width" :height="props.height" @click="openPluginDetail">
|
||||
<div class="relative pa-4 text-center card-cover-blurred" :style="{ background: `${backgroundColor}` }">
|
||||
<VCard v-if="isVisible" :width="props.width" :height="props.height" @click="openPluginDetail" class="flex flex-col">
|
||||
<div class="relative pa-3 text-center card-cover-blurred" :style="{ background: `${backgroundColor}` }">
|
||||
<div v-if="props.plugin?.has_update" class="me-n3 absolute top-0 left-1">
|
||||
<VIcon icon="mdi-new-box" class="text-white" />
|
||||
</div>
|
||||
@@ -417,7 +401,7 @@ watch(
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</div>
|
||||
<VAvatar size="8rem">
|
||||
<VAvatar size="6rem">
|
||||
<VImg
|
||||
ref="imageRef"
|
||||
:src="iconPath"
|
||||
@@ -429,33 +413,38 @@ watch(
|
||||
/>
|
||||
</VAvatar>
|
||||
</div>
|
||||
<span v-if="props.count" class="absolute bottom-1 right-2 flex items-center">
|
||||
<VIcon icon="mdi-fire" />
|
||||
<span class="text-sm ms-1">{{ props.count?.toLocaleString() }}</span>
|
||||
</span>
|
||||
<VCardItem class="py-2">
|
||||
<VCardTitle class="flex items-center flex-row">
|
||||
<VBadge v-if="props.plugin?.state" dot inline color="success" class="me-1 mb-1" />
|
||||
{{ props.plugin?.plugin_name
|
||||
}}<span class="text-sm ms-2 mt-1 text-gray-500">v{{ props.plugin?.plugin_version }}</span>
|
||||
{{ props.plugin?.plugin_name }}
|
||||
<span class="text-sm ms-2 mt-1 text-gray-500">v{{ props.plugin?.plugin_version }}</span>
|
||||
</VCardTitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VCardText class="pb-1">
|
||||
{{ props.plugin?.plugin_desc }}
|
||||
</VCardText>
|
||||
<VCardText class="flex justify-end align-self-baseline p-1 w-full align-end">
|
||||
<span v-if="props.count" class="ms-3">
|
||||
<VIcon icon="mdi-fire" />
|
||||
<span class="text-sm ms-1">{{ props.count?.toLocaleString() }}</span>
|
||||
</span>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<!-- 插件配置页面 -->
|
||||
<VDialog v-model="pluginConfigDialog" scrollable max-width="60rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VCard :title="`${props.plugin?.plugin_name} - 配置`" class="rounded-t">
|
||||
<DialogCloseBtn v-model="pluginConfigDialog" />
|
||||
<VDivider />
|
||||
<VCardText>
|
||||
<FormRender v-for="(item, index) in pluginFormItems" :key="index" :config="item" :form="pluginConfigForm" />
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VBtn v-if="pluginPageItems.length > 0" @click="showPluginInfo"> 查看数据 </VBtn>
|
||||
<VCardActions class="pt-3">
|
||||
<VBtn v-if="pluginPageItems.length > 0" @click="showPluginInfo" variant="outlined" color="info">
|
||||
查看数据
|
||||
</VBtn>
|
||||
<VSpacer />
|
||||
<VBtn variant="tonal" @click="savePluginConf"> 保存 </VBtn>
|
||||
<VBtn @click="savePluginConf" variant="elevated" prepend-icon="mdi-content-save" class="px-5"> 保存 </VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
@@ -464,14 +453,10 @@ watch(
|
||||
<VDialog v-model="pluginInfoDialog" scrollable max-width="80rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VCard :title="`${props.plugin?.plugin_name}`" class="rounded-t">
|
||||
<DialogCloseBtn v-model="pluginInfoDialog" />
|
||||
<VCardText>
|
||||
<VCardText class="min-h-40">
|
||||
<PageRender @action="loadPluginPage" v-for="(item, index) in pluginPageItems" :key="index" :config="item" />
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VBtn @click="showPluginConfig"> 配置 </VBtn>
|
||||
<VSpacer />
|
||||
<VBtn variant="tonal" @click="pluginInfoDialog = false"> 关闭 </VBtn>
|
||||
</VCardActions>
|
||||
<VFab icon="mdi-cog" location="bottom end" size="x-large" fixed app appear @click="showPluginConfig" />
|
||||
</VCard>
|
||||
</VDialog>
|
||||
|
||||
@@ -480,10 +465,11 @@ watch(
|
||||
|
||||
<!-- 更新日志 -->
|
||||
<VDialog v-if="releaseDialog" v-model="releaseDialog" width="600" scrollable>
|
||||
<VCard>
|
||||
<VCard :title="`${props.plugin?.plugin_name} 更新说明`">
|
||||
<DialogCloseBtn @click="releaseDialog = false" />
|
||||
<VCardTitle>{{ props.plugin?.plugin_name }} 更新说明</VCardTitle>
|
||||
<VDivider />
|
||||
<VersionHistory :history="props.plugin?.history" />
|
||||
<VDivider />
|
||||
<VCardText>
|
||||
<VBtn @click="updatePlugin" block>
|
||||
<template #prepend>
|
||||
|
||||
@@ -18,26 +18,21 @@ const imageLoadError = ref(false)
|
||||
|
||||
// 角标颜色
|
||||
function getChipColor(type: string) {
|
||||
if (type === '电影')
|
||||
return 'border-blue-500 bg-blue-600'
|
||||
else if (type === '电视剧')
|
||||
return ' bg-indigo-500 border-indigo-600'
|
||||
else
|
||||
return 'border-purple-600 bg-purple-600'
|
||||
if (type === '电影') return 'border-blue-500 bg-blue-600'
|
||||
else if (type === '电视剧') return ' bg-indigo-500 border-indigo-600'
|
||||
else return 'border-purple-600 bg-purple-600'
|
||||
}
|
||||
|
||||
// 计算图片地址
|
||||
const getImgUrl = computed(() => {
|
||||
if (imageLoadError.value)
|
||||
return noImage
|
||||
if (imageLoadError.value) return noImage
|
||||
const image = props.media?.image || ''
|
||||
return `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(image)}`
|
||||
})
|
||||
|
||||
// 跳转播放
|
||||
function goPlay(isHovering = false) {
|
||||
if (props.media?.link && isHovering)
|
||||
window.open(props.media?.link, '_blank')
|
||||
if (props.media?.link && isHovering) window.open(props.media?.link, '_blank')
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -72,24 +67,24 @@ function goPlay(isHovering = false) {
|
||||
</VImg>
|
||||
<!-- 类型角标 -->
|
||||
<VChip
|
||||
v-show="isImageLoaded"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
:class="getChipColor(props.media?.type || '')"
|
||||
class="absolute left-2 top-2 bg-opacity-80 shadow-md text-white font-bold"
|
||||
>
|
||||
{{ props.media?.type }}
|
||||
</VChip>
|
||||
<!-- 详情 -->
|
||||
<VCardText
|
||||
v-show="hover.isHovering || imageLoadError"
|
||||
class="w-full flex flex-col flex-wrap justify-end align-left text-white absolute bottom-0 cursor-pointer pa-2"
|
||||
>
|
||||
<span class="font-bold">{{ props.media?.subtitle }}</span>
|
||||
<h1 class="mb-1 text-white font-extrabold text-xl line-clamp-2 overflow-hidden text-ellipsis ...">
|
||||
{{ props.media?.title }}
|
||||
</h1>
|
||||
</VCardText>
|
||||
v-show="isImageLoaded"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
:class="getChipColor(props.media?.type || '')"
|
||||
class="absolute left-2 top-2 bg-opacity-80 shadow-md text-white font-bold"
|
||||
>
|
||||
{{ props.media?.type }}
|
||||
</VChip>
|
||||
<!-- 详情 -->
|
||||
<VCardText
|
||||
v-show="hover.isHovering || imageLoadError"
|
||||
class="w-full flex flex-col flex-wrap justify-end align-left text-white absolute bottom-0 cursor-pointer pa-2"
|
||||
>
|
||||
<span class="font-bold">{{ props.media?.subtitle }}</span>
|
||||
<h1 class="mb-1 text-white font-extrabold text-xl line-clamp-2 overflow-hidden text-ellipsis ...">
|
||||
{{ props.media?.title }}
|
||||
</h1>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</template>
|
||||
</VHover>
|
||||
|
||||
@@ -167,6 +167,12 @@ watch(resourceDialog, value => {
|
||||
if (!value) getSiteStats()
|
||||
})
|
||||
|
||||
// 保存站点
|
||||
function saveSite() {
|
||||
siteEditDialog.value = false
|
||||
emit('update')
|
||||
}
|
||||
|
||||
// 装载时查询站点图标
|
||||
onMounted(() => {
|
||||
getSiteIcon()
|
||||
@@ -175,149 +181,142 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard
|
||||
:height="cardProps.height"
|
||||
:width="cardProps.width"
|
||||
:variant="cardProps.site?.is_active ? 'elevated' : 'outlined'"
|
||||
class="overflow-hidden"
|
||||
@click="siteEditDialog = true"
|
||||
>
|
||||
<template #image>
|
||||
<VAvatar class="absolute right-2 bottom-2 rounded" variant="flat" rounded="0">
|
||||
<VImg :src="siteIcon" />
|
||||
</VAvatar>
|
||||
</template>
|
||||
|
||||
<VCardItem>
|
||||
<VCardTitle class="font-bold">
|
||||
<span @click.stop="openSitePage">{{ cardProps.site?.name }}</span>
|
||||
</VCardTitle>
|
||||
<VCardSubtitle>
|
||||
<span @click.stop="openSitePage">{{ cardProps.site?.url }}</span>
|
||||
</VCardSubtitle>
|
||||
</VCardItem>
|
||||
|
||||
<StatIcon v-if="cardProps.site?.is_active" :color="statColor" />
|
||||
|
||||
<VCardText class="py-2">
|
||||
<VTooltip v-if="cardProps.site?.render === 1" text="浏览器仿真">
|
||||
<template #activator="{ props }">
|
||||
<VIcon color="primary" class="me-2" v-bind="props" icon="mdi-apple-safari" />
|
||||
</template>
|
||||
</VTooltip>
|
||||
|
||||
<VTooltip v-if="cardProps.site?.proxy === 1" text="代理">
|
||||
<template #activator="{ props }">
|
||||
<VIcon color="primary" class="me-2" v-bind="props" icon="mdi-network-outline" />
|
||||
</template>
|
||||
</VTooltip>
|
||||
|
||||
<VTooltip v-if="cardProps.site?.limit_interval" text="流控">
|
||||
<template #activator="{ props }">
|
||||
<VIcon color="primary" class="me-2" v-bind="props" icon="mdi-speedometer" />
|
||||
</template>
|
||||
</VTooltip>
|
||||
|
||||
<VTooltip v-if="cardProps.site?.filter" text="过滤">
|
||||
<template #activator="{ props }">
|
||||
<VIcon color="primary" class="me-2" v-bind="props" icon="mdi-filter-cog-outline" />
|
||||
</template>
|
||||
</VTooltip>
|
||||
</VCardText>
|
||||
|
||||
<VDivider class="opacity-75" style="border-color: rgba(var(--v-theme-on-background), var(--v-selected-opacity))" />
|
||||
|
||||
<VCardActions>
|
||||
<VBtn v-if="!cardProps.site?.public" :disabled="updateButtonDisable" @click.stop="handleSiteUpdate">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-refresh" />
|
||||
</template>
|
||||
更新
|
||||
</VBtn>
|
||||
<VBtn :disabled="testButtonDisable" @click.stop="testSite">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-link" />
|
||||
</template>
|
||||
{{ testButtonText }}
|
||||
</VBtn>
|
||||
<VBtn @click.stop="handleResourceBrowse">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-web" />
|
||||
</template>
|
||||
浏览
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
<!-- 更新站点Cookie & UA弹窗 -->
|
||||
<VDialog v-model="siteCookieDialog" max-width="50rem">
|
||||
<!-- Dialog Content -->
|
||||
<VCard title="更新站点Cookie & UA">
|
||||
<DialogCloseBtn @click="siteCookieDialog = false" />
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<VRow>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField v-model="userPwForm.username" label="用户名" :rules="[requiredValidator]" />
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="userPwForm.password"
|
||||
label="密码"
|
||||
:type="isPasswordVisible ? 'text' : 'password'"
|
||||
:append-inner-icon="isPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
|
||||
:rules="[requiredValidator]"
|
||||
@click:append-inner="isPasswordVisible = !isPasswordVisible"
|
||||
@keydown.enter="updateSiteCookie"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField v-model="userPwForm.code" label="两步验证" />
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
<div>
|
||||
<VCard
|
||||
:height="cardProps.height"
|
||||
:width="cardProps.width"
|
||||
:variant="cardProps.site?.is_active ? 'elevated' : 'outlined'"
|
||||
class="overflow-hidden"
|
||||
@click="siteEditDialog = true"
|
||||
>
|
||||
<template #image>
|
||||
<VAvatar class="absolute right-2 bottom-2 rounded" variant="flat" rounded="0">
|
||||
<VImg :src="siteIcon" />
|
||||
</VAvatar>
|
||||
</template>
|
||||
<VCardItem>
|
||||
<VCardTitle class="font-bold">
|
||||
<span @click.stop="openSitePage">{{ cardProps.site?.name }}</span>
|
||||
</VCardTitle>
|
||||
<VCardSubtitle>
|
||||
<span @click.stop="openSitePage">{{ cardProps.site?.url }}</span>
|
||||
</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText class="py-2">
|
||||
<VTooltip v-if="cardProps.site?.render === 1" text="浏览器仿真">
|
||||
<template #activator="{ props }">
|
||||
<VIcon color="primary" class="me-2" v-bind="props" icon="mdi-apple-safari" />
|
||||
</template>
|
||||
</VTooltip>
|
||||
<VTooltip v-if="cardProps.site?.proxy === 1" text="代理">
|
||||
<template #activator="{ props }">
|
||||
<VIcon color="primary" class="me-2" v-bind="props" icon="mdi-network-outline" />
|
||||
</template>
|
||||
</VTooltip>
|
||||
<VTooltip v-if="cardProps.site?.limit_interval" text="流控">
|
||||
<template #activator="{ props }">
|
||||
<VIcon color="primary" class="me-2" v-bind="props" icon="mdi-speedometer" />
|
||||
</template>
|
||||
</VTooltip>
|
||||
<VTooltip v-if="cardProps.site?.filter" text="过滤">
|
||||
<template #activator="{ props }">
|
||||
<VIcon color="primary" class="me-2" v-bind="props" icon="mdi-filter-cog-outline" />
|
||||
</template>
|
||||
</VTooltip>
|
||||
</VCardText>
|
||||
|
||||
<VDivider />
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
<VBtn variant="tonal" @click="updateSiteCookie"> 开始更新 </VBtn>
|
||||
<VBtn v-if="!cardProps.site?.public" :disabled="updateButtonDisable" @click.stop="handleSiteUpdate">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-refresh" />
|
||||
</template>
|
||||
更新
|
||||
</VBtn>
|
||||
<VBtn :disabled="testButtonDisable" @click.stop="testSite">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-link" />
|
||||
</template>
|
||||
{{ testButtonText }}
|
||||
</VBtn>
|
||||
<VBtn @click.stop="handleResourceBrowse">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-web" />
|
||||
</template>
|
||||
浏览
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
<StatIcon v-if="cardProps.site?.is_active" :color="statColor" />
|
||||
<span class="absolute top-1 right-8">
|
||||
<VIcon class="cursor-move">mdi-drag</VIcon>
|
||||
</span>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
<SiteAddEditDialog
|
||||
v-if="siteEditDialog"
|
||||
v-model="siteEditDialog"
|
||||
:siteid="cardProps.site?.id"
|
||||
@save="
|
||||
() => {
|
||||
siteEditDialog = false
|
||||
emit('update')
|
||||
}
|
||||
"
|
||||
@remove="emit('remove')"
|
||||
@close="siteEditDialog = false"
|
||||
/>
|
||||
<!-- 站点资源弹窗 -->
|
||||
<VDialog
|
||||
v-if="resourceDialog"
|
||||
v-model="resourceDialog"
|
||||
max-width="80rem"
|
||||
scrollable
|
||||
z-index="1010"
|
||||
:fullscreen="!display.mdAndUp.value"
|
||||
>
|
||||
<!-- Dialog Content -->
|
||||
<VCard :title="`浏览站点 - ${cardProps.site?.name}`">
|
||||
<DialogCloseBtn @click="resourceDialog = false" />
|
||||
<VCardText class="pt-2">
|
||||
<SiteTorrentTable :site="cardProps.site?.id" />
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
<!-- 进度框 -->
|
||||
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" />
|
||||
<!-- 更新站点Cookie & UA弹窗 -->
|
||||
<VDialog v-model="siteCookieDialog" max-width="50rem">
|
||||
<!-- Dialog Content -->
|
||||
<VCard title="更新站点Cookie & UA">
|
||||
<DialogCloseBtn @click="siteCookieDialog = false" />
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<VRow>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField v-model="userPwForm.username" label="用户名" :rules="[requiredValidator]" />
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="userPwForm.password"
|
||||
label="密码"
|
||||
:type="isPasswordVisible ? 'text' : 'password'"
|
||||
:append-inner-icon="isPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
|
||||
:rules="[requiredValidator]"
|
||||
@click:append-inner="isPasswordVisible = !isPasswordVisible"
|
||||
@keydown.enter="updateSiteCookie"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField v-model="userPwForm.code" label="两步验证" />
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
<VBtn variant="elevated" @click="updateSiteCookie" prepend-icon="mdi-refresh" class="px-5"> 开始更新 </VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
<!-- 站点编辑弹窗 -->
|
||||
<SiteAddEditDialog
|
||||
v-if="siteEditDialog"
|
||||
v-model="siteEditDialog"
|
||||
:siteid="cardProps.site?.id"
|
||||
@save="saveSite"
|
||||
@remove="emit('remove')"
|
||||
@close="siteEditDialog = false"
|
||||
/>
|
||||
<!-- 站点资源弹窗 -->
|
||||
<VDialog
|
||||
v-if="resourceDialog"
|
||||
v-model="resourceDialog"
|
||||
max-width="80rem"
|
||||
scrollable
|
||||
z-index="1010"
|
||||
:fullscreen="!display.mdAndUp.value"
|
||||
>
|
||||
<VCard :title="`浏览站点 - ${cardProps.site?.name}`">
|
||||
<DialogCloseBtn @click="resourceDialog = false" />
|
||||
<VDivider />
|
||||
<VCardText class="pt-2">
|
||||
<SiteTorrentTable :site="cardProps.site?.id" />
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
<!-- 进度框 -->
|
||||
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
<style lang="scss" scoped>
|
||||
.v-table th {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<script lang='ts' setup>
|
||||
<script lang="ts" setup>
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import SubscribeEditDialog from '../dialog/SubscribeEditDialog.vue'
|
||||
import { formatDateDifference } from '@/@core/utils/formatters'
|
||||
@@ -25,11 +25,7 @@ const imageLoaded = ref(false)
|
||||
const subscribeEditDialog = ref(false)
|
||||
|
||||
// 上一次更新时间
|
||||
const lastUpdateText = ref(
|
||||
props.media && props.media.last_update
|
||||
? formatDateDifference(props.media.last_update)
|
||||
: '',
|
||||
)
|
||||
const lastUpdateText = ref(props.media && props.media.last_update ? formatDateDifference(props.media.last_update) : '')
|
||||
|
||||
// 图片加载完成响应
|
||||
function imageLoadHandler() {
|
||||
@@ -38,23 +34,17 @@ function imageLoadHandler() {
|
||||
|
||||
// 根据 type 返回不同的图标
|
||||
function getIcon() {
|
||||
if (props.media?.type === '电影')
|
||||
return 'mdi-movie'
|
||||
else if (props.media?.type === '电视剧')
|
||||
return 'mdi-television-classic'
|
||||
else
|
||||
return 'mdi-help-circle'
|
||||
if (props.media?.type === '电影') return 'mdi-movie'
|
||||
else if (props.media?.type === '电视剧') return 'mdi-television-classic'
|
||||
else return 'mdi-help-circle'
|
||||
}
|
||||
|
||||
// 计算百分比
|
||||
function getPercentage() {
|
||||
if (props.media?.total_episode === 0)
|
||||
return 0
|
||||
if (props.media?.total_episode === 0) return 0
|
||||
|
||||
return Math.round(
|
||||
(((props.media?.total_episode ?? 0) - (props.media?.lack_episode ?? 0))
|
||||
/ (props.media?.total_episode ?? 1))
|
||||
* 100,
|
||||
(((props.media?.total_episode ?? 0) - (props.media?.lack_episode ?? 0)) / (props.media?.total_episode ?? 1)) * 100,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -71,16 +61,13 @@ function getTextClass() {
|
||||
// 删除订阅
|
||||
async function removeSubscribe() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.delete(
|
||||
`subscribe/${props.media?.id}`,
|
||||
)
|
||||
const result: { [key: string]: any } = await api.delete(`subscribe/${props.media?.id}`)
|
||||
|
||||
if (result.success) {
|
||||
// 通知父组件刷新
|
||||
emit('remove')
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
@@ -88,15 +75,11 @@ async function removeSubscribe() {
|
||||
// 搜索订阅
|
||||
async function searchSubscribe() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get(
|
||||
`subscribe/search/${props.media?.id}`,
|
||||
)
|
||||
const result: { [key: string]: any } = await api.get(`subscribe/search/${props.media?.id}`)
|
||||
|
||||
// 提示
|
||||
if (result.success)
|
||||
$toast.success(`${props.media?.name} 提交搜索请求成功!`)
|
||||
}
|
||||
catch (e) {
|
||||
if (result.success) $toast.success(`${props.media?.name} 提交搜索请求成功!`)
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
@@ -133,11 +116,7 @@ const dropdownItems = ref([
|
||||
router.push({
|
||||
path: '/media',
|
||||
query: {
|
||||
mediaid: `${
|
||||
props.media?.tmdbid
|
||||
? `tmdb:${props.media?.tmdbid}`
|
||||
: `douban:${props.media?.doubanid}`
|
||||
}`,
|
||||
mediaid: `${props.media?.tmdbid ? `tmdb:${props.media?.tmdbid}` : `douban:${props.media?.doubanid}`}`,
|
||||
type: props.media?.type,
|
||||
},
|
||||
})
|
||||
@@ -157,137 +136,108 @@ const dropdownItems = ref([
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard
|
||||
:key="props.media?.id"
|
||||
:class="`${props.media?.best_version ? 'outline-dashed outline-1' : ''}`"
|
||||
@click="editSubscribeDialog"
|
||||
>
|
||||
<template #image>
|
||||
<VImg
|
||||
:src="props.media?.backdrop || props.media?.poster"
|
||||
aspect-ratio="2/3"
|
||||
cover
|
||||
class="brightness-50"
|
||||
@load="imageLoadHandler"
|
||||
/>
|
||||
</template>
|
||||
<VCardItem>
|
||||
<template #prepend>
|
||||
<VIcon
|
||||
size="1.9rem"
|
||||
:color="getTextColor()"
|
||||
:icon="getIcon()"
|
||||
/>
|
||||
</template>
|
||||
<VCardTitle :class="getTextClass()">
|
||||
{{ props.media?.name }}
|
||||
{{ formatSeason(props.media?.season ? props.media?.season.toString() : '') }}
|
||||
</VCardTitle>
|
||||
<template #append>
|
||||
<div class="me-n3">
|
||||
<IconBtn>
|
||||
<VIcon
|
||||
icon="mdi-dots-vertical"
|
||||
:color="getTextColor()"
|
||||
/>
|
||||
<VMenu
|
||||
activator="parent"
|
||||
close-on-content-click
|
||||
>
|
||||
<VList>
|
||||
<VListItem
|
||||
v-for="(item, i) in dropdownItems"
|
||||
:key="i"
|
||||
variant="plain"
|
||||
:base-color="item.props.color"
|
||||
@click="item.props.click"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon :icon="item.props.prependIcon" />
|
||||
</template>
|
||||
<VListItemTitle v-text="item.title" />
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</div>
|
||||
</template>
|
||||
</VCardItem>
|
||||
|
||||
<VCardText>
|
||||
<p
|
||||
class="clamp-text mb-0"
|
||||
:class="getTextClass()"
|
||||
<VHover>
|
||||
<template #default="hover">
|
||||
<VCard
|
||||
v-bind="hover.props"
|
||||
:key="props.media?.id"
|
||||
class="flex flex-col"
|
||||
:class="{
|
||||
'outline-dashed outline-1': props.media?.best_version,
|
||||
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
|
||||
}"
|
||||
@click="editSubscribeDialog"
|
||||
>
|
||||
{{ props.media?.description }}
|
||||
</p>
|
||||
</VCardText>
|
||||
|
||||
<VCardText class="d-flex justify-space-between align-center flex-wrap">
|
||||
<div class="d-flex align-center">
|
||||
<IconBtn
|
||||
icon="mdi-star"
|
||||
:color="getTextColor()"
|
||||
class="me-1"
|
||||
/>
|
||||
<span
|
||||
class="text-subtitle-2 me-4"
|
||||
:class="getTextClass()"
|
||||
>{{
|
||||
props.media?.vote
|
||||
}}</span>
|
||||
<IconBtn
|
||||
v-if="props.media?.total_episode"
|
||||
v-bind="props"
|
||||
icon="mdi-progress-clock"
|
||||
:color="getTextColor()"
|
||||
class="me-1"
|
||||
/>
|
||||
<span
|
||||
v-if="props.media?.season"
|
||||
class="text-subtitle-2 me-4"
|
||||
:class="getTextClass()"
|
||||
>{{ (props.media?.total_episode || 0) - (props.media?.lack_episode || 0) }} /
|
||||
{{ props.media?.total_episode }}</span>
|
||||
<IconBtn
|
||||
v-if="props.media?.username"
|
||||
icon="mdi-account"
|
||||
:color="getTextColor()"
|
||||
class="me-1"
|
||||
/>
|
||||
<span
|
||||
v-if="props.media?.username"
|
||||
class="text-subtitle-2 me-4"
|
||||
:class="getTextClass()"
|
||||
>
|
||||
{{ props.media?.username }}
|
||||
</span>
|
||||
</div>
|
||||
</VCardText>
|
||||
<VCardText
|
||||
v-if="lastUpdateText"
|
||||
class="absolute right-0 bottom-0 d-flex align-center p-2 text-gray-300"
|
||||
>
|
||||
<VIcon
|
||||
icon="mdi-download"
|
||||
class="me-1"
|
||||
/>
|
||||
{{ lastUpdateText }}
|
||||
</VCardText>
|
||||
<VProgressLinear
|
||||
v-if="getPercentage() > 0"
|
||||
:model-value="getPercentage()"
|
||||
bg-color="success"
|
||||
color="success"
|
||||
/>
|
||||
</VCard>
|
||||
<template #image>
|
||||
<VImg
|
||||
:src="props.media?.backdrop || props.media?.poster"
|
||||
aspect-ratio="2/3"
|
||||
cover
|
||||
class="brightness-50"
|
||||
@load="imageLoadHandler"
|
||||
/>
|
||||
</template>
|
||||
<VCardItem>
|
||||
<template #prepend>
|
||||
<VIcon size="1.9rem" :color="getTextColor()" :icon="getIcon()" />
|
||||
</template>
|
||||
<VCardTitle :class="getTextClass()">
|
||||
{{ props.media?.name }}
|
||||
{{ formatSeason(props.media?.season ? props.media?.season.toString() : '') }}
|
||||
</VCardTitle>
|
||||
<template #append>
|
||||
<div class="me-n3">
|
||||
<IconBtn>
|
||||
<VIcon icon="mdi-dots-vertical" :color="getTextColor()" />
|
||||
<VMenu activator="parent" close-on-content-click>
|
||||
<VList>
|
||||
<VListItem
|
||||
v-for="(item, i) in dropdownItems"
|
||||
:key="i"
|
||||
variant="plain"
|
||||
:base-color="item.props.color"
|
||||
@click="item.props.click"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon :icon="item.props.prependIcon" />
|
||||
</template>
|
||||
<VListItemTitle v-text="item.title" />
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</div>
|
||||
</template>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<p class="clamp-text mb-0" :class="getTextClass()">
|
||||
{{ props.media?.description }}
|
||||
</p>
|
||||
</VCardText>
|
||||
<VCardText class="d-flex justify-space-between align-center flex-wrap">
|
||||
<div class="d-flex align-center">
|
||||
<IconBtn
|
||||
v-if="props.media?.total_episode"
|
||||
v-bind="props"
|
||||
icon="mdi-progress-clock"
|
||||
:color="getTextColor()"
|
||||
class="me-1"
|
||||
/>
|
||||
<span v-if="props.media?.season" class="text-subtitle-2 me-4" :class="getTextClass()"
|
||||
>{{ (props.media?.total_episode || 0) - (props.media?.lack_episode || 0) }} /
|
||||
{{ props.media?.total_episode }}</span
|
||||
>
|
||||
<IconBtn v-if="props.media?.username" icon="mdi-account" :color="getTextColor()" class="me-1" />
|
||||
<span v-if="props.media?.username" class="text-subtitle-2 me-4" :class="getTextClass()">
|
||||
{{ props.media?.username }}
|
||||
</span>
|
||||
</div>
|
||||
</VCardText>
|
||||
<VCardText v-if="lastUpdateText" class="absolute right-0 bottom-0 d-flex align-center p-2 text-gray-300">
|
||||
<VIcon icon="mdi-download" class="me-1" />
|
||||
{{ lastUpdateText }}
|
||||
</VCardText>
|
||||
<VProgressLinear v-if="getPercentage() > 0" :model-value="getPercentage()" bg-color="success" color="success" />
|
||||
</VCard>
|
||||
</template>
|
||||
</VHover>
|
||||
<!-- 订阅编辑弹窗 -->
|
||||
<SubscribeEditDialog
|
||||
v-if="subscribeEditDialog"
|
||||
v-model="subscribeEditDialog"
|
||||
:subid="props.media?.id"
|
||||
@remove="() => { emit('remove');subscribeEditDialog = false; }"
|
||||
@save="() => { emit('save');subscribeEditDialog = false; }"
|
||||
@remove="
|
||||
() => {
|
||||
emit('remove')
|
||||
subscribeEditDialog = false
|
||||
}
|
||||
"
|
||||
@save="
|
||||
() => {
|
||||
emit('save')
|
||||
subscribeEditDialog = false
|
||||
}
|
||||
"
|
||||
@close="subscribeEditDialog = false"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -43,16 +43,13 @@ const downloaded = ref<String[]>([])
|
||||
async function getSiteIcon() {
|
||||
try {
|
||||
siteIcon.value = (await api.get(`site/icon/${torrent?.value?.site}`)).data.icon
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 询问并添加下载
|
||||
async function handleAddDownload(_site: any = undefined,
|
||||
_media: any = undefined,
|
||||
_torrent: any = undefined) {
|
||||
async function handleAddDownload(_site: any = undefined, _media: any = undefined, _torrent: any = undefined) {
|
||||
if (!_media || !_torrent || !_site) {
|
||||
_site = torrent.value?.site_name
|
||||
_media = media.value
|
||||
@@ -62,18 +59,9 @@ async function handleAddDownload(_site: any = undefined,
|
||||
const isConfirmed = await createConfirm({
|
||||
title: '确认',
|
||||
content: `是否确认下载【${_site}】${_torrent?.title} ?`,
|
||||
confirmationText: '确认',
|
||||
cancellationText: '取消',
|
||||
dialogProps: {
|
||||
maxWidth: '50rem',
|
||||
},
|
||||
confirmationButtonProps: {
|
||||
variant: 'tonal',
|
||||
},
|
||||
})
|
||||
|
||||
if (!isConfirmed)
|
||||
return
|
||||
if (!isConfirmed) return
|
||||
|
||||
addDownload(_media, _torrent)
|
||||
}
|
||||
@@ -91,13 +79,11 @@ async function addDownload(_media: MediaInfo, _torrent: TorrentInfo) {
|
||||
// 添加下载成功
|
||||
$toast.success(`${_torrent?.site_name} ${_torrent?.title} 添加下载成功!`)
|
||||
downloaded.value.push(_torrent?.enclosure || '')
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// 添加下载失败
|
||||
$toast.error(`${_torrent?.site_name} ${_torrent?.title} 添加下载失败!`)
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
doneNProgress()
|
||||
@@ -115,14 +101,10 @@ async function downloadTorrentFile() {
|
||||
|
||||
// 促销Chip类
|
||||
function getVolumeFactorClass(downloadVolume: number, uploadVolume: number) {
|
||||
if (downloadVolume === 0)
|
||||
return 'text-white bg-lime-500'
|
||||
else if (downloadVolume < 1)
|
||||
return 'text-white bg-green-500'
|
||||
else if (uploadVolume !== 1)
|
||||
return 'text-white bg-sky-500'
|
||||
else
|
||||
return 'text-white bg-gray-500'
|
||||
if (downloadVolume === 0) return 'text-white bg-lime-500'
|
||||
else if (downloadVolume < 1) return 'text-white bg-green-500'
|
||||
else if (uploadVolume !== 1) return 'text-white bg-sky-500'
|
||||
else return 'text-white bg-gray-500'
|
||||
}
|
||||
|
||||
// 装载时查询站点图标
|
||||
@@ -138,15 +120,8 @@ onMounted(() => {
|
||||
:variant="downloaded.includes(torrent?.enclosure || '') ? 'outlined' : 'elevated'"
|
||||
@click="handleAddDownload"
|
||||
>
|
||||
<template
|
||||
v-if="!showMoreTorrents"
|
||||
#image
|
||||
>
|
||||
<VAvatar
|
||||
class="absolute right-2 bottom-2 rounded"
|
||||
variant="flat"
|
||||
rounded="0"
|
||||
>
|
||||
<template v-if="!showMoreTorrents" #image>
|
||||
<VAvatar class="absolute right-2 bottom-2 rounded" variant="flat" rounded="0">
|
||||
<VImg :src="siteIcon" />
|
||||
</VAvatar>
|
||||
</template>
|
||||
@@ -159,18 +134,10 @@ onMounted(() => {
|
||||
<template #append>
|
||||
<div class="me-n3">
|
||||
<IconBtn>
|
||||
<VIcon
|
||||
icon="mdi-dots-vertical"
|
||||
/>
|
||||
<VMenu
|
||||
activator="parent"
|
||||
close-on-content-click
|
||||
>
|
||||
<VIcon icon="mdi-dots-vertical" />
|
||||
<VMenu activator="parent" close-on-content-click>
|
||||
<VList>
|
||||
<VListItem
|
||||
variant="plain"
|
||||
@click="openTorrentDetail()"
|
||||
>
|
||||
<VListItem variant="plain" @click="openTorrentDetail()">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-information" />
|
||||
</template>
|
||||
@@ -196,25 +163,11 @@ onMounted(() => {
|
||||
{{ torrent?.title }}
|
||||
</VCardText>
|
||||
<VCardText>{{ torrent?.description }}</VCardText>
|
||||
<VCardItem
|
||||
v-if="torrent?.labels"
|
||||
class="pb-3 pt-0 pe-12"
|
||||
>
|
||||
<VChip
|
||||
v-if="torrent?.hit_and_run"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
class="me-1 mb-1 text-white bg-black"
|
||||
>
|
||||
<VCardItem v-if="torrent?.labels" class="pb-3 pt-0 pe-12">
|
||||
<VChip v-if="torrent?.hit_and_run" variant="elevated" size="small" class="me-1 mb-1 text-white bg-black">
|
||||
H&R
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="torrent?.freedate_diff"
|
||||
variant="elevated"
|
||||
color="secondary"
|
||||
size="small"
|
||||
class="me-1 mb-1"
|
||||
>
|
||||
<VChip v-if="torrent?.freedate_diff" variant="elevated" color="secondary" size="small" class="me-1 mb-1">
|
||||
{{ torrent?.freedate_diff }}
|
||||
</VChip>
|
||||
<VChip
|
||||
@@ -227,51 +180,24 @@ onMounted(() => {
|
||||
>
|
||||
{{ label }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="meta?.edition"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
class="me-1 mb-1 text-white bg-red-500"
|
||||
>
|
||||
<VChip v-if="meta?.edition" variant="elevated" size="small" class="me-1 mb-1 text-white bg-red-500">
|
||||
{{ meta?.edition }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="meta?.resource_pix"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
class="me-1 mb-1 text-white bg-red-500"
|
||||
>
|
||||
<VChip v-if="meta?.resource_pix" variant="elevated" size="small" class="me-1 mb-1 text-white bg-red-500">
|
||||
{{ meta?.resource_pix }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="meta?.video_encode"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
class="me-1 mb-1 text-white bg-orange-500"
|
||||
>
|
||||
<VChip v-if="meta?.video_encode" variant="elevated" size="small" class="me-1 mb-1 text-white bg-orange-500">
|
||||
{{ meta?.video_encode }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="torrent?.size"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
class="me-1 mb-1 text-white bg-yellow-500"
|
||||
>
|
||||
<VChip v-if="torrent?.size" variant="elevated" size="small" class="me-1 mb-1 text-white bg-yellow-500">
|
||||
{{ formatFileSize(torrent?.size) }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="meta?.resource_team"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
class="me-1 mb-1 text-white bg-cyan-500"
|
||||
>
|
||||
<VChip v-if="meta?.resource_team" variant="elevated" size="small" class="me-1 mb-1 text-white bg-cyan-500">
|
||||
{{ meta?.resource_team }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="torrent?.downloadvolumefactor !== 1 || torrent?.uploadvolumefactor !== 1"
|
||||
:class="
|
||||
getVolumeFactorClass(torrent?.downloadvolumefactor, torrent?.uploadvolumefactor)
|
||||
"
|
||||
:class="getVolumeFactorClass(torrent?.downloadvolumefactor, torrent?.uploadvolumefactor)"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
class="me-1 mb-1"
|
||||
@@ -280,10 +206,7 @@ onMounted(() => {
|
||||
</VChip>
|
||||
</VCardItem>
|
||||
<VCardActions>
|
||||
<VBtn
|
||||
v-if="props.more && props.more.length > 0"
|
||||
@click.stop="showMoreTorrents = !showMoreTorrents"
|
||||
>
|
||||
<VBtn v-if="props.more && props.more.length > 0" @click.stop="showMoreTorrents = !showMoreTorrents">
|
||||
<template #append>
|
||||
<VIcon :icon="showMoreTorrents ? 'mdi-chevron-up' : 'mdi-chevron-down'" />
|
||||
</template>
|
||||
@@ -297,26 +220,12 @@ onMounted(() => {
|
||||
<VChip
|
||||
v-for="(item, index) in props.more"
|
||||
:key="index"
|
||||
@click.stop="
|
||||
handleAddDownload(
|
||||
item.torrent_info?.site_name,
|
||||
item.media_info,
|
||||
item.torrent_info,
|
||||
)
|
||||
"
|
||||
@click.stop="handleAddDownload(item.torrent_info?.site_name, item.media_info, item.torrent_info)"
|
||||
>
|
||||
<template #append>
|
||||
<VBadge color="primary" :content="`↑${item.torrent_info?.seeders}`" inline size="small" />
|
||||
<VBadge
|
||||
color="primary"
|
||||
:content="`↑${item.torrent_info?.seeders}`"
|
||||
inline
|
||||
size="small"
|
||||
/>
|
||||
<VBadge
|
||||
v-if="
|
||||
item.torrent_info?.downloadvolumefactor !== 1
|
||||
|| item.torrent_info?.uploadvolumefactor !== 1
|
||||
"
|
||||
v-if="item.torrent_info?.downloadvolumefactor !== 1 || item.torrent_info?.uploadvolumefactor !== 1"
|
||||
:content="item.torrent_info?.volume_factor"
|
||||
inline
|
||||
size="small"
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useConfirm } from 'vuetify-use-dialog'
|
||||
import { formatFileSize } from '@/@core/utils/formatters'
|
||||
import api from '@/api'
|
||||
import { doneNProgress, startNProgress } from '@/api/nprogress'
|
||||
import type { Context, MediaInfo, TorrentInfo } from '@/api/types'
|
||||
import type { Context, MediaInfo, TorrentInfo } from '@/api/types'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
@@ -40,16 +40,13 @@ const downloaded = ref<String[]>([])
|
||||
async function getSiteIcon() {
|
||||
try {
|
||||
siteIcon.value = (await api.get(`site/icon/${torrent?.value?.site}`)).data.icon
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 询问并添加下载
|
||||
async function handleAddDownload(_site: any = undefined,
|
||||
_media: any = undefined,
|
||||
_torrent: any = undefined) {
|
||||
async function handleAddDownload(_site: any = undefined, _media: any = undefined, _torrent: any = undefined) {
|
||||
if (!_media || !_torrent || !_site) {
|
||||
_site = torrent.value?.site_name
|
||||
_media = media.value
|
||||
@@ -59,18 +56,9 @@ async function handleAddDownload(_site: any = undefined,
|
||||
const isConfirmed = await createConfirm({
|
||||
title: '确认',
|
||||
content: `是否确认下载【${_site}】${_torrent?.title} ?`,
|
||||
confirmationText: '确认',
|
||||
cancellationText: '取消',
|
||||
dialogProps: {
|
||||
maxWidth: '50rem',
|
||||
},
|
||||
confirmationButtonProps: {
|
||||
variant: 'tonal',
|
||||
},
|
||||
})
|
||||
|
||||
if (!isConfirmed)
|
||||
return
|
||||
if (!isConfirmed) return
|
||||
|
||||
addDownload(_media, _torrent)
|
||||
}
|
||||
@@ -88,13 +76,11 @@ async function addDownload(_media: MediaInfo, _torrent: TorrentInfo) {
|
||||
// 添加下载成功
|
||||
$toast.success(`${_torrent?.site_name} ${_torrent?.title} 添加下载成功!`)
|
||||
downloaded.value.push(_torrent?.enclosure || '')
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// 添加下载失败
|
||||
$toast.error(`${_torrent?.site_name} ${_torrent?.title} 添加下载失败!`)
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
doneNProgress()
|
||||
@@ -112,14 +98,10 @@ async function downloadTorrentFile() {
|
||||
|
||||
// 促销Chip类
|
||||
function getVolumeFactorClass(downloadVolume: number, uploadVolume: number) {
|
||||
if (downloadVolume === 0)
|
||||
return 'text-white bg-lime-500'
|
||||
else if (downloadVolume < 1)
|
||||
return 'text-white bg-green-500'
|
||||
else if (uploadVolume !== 1)
|
||||
return 'text-white bg-sky-500'
|
||||
else
|
||||
return 'text-white bg-gray-500'
|
||||
if (downloadVolume === 0) return 'text-white bg-lime-500'
|
||||
else if (downloadVolume < 1) return 'text-white bg-green-500'
|
||||
else if (uploadVolume !== 1) return 'text-white bg-sky-500'
|
||||
else return 'text-white bg-gray-500'
|
||||
}
|
||||
|
||||
// 装载时查询站点图标
|
||||
@@ -129,19 +111,9 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VListItem
|
||||
@click="handleAddDownload"
|
||||
:variant="downloaded.includes(torrent?.enclosure || '') ? 'outlined' : 'flat'"
|
||||
>
|
||||
<template
|
||||
v-if="!showMoreTorrents"
|
||||
#prepend
|
||||
>
|
||||
<VAvatar
|
||||
class="rounded"
|
||||
variant="flat"
|
||||
@click.stop="openTorrentDetail"
|
||||
>
|
||||
<VListItem @click="handleAddDownload" :variant="downloaded.includes(torrent?.enclosure || '') ? 'outlined' : 'flat'">
|
||||
<template v-if="!showMoreTorrents" #prepend>
|
||||
<VAvatar class="rounded" variant="flat" @click.stop="openTorrentDetail">
|
||||
<VImg :src="siteIcon" />
|
||||
</VAvatar>
|
||||
</template>
|
||||
@@ -153,25 +125,11 @@ onMounted(() => {
|
||||
<VListItemSubtitle>
|
||||
{{ torrent?.description }}
|
||||
</VListItemSubtitle>
|
||||
<div
|
||||
v-if="torrent?.labels"
|
||||
class="pt-2"
|
||||
>
|
||||
<VChip
|
||||
v-if="torrent?.hit_and_run"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
class="me-1 mb-1 text-white bg-black"
|
||||
>
|
||||
<div v-if="torrent?.labels" class="pt-2">
|
||||
<VChip v-if="torrent?.hit_and_run" variant="elevated" size="small" class="me-1 mb-1 text-white bg-black">
|
||||
H&R
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="torrent?.freedate_diff"
|
||||
variant="elevated"
|
||||
color="secondary"
|
||||
size="small"
|
||||
class="me-1 mb-1"
|
||||
>
|
||||
<VChip v-if="torrent?.freedate_diff" variant="elevated" color="secondary" size="small" class="me-1 mb-1">
|
||||
{{ torrent?.freedate_diff }}
|
||||
</VChip>
|
||||
<VChip
|
||||
@@ -184,51 +142,24 @@ onMounted(() => {
|
||||
>
|
||||
{{ label }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="meta?.edition"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
class="me-1 mb-1 text-white bg-red-500"
|
||||
>
|
||||
<VChip v-if="meta?.edition" variant="elevated" size="small" class="me-1 mb-1 text-white bg-red-500">
|
||||
{{ meta?.edition }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="meta?.resource_pix"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
class="me-1 mb-1 text-white bg-red-500"
|
||||
>
|
||||
<VChip v-if="meta?.resource_pix" variant="elevated" size="small" class="me-1 mb-1 text-white bg-red-500">
|
||||
{{ meta?.resource_pix }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="meta?.video_encode"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
class="me-1 mb-1 text-white bg-orange-500"
|
||||
>
|
||||
<VChip v-if="meta?.video_encode" variant="elevated" size="small" class="me-1 mb-1 text-white bg-orange-500">
|
||||
{{ meta?.video_encode }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="torrent?.size"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
class="me-1 mb-1 text-white bg-yellow-500"
|
||||
>
|
||||
<VChip v-if="torrent?.size" variant="elevated" size="small" class="me-1 mb-1 text-white bg-yellow-500">
|
||||
{{ formatFileSize(torrent?.size) }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="meta?.resource_team"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
class="me-1 mb-1 text-white bg-cyan-500"
|
||||
>
|
||||
<VChip v-if="meta?.resource_team" variant="elevated" size="small" class="me-1 mb-1 text-white bg-cyan-500">
|
||||
{{ meta?.resource_team }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="torrent?.downloadvolumefactor !== 1 || torrent?.uploadvolumefactor !== 1"
|
||||
:class="
|
||||
getVolumeFactorClass(torrent?.downloadvolumefactor, torrent?.uploadvolumefactor)
|
||||
"
|
||||
:class="getVolumeFactorClass(torrent?.downloadvolumefactor, torrent?.uploadvolumefactor)"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
class="me-1 mb-1"
|
||||
@@ -239,18 +170,10 @@ onMounted(() => {
|
||||
<template #append>
|
||||
<div class="me-n3">
|
||||
<IconBtn>
|
||||
<VIcon
|
||||
icon="mdi-dots-vertical"
|
||||
/>
|
||||
<VMenu
|
||||
activator="parent"
|
||||
close-on-content-click
|
||||
>
|
||||
<VIcon icon="mdi-dots-vertical" />
|
||||
<VMenu activator="parent" close-on-content-click>
|
||||
<VList>
|
||||
<VListItem
|
||||
variant="plain"
|
||||
@click="openTorrentDetail()"
|
||||
>
|
||||
<VListItem variant="plain" @click="openTorrentDetail()">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-information" />
|
||||
</template>
|
||||
|
||||
@@ -18,27 +18,15 @@ function handleImport() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VDialog
|
||||
width="40rem"
|
||||
scrollable
|
||||
max-height="85vh"
|
||||
>
|
||||
<VCard
|
||||
:title="props.title"
|
||||
class="rounded-t"
|
||||
>
|
||||
<VDialog width="40rem" scrollable max-height="85vh">
|
||||
<VCard :title="props.title" class="rounded-t">
|
||||
<DialogCloseBtn @click="emit('close')" />
|
||||
<VCardText class="pt-2">
|
||||
<VTextarea v-model="codeString" />
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
<VBtn
|
||||
variant="tonal"
|
||||
@click="handleImport"
|
||||
>
|
||||
导入
|
||||
</VBtn>
|
||||
<VBtn variant="elevated" @click="handleImport" prepend-icon="mdi-import" class="px-5 me-3"> 导入 </VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<script lang="ts" setup>
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import TmdbSelector from '../misc/TmdbSelector.vue'
|
||||
import MediaIdSelector from '../misc/MediaIdSelector.vue'
|
||||
import store from '@/store'
|
||||
import api from '@/api'
|
||||
import { numberValidator } from '@/@validators'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import ProgressDialog from './ProgressDialog.vue'
|
||||
import { MediaDirectory } from '@/api/types'
|
||||
|
||||
// 显示器宽度
|
||||
const display = useDisplay()
|
||||
@@ -20,19 +21,22 @@ const props = defineProps({
|
||||
// 定义事件
|
||||
const emit = defineEmits(['done', 'close'])
|
||||
|
||||
// 生成1到50季的下拉框选项
|
||||
// 生成1到100季的下拉框选项
|
||||
const seasonItems = ref(
|
||||
Array.from({ length: 51 }, (_, i) => i).map(item => ({
|
||||
Array.from({ length: 101 }, (_, i) => i).map(item => ({
|
||||
title: `第 ${item} 季`,
|
||||
value: item,
|
||||
})),
|
||||
)
|
||||
|
||||
// 当前识别类型
|
||||
const mediaSource = ref('themoviedb')
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
// TMDB选择对话框
|
||||
const tmdbSelectorDialog = ref(false)
|
||||
const mediaSelectorDialog = ref(false)
|
||||
|
||||
// 加载进度SSE
|
||||
const progressEventSource = ref<EventSource>()
|
||||
@@ -50,8 +54,9 @@ const progressValue = ref(0)
|
||||
const transferForm = reactive({
|
||||
logid: 0,
|
||||
path: '',
|
||||
target: props.target ?? '',
|
||||
target: props.target ?? null,
|
||||
tmdbid: null,
|
||||
doubanid: null,
|
||||
season: null,
|
||||
type_name: '',
|
||||
transfer_type: '',
|
||||
@@ -60,11 +65,32 @@ const transferForm = reactive({
|
||||
episode_part: '',
|
||||
episode_offset: null,
|
||||
min_filesize: 0,
|
||||
scrape: false,
|
||||
})
|
||||
|
||||
// 所有媒体库目录
|
||||
const libraryDirectories = ref<MediaDirectory[]>([])
|
||||
|
||||
// 目的目录下拉框
|
||||
const targetDirectories = computed(() => {
|
||||
const directories = libraryDirectories.value.map(item => item.path)
|
||||
return [...new Set(directories)]
|
||||
})
|
||||
|
||||
// 监听输入变化
|
||||
watchEffect(() => {
|
||||
transferForm.path = props.path ?? ''
|
||||
transferForm.target = props.target ?? ''
|
||||
transferForm.target = props.target ?? null
|
||||
})
|
||||
|
||||
// 监听目的路径变化,自动查询目录的刮削配置
|
||||
watch(transferForm, async () => {
|
||||
if (transferForm.target) {
|
||||
const directory = libraryDirectories.value.find(item => item.path === transferForm.target)
|
||||
if (directory) {
|
||||
transferForm.scrape = directory.scrape ?? false
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 使用SSE监听加载进度
|
||||
@@ -142,24 +168,53 @@ async function transfer() {
|
||||
// 重新加载
|
||||
emit('done')
|
||||
}
|
||||
|
||||
// 调用API,加载当前系统环境设置
|
||||
async function loadSystemSettings() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/env')
|
||||
if (result) mediaSource.value = result.data?.RECOGNIZE_SOURCE || 'themoviedb'
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
// 查询媒体库目录
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadSystemSettings()
|
||||
loadLibraryDirectories()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VDialog scrollable max-width="60rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VDialog scrollable max-width="50rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VCard
|
||||
:title="`${props.path ? `整理 - ${props.path}` : `整理 - 共 ${props.logids?.length} 条记录`}`"
|
||||
class="rounded-t"
|
||||
>
|
||||
<DialogCloseBtn @click="emit('close')" />
|
||||
<VCardText class="pt-2">
|
||||
<VDivider />
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<VRow>
|
||||
<VCol cols="12" md="8">
|
||||
<VTextField
|
||||
<VCombobox
|
||||
v-model="transferForm.target"
|
||||
:items="targetDirectories"
|
||||
label="目的路径"
|
||||
placeholder="留空自动"
|
||||
hint="留空将自动整理到媒体库目录"
|
||||
hint="留空将自动匹配目标路径"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
@@ -192,14 +247,26 @@ async function transfer() {
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-if="mediaSource === 'themoviedb'"
|
||||
v-model="transferForm.tmdbid"
|
||||
:disabled="transferForm.type_name === ''"
|
||||
label="TMDBID"
|
||||
label="TheMovieDb编号"
|
||||
placeholder="留空自动识别"
|
||||
:rules="[numberValidator]"
|
||||
append-inner-icon="mdi-magnify"
|
||||
hint="点击图标按名称搜索,留空将自动重新识别"
|
||||
@click:append-inner="tmdbSelectorDialog = true"
|
||||
@click:append-inner="mediaSelectorDialog = true"
|
||||
/>
|
||||
<VTextField
|
||||
v-else
|
||||
v-model="transferForm.doubanid"
|
||||
:disabled="transferForm.type_name === ''"
|
||||
label="豆瓣编号"
|
||||
placeholder="留空自动识别"
|
||||
:rules="[numberValidator]"
|
||||
append-inner-icon="mdi-magnify"
|
||||
hint="点击图标按名称搜索,留空将自动重新识别"
|
||||
@click:append-inner="mediaSelectorDialog = true"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
@@ -254,19 +321,34 @@ async function transfer() {
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch v-model="transferForm.scrape" label="刮削元数据" hint="整理完成后自动刮削元数据" />
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VBtn depressed @click="emit('close')"> 取消 </VBtn>
|
||||
<VCardActions class="pt-3">
|
||||
<VSpacer />
|
||||
<VBtn variant="tonal" @click="transfer"> 开始整理 </VBtn>
|
||||
<VBtn variant="elevated" @click="transfer" prepend-icon="mdi-arrow-right-bold" class="px-5"> 开始整理 </VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
<!-- 手动整理进度框 -->
|
||||
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" :value="progressValue" />
|
||||
<!-- TMDB ID搜索框 -->
|
||||
<VDialog v-model="tmdbSelectorDialog" width="40rem" scrollable max-height="85vh">
|
||||
<TmdbSelector v-model="transferForm.tmdbid" @close="tmdbSelectorDialog = false" />
|
||||
<VDialog v-model="mediaSelectorDialog" width="40rem" scrollable max-height="85vh">
|
||||
<MediaIdSelector
|
||||
v-if="mediaSource === 'themoviedb'"
|
||||
v-model="transferForm.tmdbid"
|
||||
@close="mediaSelectorDialog = false"
|
||||
:type="mediaSource"
|
||||
/>
|
||||
<MediaIdSelector
|
||||
v-else
|
||||
v-model="transferForm.doubanid"
|
||||
@close="mediaSelectorDialog = false"
|
||||
:type="mediaSource"
|
||||
/>
|
||||
</VDialog>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
@@ -5,10 +5,14 @@ import { doneNProgress, startNProgress } from '@/api/nprogress'
|
||||
import { numberValidator, requiredValidator } from '@/@validators'
|
||||
import api from '@/api'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import { useConfirm } from 'vuetify-use-dialog'
|
||||
|
||||
// 显示器宽度
|
||||
const display = useDisplay()
|
||||
|
||||
// 确认框
|
||||
const createConfirm = useConfirm()
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
siteid: Number,
|
||||
@@ -44,7 +48,7 @@ const statusItems = [
|
||||
|
||||
// 生成1到50的优先级下拉框选项
|
||||
const priorityItems = ref(
|
||||
Array.from({ length: 50 }, (_, i) => i + 1).map(item => ({
|
||||
Array.from({ length: 100 }, (_, i) => i + 1).map(item => ({
|
||||
title: item,
|
||||
value: item,
|
||||
})),
|
||||
@@ -86,6 +90,13 @@ async function addSite() {
|
||||
|
||||
// 调用API删除站点信息
|
||||
async function deleteSiteInfo() {
|
||||
const isConfirmed = await createConfirm({
|
||||
title: '确认',
|
||||
content: `是否确认删除站点?`,
|
||||
})
|
||||
|
||||
if (!isConfirmed) return
|
||||
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.delete(`site/${siteForm.value?.id}`)
|
||||
if (result.success) emit('remove')
|
||||
@@ -116,13 +127,14 @@ async function updateSiteInfo() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VDialog scrollable :close-on-back="false" persistent eager max-width="60rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VDialog scrollable :close-on-back="false" persistent eager max-width="50rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VCard
|
||||
:title="`${props.oper === 'add' ? '新增' : '编辑'}站点${props.oper !== 'add' ? ` - ${siteForm.name}` : ''}`"
|
||||
class="rounded-t"
|
||||
>
|
||||
<DialogCloseBtn @click="emit('close')" />
|
||||
<VCardText class="pt-2">
|
||||
<VDivider />
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -133,7 +145,7 @@ async function updateSiteInfo() {
|
||||
hint="格式:http://www.example.com/"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="3">
|
||||
<VCol cols="6" md="3">
|
||||
<VSelect
|
||||
v-model="siteForm.pri"
|
||||
label="优先级"
|
||||
@@ -142,18 +154,21 @@ async function updateSiteInfo() {
|
||||
hint="站点资源下载优先级,优先级数字越小越优先下载"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="3">
|
||||
<VCol cols="6" md="3">
|
||||
<VSelect v-model="siteForm.is_active" :items="statusItems" label="状态" />
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VCol cols="12" md="9">
|
||||
<VTextField
|
||||
v-model="siteForm.rss"
|
||||
label="RSS地址"
|
||||
hint="订阅模式为站点RSS时,将会使用此地址获取站点种子资源,该地址一般会自动获取,也可手动补充"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="3">
|
||||
<VTextField v-model="siteForm.timeout" label="超时时间(秒)" hint="站点请求超时时间,为空将使用默认值" />
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VTextarea
|
||||
v-model="siteForm.cookie"
|
||||
@@ -191,7 +206,7 @@ async function updateSiteInfo() {
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="siteForm.limit_count"
|
||||
label="访问次数"
|
||||
label="周期内访问次数"
|
||||
:rules="[numberValidator]"
|
||||
hint="设定单位周期内站点允许的访问次数,0为不限制"
|
||||
/>
|
||||
@@ -219,12 +234,31 @@ async function updateSiteInfo() {
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VBtn v-if="props.oper === 'add'" @click="emit('close')"> 取消 </VBtn>
|
||||
<VBtn v-else color="error" @click="deleteSiteInfo"> 删除 </VBtn>
|
||||
<VCardActions class="pt-3">
|
||||
<VBtn v-if="props.oper !== 'add'" color="error" @click="deleteSiteInfo" variant="outlined" class="me-3">
|
||||
删除
|
||||
</VBtn>
|
||||
<VSpacer />
|
||||
<VBtn v-if="props.oper === 'add'" color="primary" variant="tonal" @click="addSite"> 新增 </VBtn>
|
||||
<VBtn v-else color="primary" variant="tonal" @click="updateSiteInfo"> 保存 </VBtn>
|
||||
<VBtn
|
||||
v-if="props.oper === 'add'"
|
||||
color="primary"
|
||||
variant="elevated"
|
||||
@click="addSite"
|
||||
prepend-icon="mdi-plus"
|
||||
class="px-5"
|
||||
>
|
||||
新增
|
||||
</VBtn>
|
||||
<VBtn
|
||||
v-else
|
||||
color="primary"
|
||||
variant="elevated"
|
||||
@click="updateSiteInfo"
|
||||
prepend-icon="mdi-content-save"
|
||||
class="px-5"
|
||||
>
|
||||
保存
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
|
||||
@@ -2,12 +2,16 @@
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import { numberValidator } from '@/@validators'
|
||||
import api from '@/api'
|
||||
import type { Site, Subscribe } from '@/api/types'
|
||||
import type { MediaDirectory, Site, Subscribe } from '@/api/types'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import { useConfirm } from 'vuetify-use-dialog'
|
||||
|
||||
// 显示器宽度
|
||||
const display = useDisplay()
|
||||
|
||||
// 确认框
|
||||
const createConfirm = useConfirm()
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
subid: Number,
|
||||
@@ -21,6 +25,9 @@ const emit = defineEmits(['remove', 'save', 'close'])
|
||||
// 站点数据列表
|
||||
const siteList = ref<Site[]>([])
|
||||
|
||||
// 下载目录列表
|
||||
const downloadDirectories = ref<MediaDirectory[]>([])
|
||||
|
||||
// 站点选择下载框
|
||||
const selectSitesOptions = ref<{ [key: number]: string }[]>([])
|
||||
|
||||
@@ -145,6 +152,12 @@ async function getSubscribeInfo() {
|
||||
|
||||
// 删除订阅
|
||||
async function removeSubscribe() {
|
||||
const isConfirmed = await createConfirm({
|
||||
title: '确认',
|
||||
content: `是否确认取消订阅?`,
|
||||
})
|
||||
|
||||
if (!isConfirmed) return
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.delete(`subscribe/${props.subid}`)
|
||||
|
||||
@@ -157,6 +170,25 @@ async function removeSubscribe() {
|
||||
}
|
||||
}
|
||||
|
||||
// 查询下载目录
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存目录下拉框
|
||||
const targetDirectories = computed(() => {
|
||||
// 去重后的下载目录
|
||||
const directories = downloadDirectories.value.map(item => item.path)
|
||||
return [...new Set(directories)]
|
||||
})
|
||||
|
||||
// 质量选择框数据
|
||||
const qualityOptions = ref([
|
||||
{
|
||||
@@ -242,15 +274,15 @@ const effectOptions = ref([
|
||||
])
|
||||
|
||||
onMounted(() => {
|
||||
loadDownloadDirectories()
|
||||
getSiteList()
|
||||
if (props.subid) getSubscribeInfo()
|
||||
|
||||
if (props.default) queryDefaultSubscribeConfig()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VDialog scrollable max-width="60rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VDialog scrollable max-width="50rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VCard
|
||||
:title="`${
|
||||
props.default
|
||||
@@ -259,7 +291,8 @@ onMounted(() => {
|
||||
}`"
|
||||
class="rounded-t"
|
||||
>
|
||||
<VCardText class="pt-2">
|
||||
<VDivider />
|
||||
<VCardText>
|
||||
<DialogCloseBtn @click="emit('close')" />
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<VRow>
|
||||
@@ -276,7 +309,7 @@ onMounted(() => {
|
||||
v-model="subscribeForm.total_episode"
|
||||
label="总集数"
|
||||
:rules="[numberValidator]"
|
||||
hint="设定剧集的总集数,以应对themoviedb中剧集信息未维护完整,导致提前结束订阅的情况"
|
||||
hint="手动设定总集数"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol v-if="subscribeForm.type === '电视剧'" cols="12" md="2">
|
||||
@@ -284,7 +317,7 @@ onMounted(() => {
|
||||
v-model="subscribeForm.start_episode"
|
||||
label="开始集数"
|
||||
:rules="[numberValidator]"
|
||||
hint="只订阅下载此集数及之后的剧集"
|
||||
hint="只下载此集数及之后的集"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -327,8 +360,9 @@ onMounted(() => {
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
<VCombobox
|
||||
v-model="subscribeForm.save_path"
|
||||
:items="targetDirectories"
|
||||
label="保存路径"
|
||||
hint="指定该订阅的下载保存路径,留空自动使用设定的下载目录"
|
||||
/>
|
||||
@@ -359,11 +393,17 @@ onMounted(() => {
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
|
||||
<VCardActions>
|
||||
<VBtn v-if="!props.default" color="error" @click="removeSubscribe"> 取消订阅 </VBtn>
|
||||
<VCardActions class="pt-3">
|
||||
<VBtn v-if="!props.default" color="error" @click="removeSubscribe" variant="outlined" class="me-3">
|
||||
取消订阅
|
||||
</VBtn>
|
||||
<VSpacer />
|
||||
<VBtn variant="tonal" @click=";`${props.default ? saveDefaultSubscribeConfig() : updateSubscribeInfo()}`">
|
||||
<VBtn
|
||||
variant="elevated"
|
||||
@click=";`${props.default ? saveDefaultSubscribeConfig() : updateSubscribeInfo()}`"
|
||||
prepend-icon="mdi-content-save"
|
||||
class="px-5"
|
||||
>
|
||||
保存
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
|
||||
@@ -134,9 +134,10 @@ const dropdownItems = ref([
|
||||
<template>
|
||||
<VDialog scrollable max-width="50rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VCard class="mx-auto" width="100%">
|
||||
<VCardItem class="pb-0">
|
||||
<VCardItem>
|
||||
<VCardTitle>{{ props.type + '订阅历史' }}</VCardTitle>
|
||||
</VCardItem>
|
||||
<VDivider />
|
||||
<DialogCloseBtn
|
||||
@click="
|
||||
() => {
|
||||
|
||||
@@ -74,9 +74,6 @@ const nameTestResult = ref<Context>()
|
||||
// 识别结果对话框
|
||||
const nameTestDialog = ref(false)
|
||||
|
||||
// 延迟加载
|
||||
const defer = (_: number) => true
|
||||
|
||||
// 目录过滤
|
||||
const dirs = computed(() => items.value.filter(item => item.type === 'dir' && item.basename.includes(filter.value)))
|
||||
|
||||
@@ -120,14 +117,6 @@ async function deleteItem(item: FileItem) {
|
||||
const confirmed = await createConfirm({
|
||||
title: '确认',
|
||||
content: `是否确认删除${item.type === 'dir' ? '目录' : '文件'} ${item.basename}?`,
|
||||
confirmationText: '确认',
|
||||
cancellationText: '取消',
|
||||
dialogProps: {
|
||||
maxWidth: '50rem',
|
||||
},
|
||||
cancellationButtonProps: {
|
||||
variant: 'tonal',
|
||||
},
|
||||
})
|
||||
|
||||
if (confirmed) {
|
||||
|
||||
64
src/components/misc/DashboardElement.vue
Normal file
64
src/components/misc/DashboardElement.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<script setup lang="ts">
|
||||
import { DashboardItem } from '@/api/types'
|
||||
import AnalyticsMediaStatistic from '@/views/dashboard/AnalyticsMediaStatistic.vue'
|
||||
import AnalyticsScheduler from '@/views/dashboard/AnalyticsScheduler.vue'
|
||||
import AnalyticsSpeed from '@/views/dashboard/AnalyticsSpeed.vue'
|
||||
import AnalyticsStorage from '@/views/dashboard/AnalyticsStorage.vue'
|
||||
import AnalyticsWeeklyOverview from '@/views/dashboard/AnalyticsWeeklyOverview.vue'
|
||||
import AnalyticsCpu from '@/views/dashboard/AnalyticsCpu.vue'
|
||||
import AnalyticsMemory from '@/views/dashboard/AnalyticsMemory.vue'
|
||||
import MediaServerLatest from '@/views/dashboard/MediaServerLatest.vue'
|
||||
import MediaServerLibrary from '@/views/dashboard/MediaServerLibrary.vue'
|
||||
import MediaServerPlaying from '@/views/dashboard/MediaServerPlaying.vue'
|
||||
import DashboardRender from '@/components/render/DashboardRender.vue'
|
||||
import { isNullOrEmptyObject } from '@/@core/utils'
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
// 仪表板配置
|
||||
config: Object as PropType<DashboardItem>,
|
||||
// 刷新状态
|
||||
refreshStatus: Boolean,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:refreshStatus'])
|
||||
|
||||
onUnmounted(() => {
|
||||
// 组件卸载时禁用刷新状态
|
||||
emit('update:refreshStatus', false)
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<!-- 系统内置的仪表板 -->
|
||||
<AnalyticsStorage v-if="config?.id === 'storage'" />
|
||||
<AnalyticsMediaStatistic v-else-if="config?.id === 'mediaStatistic'" />
|
||||
<AnalyticsWeeklyOverview v-else-if="config?.id === 'weeklyOverview'" />
|
||||
<AnalyticsSpeed v-else-if="config?.id === 'speed'" />
|
||||
<AnalyticsScheduler v-else-if="config?.id === 'scheduler'" />
|
||||
<AnalyticsCpu v-else-if="config?.id === 'cpu'" />
|
||||
<AnalyticsMemory v-else-if="config?.id === 'memory'" />
|
||||
<MediaServerLibrary v-else-if="config?.id === 'library'" />
|
||||
<MediaServerPlaying v-else-if="config?.id === 'playing'" />
|
||||
<MediaServerLatest v-else-if="config?.id === 'latest'" />
|
||||
<!-- 插件仪表板 -->
|
||||
<VHover v-else-if="!isNullOrEmptyObject(props.config)">
|
||||
<template #default="hover">
|
||||
<VCard v-bind="hover.props">
|
||||
<VCardItem v-if="props.config?.attrs.border !== false">
|
||||
<template #append>
|
||||
<VIcon class="cursor-move" v-if="hover.isHovering">mdi-drag</VIcon>
|
||||
</template>
|
||||
<VCardTitle>
|
||||
{{ props.config?.attrs?.title ?? props.config?.name }}
|
||||
</VCardTitle>
|
||||
<VCardSubtitle v-if="props.config?.attrs?.subtitle"> {{ props.config?.attrs?.subtitle }}</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText :class="{ 'p-0': props.config?.attrs.border === false }">
|
||||
<DashboardRender v-for="(item, index) in props.config?.elements" :key="index" :config="item" />
|
||||
</VCardText>
|
||||
<div v-if="props.config?.attrs.border === false && hover.isHovering" class="absolute right-5 top-5">
|
||||
<VIcon class="cursor-move">mdi-drag</VIcon>
|
||||
</div>
|
||||
</VCard>
|
||||
</template>
|
||||
</VHover>
|
||||
</template>
|
||||
@@ -2,10 +2,16 @@
|
||||
import api from '@/api'
|
||||
import type { MediaInfo } from '@/api/types'
|
||||
|
||||
// 定义输入变量
|
||||
const props = defineProps({
|
||||
type: String, // 来源 themoviedb | douban
|
||||
})
|
||||
|
||||
interface TmdbItem {
|
||||
title: string
|
||||
overview: string
|
||||
tmdbid: number
|
||||
doubanid: string
|
||||
poster: string
|
||||
}
|
||||
|
||||
@@ -21,25 +27,23 @@ const keyword = ref('')
|
||||
const loading = ref(false)
|
||||
|
||||
// ref
|
||||
const tmdbKeyword = ref<HTMLElement | null>(null)
|
||||
const inputKeyword = ref<HTMLElement | null>(null)
|
||||
|
||||
// 选中条目
|
||||
function selectMedia(item: TmdbItem) {
|
||||
emit('update:modelValue', item.tmdbid)
|
||||
emit('update:modelValue', item.tmdbid || item.doubanid)
|
||||
emit('close')
|
||||
}
|
||||
|
||||
// TMDB图片转换为w500大小
|
||||
function getW500Image(url = '') {
|
||||
if (!url)
|
||||
return ''
|
||||
if (!url) return ''
|
||||
return url.replace('original', 'w500')
|
||||
}
|
||||
|
||||
// 搜索词条
|
||||
async function searchMedias() {
|
||||
if (!keyword)
|
||||
return
|
||||
if (!keyword) return
|
||||
|
||||
// 调用API搜索词条
|
||||
try {
|
||||
@@ -57,16 +61,17 @@ async function searchMedias() {
|
||||
|
||||
// 赋值
|
||||
for (const item of result) {
|
||||
if (props.type && props.type !== item.source) continue
|
||||
items.value.push({
|
||||
tmdbid: item.tmdb_id || 0,
|
||||
doubanid: item.douban_id || '',
|
||||
poster: getW500Image(item.poster_path),
|
||||
title: `${item.title}(${item.year})`,
|
||||
overview: `<span class="text-primary">${item.type}</span> ${item.overview}`,
|
||||
})
|
||||
}
|
||||
loading.value = false
|
||||
}
|
||||
catch (e) {
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
@@ -75,19 +80,16 @@ async function searchMedias() {
|
||||
onMounted(() => {
|
||||
// 500ms后聚焦
|
||||
setTimeout(() => {
|
||||
tmdbKeyword.value?.focus()
|
||||
inputKeyword.value?.focus()
|
||||
}, 500)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard
|
||||
class="mx-auto"
|
||||
width="100%"
|
||||
>
|
||||
<VCard class="mx-auto" width="100%">
|
||||
<VToolbar flat class="p-0">
|
||||
<VTextField
|
||||
ref="tmdbKeyword"
|
||||
ref="inputKeyword"
|
||||
v-model="keyword"
|
||||
label="输入名称搜索"
|
||||
single-line
|
||||
@@ -101,15 +103,17 @@ onMounted(() => {
|
||||
@keydown.enter="searchMedias"
|
||||
/>
|
||||
</VToolbar>
|
||||
<DialogCloseBtn @click="() => { emit('close') }" />
|
||||
<VList
|
||||
v-if="items.length > 0"
|
||||
lines="three"
|
||||
>
|
||||
<DialogCloseBtn
|
||||
@click="
|
||||
() => {
|
||||
emit('close')
|
||||
}
|
||||
"
|
||||
/>
|
||||
<VDivider />
|
||||
<VList v-if="items.length > 0" lines="three">
|
||||
<template v-for="(item, i) in items" :key="i">
|
||||
<VListItem
|
||||
@click="selectMedia(item)"
|
||||
>
|
||||
<VListItem @click="selectMedia(item)">
|
||||
<template #prepend>
|
||||
<VImg
|
||||
height="75"
|
||||
@@ -8,17 +8,16 @@ const props = defineProps({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCardItem>
|
||||
<VCardText>
|
||||
<VList>
|
||||
<VListItem
|
||||
v-for="(value, key) in props.history"
|
||||
:key="key"
|
||||
>
|
||||
<VListItemTitle>{{ key }}</VListItemTitle>
|
||||
<VListItem v-for="(value, key) in props.history" :key="key">
|
||||
<VListItemTitle class="font-bold text-lg">
|
||||
{{ key }}
|
||||
</VListItemTitle>
|
||||
<div class="text-gray-500">
|
||||
{{ value }}
|
||||
</div>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VCardItem>
|
||||
</VCardText>
|
||||
</template>
|
||||
|
||||
31
src/components/render/DashboardRender.vue
Normal file
31
src/components/render/DashboardRender.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<script lang="ts" setup>
|
||||
import { RenderProps } from '@/api/types'
|
||||
import { type PropType } from 'vue'
|
||||
|
||||
// 输入参数
|
||||
const elementProps = defineProps({
|
||||
config: Object as PropType<RenderProps>,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Component :is="elementProps.config?.component" v-if="!elementProps.config?.html" v-bind="elementProps.config?.props">
|
||||
{{ elementProps.config?.text }}
|
||||
<template v-for="(content, name) in elementProps.config?.slots || []" :key="name" v-slot:[name]="{ _props }">
|
||||
<slot :name="name" v-bind="_props">
|
||||
<DashboardRender v-for="(slotItem, slotIndex) in content || []" :key="slotIndex" :config="slotItem" />
|
||||
</slot>
|
||||
</template>
|
||||
<DashboardRender
|
||||
v-for="(innerItem, innerIndex) in elementProps.config?.content || []"
|
||||
:key="innerIndex"
|
||||
:config="innerItem"
|
||||
/>
|
||||
</Component>
|
||||
<Component
|
||||
:is="elementProps.config?.component"
|
||||
v-if="elementProps.config?.html"
|
||||
v-bind="elementProps.config?.props"
|
||||
v-html="elementProps.config?.html"
|
||||
/>
|
||||
</template>
|
||||
@@ -1,15 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { RenderProps } from '@/api/types'
|
||||
import { type PropType, ref } from 'vue'
|
||||
|
||||
// 组件接口
|
||||
interface RenderProps {
|
||||
component: string
|
||||
text: string
|
||||
html: string
|
||||
content?: any
|
||||
props?: any
|
||||
}
|
||||
|
||||
// 输入参数
|
||||
const elementProps = defineProps({
|
||||
config: Object as PropType<RenderProps>,
|
||||
@@ -17,13 +9,15 @@ const elementProps = defineProps({
|
||||
})
|
||||
|
||||
// 配置元素
|
||||
const formItem = ref<RenderProps>(elementProps.config ?? {
|
||||
component: 'div',
|
||||
text: '',
|
||||
html: '',
|
||||
props: {},
|
||||
content: [],
|
||||
})
|
||||
const formItem = ref<RenderProps>(
|
||||
elementProps.config ?? {
|
||||
component: 'div',
|
||||
text: '',
|
||||
html: '',
|
||||
props: {},
|
||||
content: [],
|
||||
},
|
||||
)
|
||||
|
||||
// 配置数据
|
||||
const formData = ref<any>(elementProps.form || {})
|
||||
@@ -37,53 +31,27 @@ const formData = ref<any>(elementProps.form || {})
|
||||
v-model:value="formData[formItem.props?.modelvalue]"
|
||||
>
|
||||
{{ formItem.text }}
|
||||
<template
|
||||
v-for="(innerItem, innerIndex) in (formItem.content || [])"
|
||||
:key="innerIndex"
|
||||
>
|
||||
<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"
|
||||
/>
|
||||
<FormRender v-else v-model="formData[innerItem.props?.model]" :config="innerItem" :form="formData" />
|
||||
</template>
|
||||
</Component>
|
||||
<Component
|
||||
:is="formItem.component"
|
||||
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]"
|
||||
>
|
||||
<Component :is="formItem.component" 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"
|
||||
>
|
||||
<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"
|
||||
/>
|
||||
<FormRender v-else v-model="formData[innerItem.props?.model]" :config="innerItem" :form="formData" />
|
||||
</template>
|
||||
</Component>
|
||||
</template>
|
||||
|
||||
@@ -3,21 +3,11 @@ import { isNullOrEmptyObject } from '@/@core/utils'
|
||||
import api from '@/api'
|
||||
import { type PropType } from 'vue'
|
||||
import ProgressDialog from '../dialog/ProgressDialog.vue'
|
||||
import { RenderProps } from '@/api/types'
|
||||
|
||||
// 定议外部事件
|
||||
const emit = defineEmits(['action'])
|
||||
|
||||
// 组件接口
|
||||
interface RenderProps {
|
||||
component: string
|
||||
text: string
|
||||
html: string
|
||||
content?: any
|
||||
slots?: any
|
||||
props?: any
|
||||
events?: any
|
||||
}
|
||||
|
||||
// 输入参数
|
||||
const elementProps = defineProps({
|
||||
config: Object as PropType<RenderProps>,
|
||||
|
||||
@@ -60,22 +60,17 @@ async function getResourceList() {
|
||||
try {
|
||||
resourceDataList.value = await api.get(`site/resource/${props.site}`)
|
||||
resourceLoading.value = false
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 促销Chip类
|
||||
function getVolumeFactorClass(downloadVolume: number, uploadVolume: number) {
|
||||
if (downloadVolume === 0)
|
||||
return 'text-white bg-lime-500'
|
||||
else if (downloadVolume < 1)
|
||||
return 'text-white bg-green-500'
|
||||
else if (uploadVolume !== 1)
|
||||
return 'text-white bg-sky-500'
|
||||
else
|
||||
return 'text-white bg-gray-500'
|
||||
if (downloadVolume === 0) return 'text-white bg-lime-500'
|
||||
else if (downloadVolume < 1) return 'text-white bg-green-500'
|
||||
else if (uploadVolume !== 1) return 'text-white bg-sky-500'
|
||||
else return 'text-white bg-gray-500'
|
||||
}
|
||||
|
||||
// 添加下载
|
||||
@@ -83,18 +78,9 @@ async function addDownload(_torrent: any) {
|
||||
const isConfirmed = await createConfirm({
|
||||
title: '确认',
|
||||
content: `是否确认下载【${_torrent.site_name}】${_torrent?.title} ?`,
|
||||
confirmationText: '确认',
|
||||
cancellationText: '取消',
|
||||
dialogProps: {
|
||||
maxWidth: '50rem',
|
||||
},
|
||||
confirmationButtonProps: {
|
||||
variant: 'tonal',
|
||||
},
|
||||
})
|
||||
|
||||
if (!isConfirmed)
|
||||
return
|
||||
if (!isConfirmed) return
|
||||
|
||||
startNProgress()
|
||||
try {
|
||||
@@ -103,13 +89,11 @@ async function addDownload(_torrent: any) {
|
||||
if (result.success) {
|
||||
// 添加下载成功
|
||||
$toast.success(`${_torrent?.site_name} ${_torrent?.title} 添加下载成功!`)
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// 添加下载失败
|
||||
$toast.error(`${_torrent?.site_name} ${_torrent?.title} 添加下载失败:${result.message || '未知错误'}`)
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
doneNProgress()
|
||||
@@ -146,21 +130,10 @@ onMounted(() => {
|
||||
<div class="text-sm my-1">
|
||||
{{ item.description }}
|
||||
</div>
|
||||
<VChip
|
||||
v-if="item.hit_and_run"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
class="me-1 mb-1 text-white bg-black"
|
||||
>
|
||||
<VChip v-if="item.hit_and_run" variant="elevated" size="small" class="me-1 mb-1 text-white bg-black">
|
||||
H&R
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="item.freedate_diff"
|
||||
variant="elevated"
|
||||
color="secondary"
|
||||
size="small"
|
||||
class="me-1 mb-1"
|
||||
>
|
||||
<VChip v-if="item.freedate_diff" variant="elevated" color="secondary" size="small" class="me-1 mb-1">
|
||||
{{ item.freedate_diff }}
|
||||
</VChip>
|
||||
<VChip
|
||||
@@ -175,9 +148,7 @@ onMounted(() => {
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="item.downloadvolumefactor !== 1 || item.uploadvolumefactor !== 1"
|
||||
:class="
|
||||
getVolumeFactorClass(item.downloadvolumefactor, item.uploadvolumefactor)
|
||||
"
|
||||
:class="getVolumeFactorClass(item.downloadvolumefactor, item.uploadvolumefactor)"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
class="me-1 mb-1"
|
||||
@@ -206,18 +177,10 @@ onMounted(() => {
|
||||
<template #item.actions="{ item }">
|
||||
<div class="me-n3">
|
||||
<IconBtn>
|
||||
<VIcon
|
||||
icon="mdi-dots-vertical"
|
||||
/>
|
||||
<VMenu
|
||||
activator="parent"
|
||||
close-on-content-click
|
||||
>
|
||||
<VIcon icon="mdi-dots-vertical" />
|
||||
<VMenu activator="parent" close-on-content-click>
|
||||
<VList>
|
||||
<VListItem
|
||||
variant="plain"
|
||||
@click="openTorrentDetail(item.page_url || '')"
|
||||
>
|
||||
<VListItem variant="plain" @click="openTorrentDetail(item.page_url || '')">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-information" />
|
||||
</template>
|
||||
@@ -238,8 +201,6 @@ onMounted(() => {
|
||||
</IconBtn>
|
||||
</div>
|
||||
</template>
|
||||
<template #no-data>
|
||||
没有数据
|
||||
</template>
|
||||
<template #no-data> 没有数据 </template>
|
||||
</VDataTable>
|
||||
</template>
|
||||
|
||||
@@ -6,6 +6,7 @@ import VerticalNavLink from '@layouts/components/VerticalNavLink.vue'
|
||||
// Components
|
||||
import Footer from '@/layouts/components/Footer.vue'
|
||||
import NavbarThemeSwitcher from '@/layouts/components/NavbarThemeSwitcher.vue'
|
||||
import UserNofification from '@/layouts/components/UserNotification.vue'
|
||||
import SearchBar from '@/layouts/components/SearchBar.vue'
|
||||
import ShortcutBar from '@/layouts/components/ShortcutBar.vue'
|
||||
import UserProfile from '@/layouts/components/UserProfile.vue'
|
||||
@@ -21,10 +22,7 @@ const superUser = store.state.auth.superUser
|
||||
<template #navbar="{ toggleVerticalOverlayNavActive }">
|
||||
<div class="d-flex h-100 align-center mx-1">
|
||||
<!-- 👉 Vertical Nav Toggle -->
|
||||
<IconBtn
|
||||
class="ms-n2 d-lg-none"
|
||||
@click="toggleVerticalOverlayNavActive(true)"
|
||||
>
|
||||
<IconBtn class="ms-n2 d-lg-none" @click="toggleVerticalOverlayNavActive(true)">
|
||||
<VIcon icon="mdi-menu" />
|
||||
</IconBtn>
|
||||
|
||||
@@ -33,21 +31,14 @@ const superUser = store.state.auth.superUser
|
||||
|
||||
<VSpacer />
|
||||
|
||||
<!-- 👉 Github -->
|
||||
<IconBtn
|
||||
class="me-2"
|
||||
href="https://github.com/jxxghp/MoviePilot"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<VIcon icon="mdi-github" />
|
||||
</IconBtn>
|
||||
|
||||
<!-- 👉 Shortcuts -->
|
||||
<ShortcutBar v-if="superUser" />
|
||||
|
||||
<!-- 👉 Theme -->
|
||||
<NavbarThemeSwitcher class="me-2" />
|
||||
<NavbarThemeSwitcher />
|
||||
|
||||
<!-- 👉 Notification -->
|
||||
<UserNofification />
|
||||
|
||||
<!-- 👉 UserProfile -->
|
||||
<UserProfile />
|
||||
|
||||
@@ -2,25 +2,29 @@
|
||||
import type { ThemeSwitcherTheme } from '@layouts/types'
|
||||
|
||||
const themes: ThemeSwitcherTheme[] = [
|
||||
{
|
||||
name: 'auto',
|
||||
title: '跟随系统',
|
||||
icon: 'mdi-laptop',
|
||||
},
|
||||
{
|
||||
name: 'light',
|
||||
title: '明亮',
|
||||
icon: 'mdi-weather-sunny',
|
||||
},
|
||||
{
|
||||
name: 'dark',
|
||||
title: '暗黑',
|
||||
icon: 'mdi-weather-night',
|
||||
},
|
||||
{
|
||||
name: 'purple',
|
||||
title: '紫韵幽兰',
|
||||
icon: 'mdi-brightness-4',
|
||||
},
|
||||
{
|
||||
name: 'auto',
|
||||
icon: 'mdi-brightness-auto',
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ThemeSwitcher :themes="themes" />
|
||||
<ThemeSwitcher class="ms-2" :themes="themes" />
|
||||
</template>
|
||||
|
||||
@@ -91,7 +91,7 @@ onMounted(() => {
|
||||
>
|
||||
<!-- Menu Activator -->
|
||||
<template #activator="{ props }">
|
||||
<IconBtn class="me-2" v-bind="props">
|
||||
<IconBtn class="ms-2" v-bind="props">
|
||||
<VIcon icon="mdi-checkbox-multiple-blank-outline" />
|
||||
</IconBtn>
|
||||
</template>
|
||||
|
||||
102
src/layouts/components/UserNotification.vue
Normal file
102
src/layouts/components/UserNotification.vue
Normal file
@@ -0,0 +1,102 @@
|
||||
<script setup lang="ts">
|
||||
import store from '@/store'
|
||||
import { formatDateDifference } from '@core/utils/formatters'
|
||||
import { SystemNotification } from '@/api/types'
|
||||
|
||||
// 是否有新消息
|
||||
const hasNewMessage = ref(false)
|
||||
|
||||
// 通知列表
|
||||
const notificationList = ref<SystemNotification[]>([])
|
||||
|
||||
// 事件源
|
||||
let eventSource: EventSource | null = null
|
||||
|
||||
// 弹窗
|
||||
const appsMenu = ref(false)
|
||||
|
||||
// SSE持续接收消息
|
||||
function startSSEMessager() {
|
||||
const token = store.state.auth.token
|
||||
if (token) {
|
||||
eventSource = new EventSource(`${import.meta.env.VITE_API_BASE_URL}system/message?token=${token}`)
|
||||
eventSource.addEventListener('message', event => {
|
||||
if (event.data) {
|
||||
const noti: SystemNotification = JSON.parse(event.data)
|
||||
notificationList.value.unshift(noti)
|
||||
hasNewMessage.value = true
|
||||
// TODO 在顶部显示消息汽泡
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载时,加载当前用户数据
|
||||
onBeforeMount(async () => {
|
||||
startSSEMessager()
|
||||
})
|
||||
|
||||
// 页面卸载时,关闭事件源
|
||||
onBeforeUnmount(() => {
|
||||
if (eventSource) eventSource.close()
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<VMenu v-model="appsMenu" width="400" transition="scale-transition" close-on-content-click>
|
||||
<!-- Menu Activator -->
|
||||
<template #activator="{ props }">
|
||||
<VBadge v-if="hasNewMessage" dot color="error" :offset-x="5" :offset-y="5" v-bind="props">
|
||||
<IconBtn>
|
||||
<VIcon icon="mdi-bell-outline" />
|
||||
</IconBtn>
|
||||
</VBadge>
|
||||
<IconBtn v-else v-bind="props">
|
||||
<VIcon icon="mdi-bell-outline" />
|
||||
</IconBtn>
|
||||
</template>
|
||||
<!-- Menu Content -->
|
||||
<VCard>
|
||||
<VCardItem class="border-b">
|
||||
<VCardTitle>通知</VCardTitle>
|
||||
<template #append>
|
||||
<VTooltip text="设为已读">
|
||||
<template #activator="{ props }">
|
||||
<IconBtn
|
||||
v-bind="props"
|
||||
@click="
|
||||
() => {
|
||||
hasNewMessage = false
|
||||
appsMenu = false
|
||||
}
|
||||
"
|
||||
>
|
||||
<VIcon icon="mdi-email-mark-as-unread" />
|
||||
</IconBtn>
|
||||
</template>
|
||||
</VTooltip>
|
||||
</template>
|
||||
</VCardItem>
|
||||
<VList lines="two" v-if="notificationList.length > 0" max-height="600">
|
||||
<VListItem v-for="(item, i) in notificationList" :key="i">
|
||||
<template #prepend>
|
||||
<VAvatar rounded>
|
||||
<VIcon v-if="item.type === 'user'" icon="mdi-account-alert" size="large"></VIcon>
|
||||
<VIcon v-else-if="item.type === 'plugin'" icon="mdi-robot-happy" size="large"></VIcon>
|
||||
<VIcon v-else icon="mdi-laptop" size="large"></VIcon>
|
||||
</VAvatar>
|
||||
</template>
|
||||
<VListItemTitle class="overflow-visiable break-words whitespace-break-spaces">
|
||||
{{ item.title }}
|
||||
</VListItemTitle>
|
||||
<VListItemSubtitle class="mt-2">{{ item.text }}</VListItemSubtitle>
|
||||
<VListItemSubtitle class="mt-2">{{ formatDateDifference(item.date) }}</VListItemSubtitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
<VList v-else>
|
||||
<VListItem>
|
||||
<VListItemTitle class="text-center">暂无通知</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VCard>
|
||||
</VMenu>
|
||||
</template>
|
||||
@@ -23,7 +23,8 @@ const progressDialog = ref(false)
|
||||
function logout() {
|
||||
// 清除登录状态信息
|
||||
store.dispatch('auth/clearToken')
|
||||
|
||||
// 主动登出时清除路由标记
|
||||
store.state.auth.originalPath = null
|
||||
// 重定向到登录页面或其他适当的页面
|
||||
router.push('/login')
|
||||
}
|
||||
@@ -34,14 +35,6 @@ async function restart() {
|
||||
const confirmed = await createConfirm({
|
||||
title: '确认',
|
||||
content: '确认重启系统吗?',
|
||||
confirmationText: '确认',
|
||||
cancellationText: '取消',
|
||||
dialogProps: {
|
||||
maxWidth: '30rem',
|
||||
},
|
||||
cancellationButtonProps: {
|
||||
variant: 'tonal',
|
||||
},
|
||||
})
|
||||
|
||||
if (confirmed) {
|
||||
@@ -72,7 +65,7 @@ const avatar = store.state.auth.avatar
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VAvatar class="cursor-pointer" color="primary" variant="tonal">
|
||||
<VAvatar class="cursor-pointer ms-3" color="primary" variant="tonal">
|
||||
<VImg :src="avatar ?? avatar1" />
|
||||
|
||||
<!-- SECTION Menu -->
|
||||
@@ -100,10 +93,17 @@ const avatar = store.state.auth.avatar
|
||||
<template #prepend>
|
||||
<VIcon class="me-2" icon="mdi-account-outline" size="22" />
|
||||
</template>
|
||||
|
||||
<VListItemTitle>设定</VListItemTitle>
|
||||
</VListItem>
|
||||
|
||||
<!-- 👉 FAQ -->
|
||||
<VListItem href="https://github.com/jxxghp/MoviePilot/blob/main/README.md" target="_blank">
|
||||
<template #prepend>
|
||||
<VIcon class="me-2" icon="mdi-help-circle-outline" size="22" />
|
||||
</template>
|
||||
<VListItemTitle>帮助</VListItemTitle>
|
||||
</VListItem>
|
||||
|
||||
<!-- Divider -->
|
||||
<VDivider class="my-2" />
|
||||
|
||||
@@ -112,26 +112,18 @@ const avatar = store.state.auth.avatar
|
||||
<template #prepend>
|
||||
<VIcon class="me-2" icon="mdi-restart" size="22" />
|
||||
</template>
|
||||
|
||||
<VListItemTitle>重启</VListItemTitle>
|
||||
</VListItem>
|
||||
|
||||
<!-- 👉 FAQ -->
|
||||
<VListItem href="https://github.com/jxxghp/MoviePilot/blob/main/README.md" target="_blank">
|
||||
<template #prepend>
|
||||
<VIcon class="me-2" icon="mdi-help-circle-outline" size="22" />
|
||||
</template>
|
||||
|
||||
<VListItemTitle>帮助</VListItemTitle>
|
||||
</VListItem>
|
||||
<!-- Divider -->
|
||||
<VDivider class="my-2" />
|
||||
|
||||
<!-- 👉 Logout -->
|
||||
<VListItem @click="logout">
|
||||
<template #prepend>
|
||||
<VIcon class="me-2" icon="mdi-logout" size="22" />
|
||||
</template>
|
||||
|
||||
<VListItemTitle>注销</VListItemTitle>
|
||||
<VBtn color="error" block>
|
||||
<template #append> <VIcon size="small" icon="mdi-logout" /> </template>
|
||||
退出登录
|
||||
</VBtn>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
|
||||
@@ -1,17 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import DefaultLayoutWithVerticalNav from './components/DefaultLayoutWithVerticalNav.vue'
|
||||
import api from '@/api'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
api.get('user/current')
|
||||
.catch(() => {
|
||||
router.replace('/login')
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -27,5 +17,5 @@ document.addEventListener('visibilitychange', () => {
|
||||
|
||||
<style lang="scss">
|
||||
// As we are using `layouts` plugin we need its styles to be imported
|
||||
@use "@layouts/styles/default-layout";
|
||||
@use '@layouts/styles/default-layout';
|
||||
</style>
|
||||
|
||||
43
src/main.ts
43
src/main.ts
@@ -15,9 +15,16 @@ import '@core/scss/template/index.scss'
|
||||
import '@layouts/styles/index.scss'
|
||||
import '@styles/styles.scss'
|
||||
import 'vue-toast-notification/dist/theme-bootstrap.css'
|
||||
import { PerfectScrollbarPlugin } from 'vue3-perfect-scrollbar';
|
||||
import 'vue3-perfect-scrollbar/style.css';
|
||||
import { PerfectScrollbarPlugin } from 'vue3-perfect-scrollbar'
|
||||
import 'vue3-perfect-scrollbar/style.css'
|
||||
import DialogCloseBtn from '@/@core/components/DialogCloseBtn.vue'
|
||||
import MediaCard from './components/cards/MediaCard.vue'
|
||||
import PosterCard from './components/cards/PosterCard.vue'
|
||||
import BackdropCard from './components/cards/BackdropCard.vue'
|
||||
import PersonCard from './components/cards/PersonCard.vue'
|
||||
import MediaInfoCard from './components/cards/MediaInfoCard.vue'
|
||||
import TorrentCard from './components/cards/TorrentCard.vue'
|
||||
import MediaIdSelector from './components/misc/MediaIdSelector.vue'
|
||||
import { fixArrayAt } from '@/@core/utils/compatibility'
|
||||
|
||||
// 修复低版本Safari等浏览器数组不支持at函数的问题
|
||||
@@ -30,9 +37,17 @@ loadFonts()
|
||||
const app = createApp(App)
|
||||
|
||||
// 注册全局组件
|
||||
app.component('VAceEditor', VAceEditor)
|
||||
app
|
||||
.component('VAceEditor', VAceEditor)
|
||||
.component('VApexChart', VueApexCharts)
|
||||
.component('VDialogCloseBtn', DialogCloseBtn)
|
||||
.component('VMediaCard', MediaCard)
|
||||
.component('VPosterCard', PosterCard)
|
||||
.component('VBackdropCard', BackdropCard)
|
||||
.component('VPersonCard', PersonCard)
|
||||
.component('VMediaInfoCard', MediaInfoCard)
|
||||
.component('VTorrentCard', TorrentCard)
|
||||
.component('VMediaIdSelector', MediaIdSelector)
|
||||
|
||||
// 注册插件
|
||||
app
|
||||
@@ -42,7 +57,27 @@ app
|
||||
.use(ToastPlugin, {
|
||||
position: 'bottom-right',
|
||||
})
|
||||
.use(VuetifyUseDialog)
|
||||
.use(VuetifyUseDialog, {
|
||||
confirmDialog: {
|
||||
dialogProps: {
|
||||
maxWidth: '40rem',
|
||||
},
|
||||
confirmationButtonProps: {
|
||||
variant: 'elevated',
|
||||
color: 'primary',
|
||||
class: 'me-3 px-5',
|
||||
'prepend-icon': 'mdi-check',
|
||||
},
|
||||
cancellationButtonProps: {
|
||||
variant: 'outlined',
|
||||
color: 'secondary',
|
||||
class: 'me-3',
|
||||
},
|
||||
confirmationText: '确认',
|
||||
cancellationText: '取消',
|
||||
},
|
||||
})
|
||||
.use(PerfectScrollbarPlugin)
|
||||
.use(VueApexCharts)
|
||||
.mount('#app')
|
||||
.$nextTick(() => removeEl('#loading-bg'))
|
||||
|
||||
@@ -1,40 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import AnalyticsMediaStatistic from '@/views/dashboard/AnalyticsMediaStatistic.vue'
|
||||
import AnalyticsScheduler from '@/views/dashboard/AnalyticsScheduler.vue'
|
||||
import AnalyticsSpeed from '@/views/dashboard/AnalyticsSpeed.vue'
|
||||
import AnalyticsStorage from '@/views/dashboard/AnalyticsStorage.vue'
|
||||
import AnalyticsWeeklyOverview from '@/views/dashboard/AnalyticsWeeklyOverview.vue'
|
||||
import AnalyticsCpu from '@/views/dashboard/AnalyticsCpu.vue'
|
||||
import AnalyticsMemory from '@/views/dashboard/AnalyticsMemory.vue'
|
||||
import MediaServerLatest from '@/views/dashboard/MediaServerLatest.vue'
|
||||
import MediaServerLibrary from '@/views/dashboard/MediaServerLibrary.vue'
|
||||
import MediaServerPlaying from '@/views/dashboard/MediaServerPlaying.vue'
|
||||
import draggable from 'vuedraggable'
|
||||
import api from '@/api'
|
||||
import { isNullOrEmptyObject } from '@/@core/utils'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import { DashboardItem } from '@/api/types'
|
||||
import store from '@/store'
|
||||
import DashboardElement from '@/components/misc/DashboardElement.vue'
|
||||
|
||||
// 显示器宽度
|
||||
const display = useDisplay()
|
||||
// 从Vuex Store中获取superuser信息
|
||||
const superUser = store.state.auth.superUser
|
||||
|
||||
// 仪表盘配置
|
||||
const dashboard_names = {
|
||||
storage: '存储空间',
|
||||
mediaStatistic: '媒体统计',
|
||||
weeklyOverview: '最近入库',
|
||||
speed: '实时速率',
|
||||
scheduler: '后台任务',
|
||||
cpu: 'CPU',
|
||||
memory: '内存',
|
||||
library: '我的媒体库',
|
||||
playing: '继续观看',
|
||||
latest: '最近添加',
|
||||
}
|
||||
|
||||
// 弹窗
|
||||
const dialog = ref(false)
|
||||
|
||||
// 从localStorage中获取数据
|
||||
const default_config = {
|
||||
// 仪表板启用配置
|
||||
const enableConfig = ref<{ [key: string]: boolean }>({
|
||||
mediaStatistic: true,
|
||||
scheduler: false,
|
||||
speed: false,
|
||||
@@ -45,87 +21,286 @@ const default_config = {
|
||||
library: true,
|
||||
playing: true,
|
||||
latest: true,
|
||||
})
|
||||
|
||||
// 仪表板顺序配置
|
||||
const orderConfig = ref<{ id: string, key: string }[]>([])
|
||||
|
||||
// 仪表板配置
|
||||
const dashboardConfigs = ref<DashboardItem[]>([
|
||||
{
|
||||
id: 'storage',
|
||||
name: '存储空间',
|
||||
key: "",
|
||||
attrs: {},
|
||||
cols: { cols: 12, md: 4 },
|
||||
elements: [],
|
||||
},
|
||||
{
|
||||
id: 'mediaStatistic',
|
||||
name: '媒体统计',
|
||||
key: "",
|
||||
attrs: {},
|
||||
cols: { cols: 12, md: 8 },
|
||||
elements: [],
|
||||
},
|
||||
{
|
||||
id: 'weeklyOverview',
|
||||
name: '最近入库',
|
||||
key: "",
|
||||
attrs: {},
|
||||
cols: { cols: 12, md: 4 },
|
||||
elements: [],
|
||||
},
|
||||
{
|
||||
id: 'speed',
|
||||
name: '实时速率',
|
||||
key: "",
|
||||
attrs: {},
|
||||
cols: { cols: 12, md: 4 },
|
||||
elements: [],
|
||||
},
|
||||
{
|
||||
id: 'scheduler',
|
||||
name: '后台任务',
|
||||
key: "",
|
||||
attrs: {},
|
||||
cols: { cols: 12, md: 4 },
|
||||
elements: [],
|
||||
},
|
||||
{
|
||||
id: 'cpu',
|
||||
name: 'CPU',
|
||||
key: "",
|
||||
attrs: {},
|
||||
cols: { cols: 12, md: 6 },
|
||||
elements: [],
|
||||
},
|
||||
{
|
||||
id: 'memory',
|
||||
name: '内存',
|
||||
key: "",
|
||||
attrs: {},
|
||||
cols: { cols: 12, md: 6 },
|
||||
elements: [],
|
||||
},
|
||||
{
|
||||
id: 'library',
|
||||
name: '我的媒体库',
|
||||
key: "",
|
||||
attrs: {},
|
||||
cols: { cols: 12 },
|
||||
elements: [],
|
||||
},
|
||||
{
|
||||
id: 'playing',
|
||||
name: '继续观看',
|
||||
key: "",
|
||||
attrs: {},
|
||||
cols: { cols: 12 },
|
||||
elements: [],
|
||||
},
|
||||
{
|
||||
id: 'latest',
|
||||
name: '最近添加',
|
||||
key: "",
|
||||
attrs: {},
|
||||
cols: { cols: 12 },
|
||||
elements: [],
|
||||
},
|
||||
])
|
||||
|
||||
// 插件的仪表板元信息
|
||||
const pluginDashboardMeta = ref<any[]>([])
|
||||
|
||||
// 插件仪表板的刷新状态
|
||||
const pluginDashboardRefreshStatus = ref<{ [key: string]: boolean }>({})
|
||||
|
||||
// 弹窗
|
||||
const dialog = ref(false)
|
||||
|
||||
// 加载用户监控面板配置(本地无配置时才加载)
|
||||
async function loadDashboardConfig() {
|
||||
// 显示配置
|
||||
const local_enable = localStorage.getItem('MP_DASHBOARD')
|
||||
if (local_enable) {
|
||||
enableConfig.value = JSON.parse(local_enable)
|
||||
} else {
|
||||
const response = await api.get('/user/config/Dashboard')
|
||||
if (response && response.data && response.data.value) {
|
||||
enableConfig.value = response.data.value
|
||||
localStorage.setItem('MP_DASHBOARD', JSON.stringify(response.data.value))
|
||||
}
|
||||
}
|
||||
// 顺序配置
|
||||
const local_order = localStorage.getItem('MP_DASHBOARD_ORDER')
|
||||
if (local_order) {
|
||||
orderConfig.value = JSON.parse(local_order)
|
||||
} else {
|
||||
const response2 = await api.get('/user/config/DashboardOrder')
|
||||
if (response2 && response2.data && response2.data.value) {
|
||||
orderConfig.value = response2.data.value
|
||||
localStorage.setItem('MP_DASHBOARD_ORDER', JSON.stringify(orderConfig.value))
|
||||
}
|
||||
}
|
||||
// 排序
|
||||
if (orderConfig.value) {
|
||||
sortDashboardConfigs()
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化默认值
|
||||
const config = ref(JSON.parse(localStorage.getItem('MP_DASHBOARD') || '{}'))
|
||||
if (isNullOrEmptyObject(config.value)) {
|
||||
config.value = default_config
|
||||
// 按order的顺序对dashboardConfigs进行排序
|
||||
function sortDashboardConfigs() {
|
||||
dashboardConfigs.value.sort((a, b) => {
|
||||
const aIndex = orderConfig.value.findIndex((item: { id: string, key: string }) => item.id === a.id && item.key === a.key)
|
||||
const bIndex = orderConfig.value.findIndex((item: { id: string, key: string }) => item.id === b.id && item.key === b.key)
|
||||
return (aIndex === -1 ? 999 : aIndex) - (bIndex === -1 ? 999 : bIndex)
|
||||
})
|
||||
}
|
||||
|
||||
// 设置项目
|
||||
function setDashboardConfig() {
|
||||
const data = JSON.stringify(config.value)
|
||||
function saveDashboardConfig() {
|
||||
// 启用配置
|
||||
const data = JSON.stringify(enableConfig.value)
|
||||
localStorage.setItem('MP_DASHBOARD', data)
|
||||
// 顺序配置,从dashboardConfigs中提取
|
||||
const order = JSON.stringify(dashboardConfigs.value.map(item => ({ id: item.id, key: item.key })))
|
||||
localStorage.setItem('MP_DASHBOARD_ORDER', order)
|
||||
// 保存到服务端
|
||||
api.post('/user/config/Dashboard', data, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
try {
|
||||
api.post('/user/config/Dashboard', data, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
api.post('/user/config/DashboardOrder', order, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
// 保存后重新获取插件仪表板
|
||||
getPluginDashboardMeta()
|
||||
dialog.value = false
|
||||
}
|
||||
|
||||
// 构造插件仪表板主ID
|
||||
function buildPluginDashboardId(plugin_id: string, key: string) {
|
||||
if (!key) return plugin_id
|
||||
return plugin_id + ':' + key
|
||||
}
|
||||
|
||||
// 调用API获取所有插件的仪表板元信息
|
||||
async function getPluginDashboardMeta() {
|
||||
// 只有超级用户才能获取
|
||||
if (!superUser) return
|
||||
pluginDashboardMeta.value = await api.get('/plugin/dashboard/meta')
|
||||
try {
|
||||
if (!isNullOrEmptyObject(pluginDashboardMeta.value)) {
|
||||
// 下载插件仪表板配置
|
||||
pluginDashboardMeta.value.forEach(async (pluginDashboard: { id: string, key: string }) => {
|
||||
const pluginDashboardId = buildPluginDashboardId(pluginDashboard.id, pluginDashboard.key)
|
||||
// 初始化插件仪表板的刷新状态
|
||||
pluginDashboardRefreshStatus.value[pluginDashboardId] = true
|
||||
await getPluginDashboard(pluginDashboard.id, pluginDashboard.key)
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取一个插件的仪表板配置项
|
||||
async function getPluginDashboard(id: string, key: string) {
|
||||
try {
|
||||
const url = key ? `/plugin/dashboard/${id}/${key}` : `/plugin/dashboard/${id}`
|
||||
api.get(url).then((res: any) => {
|
||||
if (res) {
|
||||
// 名称替换为元信息的名称
|
||||
const meta = pluginDashboardMeta.value.find((item: { id: string, key: string }) => item.id === id && item.key === key)
|
||||
if (meta) res.name = meta.name
|
||||
// 保存到仪表板配置中,如果已经存在则替换
|
||||
const index = dashboardConfigs.value.findIndex((item: { id: string, key: string }) => item.id === id && item.key === key)
|
||||
if (index !== -1) {
|
||||
dashboardConfigs.value[index] = res
|
||||
} else {
|
||||
dashboardConfigs.value.push(res)
|
||||
// 排序
|
||||
sortDashboardConfigs()
|
||||
}
|
||||
const pluginDashboardId = buildPluginDashboardId(id, key)
|
||||
// 定时刷新
|
||||
if (res.attrs?.refresh && pluginDashboardRefreshStatus.value[pluginDashboardId] && enableConfig.value[pluginDashboardId]) {
|
||||
setTimeout(() => {
|
||||
getPluginDashboard(id, key)
|
||||
}, res.attrs.refresh * 1000)
|
||||
}
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 拖动排序结束
|
||||
function dragOrderEnd() {
|
||||
// 保存数据
|
||||
saveDashboardConfig()
|
||||
}
|
||||
|
||||
onBeforeMount(async () => {
|
||||
await loadDashboardConfig()
|
||||
getPluginDashboardMeta()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 仪表板 -->
|
||||
<draggable
|
||||
v-model="dashboardConfigs"
|
||||
@end="dragOrderEnd"
|
||||
handle=".cursor-move"
|
||||
item-key="id"
|
||||
tag="VRow"
|
||||
:component-data="{ 'class': 'match-height' }"
|
||||
>
|
||||
<template #item="{ element }">
|
||||
<VCol v-if="enableConfig[buildPluginDashboardId(element.id, element.key)] && element.cols" v-bind:="element.cols">
|
||||
<DashboardElement :config="element" v-model:refreshStatus="pluginDashboardRefreshStatus[buildPluginDashboardId(element.id, element.key)]" />
|
||||
</VCol>
|
||||
</template>
|
||||
</draggable>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<VFab icon="mdi-view-dashboard-edit" location="bottom end" size="x-large" fixed app appear @click="dialog = true" />
|
||||
<VRow class="match-height">
|
||||
<VCol v-if="config.storage" cols="12" md="4">
|
||||
<AnalyticsStorage />
|
||||
</VCol>
|
||||
|
||||
<VCol v-if="config.mediaStatistic" cols="12" md="8">
|
||||
<AnalyticsMediaStatistic />
|
||||
</VCol>
|
||||
|
||||
<VCol v-if="config.weeklyOverview" cols="12" md="4">
|
||||
<AnalyticsWeeklyOverview />
|
||||
</VCol>
|
||||
|
||||
<VCol v-if="config.speed" cols="12" md="4">
|
||||
<AnalyticsSpeed />
|
||||
</VCol>
|
||||
|
||||
<VCol v-if="config.scheduler" cols="12" md="4">
|
||||
<AnalyticsScheduler />
|
||||
</VCol>
|
||||
|
||||
<VCol v-if="config.cpu" cols="12" md="6">
|
||||
<AnalyticsCpu />
|
||||
</VCol>
|
||||
|
||||
<VCol v-if="config.memory" cols="12" md="6">
|
||||
<AnalyticsMemory />
|
||||
</VCol>
|
||||
|
||||
<VCol v-if="config.library" cols="12">
|
||||
<MediaServerLibrary />
|
||||
</VCol>
|
||||
|
||||
<VCol v-if="config.playing" cols="12">
|
||||
<MediaServerPlaying />
|
||||
</VCol>
|
||||
|
||||
<VCol v-if="config.latest" cols="12">
|
||||
<MediaServerLatest />
|
||||
</VCol>
|
||||
</VRow>
|
||||
<!-- 弹窗,根据配置生成选项 -->
|
||||
<VDialog v-model="dialog" max-width="40rem" scrollable :fullscreen="!display.mdAndUp.value">
|
||||
<VCard title="设置仪表板">
|
||||
<VDialog v-model="dialog" max-width="35rem" scrollable>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>设置仪表板</VCardTitle>
|
||||
</VCardItem>
|
||||
<VDivider />
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol v-for="(item, key) in dashboard_names" :key="key" cols="12" md="4" sm="4">
|
||||
<VCheckbox v-model="config[key]" :label="dashboard_names[key]" />
|
||||
<VCol v-for="item in dashboardConfigs" :key="buildPluginDashboardId(item.id, item.key)" cols="6" md="4" sm="4">
|
||||
<VCheckbox v-model="enableConfig[buildPluginDashboardId(item.id, item.key)]" :label="item.attrs?.title ?? item.name" />
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VBtn color="primary" @click="dialog = false"> 取消 </VBtn>
|
||||
<VDivider />
|
||||
<VCardText class="pt-5 text-end">
|
||||
<VSpacer />
|
||||
<VBtn color="primary" variant="tonal" @click="setDashboardConfig"> 保存 </VBtn>
|
||||
</VCardActions>
|
||||
<VBtn variant="outlined" color="secondary" class="me-4" @click="dialog = false"> 关闭 </VBtn>
|
||||
<VBtn @click="saveDashboardConfig">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-content-save" />
|
||||
</template>
|
||||
保存
|
||||
</VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
@@ -70,25 +70,6 @@ const fetchOTP = debounce(async () => {
|
||||
})
|
||||
}, 500)
|
||||
|
||||
// 加载用户监控面板配置
|
||||
async function loadDashboardConfig() {
|
||||
const response = await api.get('/user/config/Dashboard')
|
||||
if (response && response.data && response.data.value) {
|
||||
const data = JSON.stringify(response.data.value)
|
||||
if (data != localStorage.getItem('MP_DASHBOARD')) {
|
||||
localStorage.setItem('MP_DASHBOARD', data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试加载用户监控面板配置(本地无配置时才加载)
|
||||
async function tryLoadDashboardConfig() {
|
||||
if (localStorage.getItem('MP_DASHBOARD')) {
|
||||
return
|
||||
}
|
||||
await loadDashboardConfig()
|
||||
}
|
||||
|
||||
// 获取用户主题配置
|
||||
async function fetchThemeConfig() {
|
||||
const response = await api.get('/user/config/theme')
|
||||
@@ -111,8 +92,6 @@ async function setTheme() {
|
||||
async function afterLogin() {
|
||||
// 生效主题配置
|
||||
await setTheme()
|
||||
// 尝试加载用户监控面板配置(本地无配置时才加载)
|
||||
await tryLoadDashboardConfig()
|
||||
// 跳转到首页或回原始页面
|
||||
router.push(store.state.auth.originalPath ?? '/')
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ function startLoadingProgress() {
|
||||
progressEventSource.value = new EventSource(
|
||||
`${import.meta.env.VITE_API_BASE_URL}system/progress/search?token=${token}`,
|
||||
)
|
||||
progressEventSource.value.onmessage = (event) => {
|
||||
progressEventSource.value.onmessage = event => {
|
||||
const progress = JSON.parse(event.data)
|
||||
if (progress) {
|
||||
progressText.value = progress.text
|
||||
@@ -80,34 +80,33 @@ async function fetchData() {
|
||||
if (!keyword) {
|
||||
// 查询上次搜索结果
|
||||
dataList.value = await api.get('search/last')
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
startLoadingProgress()
|
||||
// 优先按TMDBID精确查询
|
||||
if (keyword?.startsWith('tmdb:') || keyword?.startsWith('douban:') || keyword?.startsWith('bangumi:')) {
|
||||
const result: {[key: string]: any} = await api.get(`search/media/${keyword}`, {
|
||||
const result: { [key: string]: any } = await api.get(`search/media/${keyword}`, {
|
||||
params: {
|
||||
mtype: type,
|
||||
area,
|
||||
season,
|
||||
},
|
||||
})
|
||||
if (result.success){
|
||||
if (result.success) {
|
||||
dataList.value = result.data
|
||||
} else {
|
||||
errorDescription.value = result.message
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// 按标题模糊查询
|
||||
dataList.value = await api.get(`search/title/${keyword}`)
|
||||
}
|
||||
stopLoadingProgress()
|
||||
// 从浏览器历史中删除当前搜索
|
||||
window.history.replaceState(null, '', window.location.pathname)
|
||||
}
|
||||
// 标记已刷新
|
||||
isRefreshed.value = true
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
return Promise.reject(error)
|
||||
}
|
||||
@@ -120,26 +119,15 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LoadingBanner
|
||||
v-if="!isRefreshed"
|
||||
class="mt-12"
|
||||
:text="progressText"
|
||||
:progress="progressValue"
|
||||
/>
|
||||
<LoadingBanner v-if="!isRefreshed" class="mt-12" :text="progressText" :progress="progressValue" />
|
||||
<NoDataFound
|
||||
v-if="dataList.length === 0 && isRefreshed"
|
||||
:error-title="errorTitle"
|
||||
:error-description="errorDescription"
|
||||
/>
|
||||
<div v-if="dataList.length > 0">
|
||||
<TorrentRowListView
|
||||
v-if="viewType === 'list'"
|
||||
:items="dataList"
|
||||
/>
|
||||
<TorrentCardListView
|
||||
v-else
|
||||
:items="dataList"
|
||||
/>
|
||||
<TorrentRowListView v-if="viewType === 'list'" :items="dataList" />
|
||||
<TorrentCardListView v-else :items="dataList" />
|
||||
</div>
|
||||
<!-- 视图切换 -->
|
||||
<VFab
|
||||
|
||||
@@ -9,6 +9,7 @@ import AccountSettingSearch from '@/views/setting/AccountSettingSearch.vue'
|
||||
import AccountSettingSubscribe from '@/views/setting/AccountSettingSubscribe.vue'
|
||||
import AccountSettingService from '@/views/setting/AccountSettingService.vue'
|
||||
import AccountSettingSystem from '@/views/setting/AccountSettingSystem.vue'
|
||||
import AccountSettingDirectory from '@/views/setting/AccountSettingDirectory.vue'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
@@ -22,10 +23,15 @@ const tabs = [
|
||||
tab: 'account',
|
||||
},
|
||||
{
|
||||
title: '系统',
|
||||
icon: 'mdi-cog',
|
||||
title: '连接',
|
||||
icon: 'mdi-server-network',
|
||||
tab: 'system',
|
||||
},
|
||||
{
|
||||
title: '目录',
|
||||
icon: 'mdi-folder',
|
||||
tab: 'directory',
|
||||
},
|
||||
{
|
||||
title: '站点',
|
||||
icon: 'mdi-web',
|
||||
@@ -66,13 +72,12 @@ const tabs = [
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<VTabs v-model="activeTab" show-arrows>
|
||||
<VTabs v-model="activeTab" show-arrows class="v-tabs-pill">
|
||||
<VTab v-for="item in tabs" :key="item.icon" :value="item.tab">
|
||||
<VIcon size="20" start :icon="item.icon" />
|
||||
{{ item.title }}
|
||||
</VTab>
|
||||
</VTabs>
|
||||
<VDivider />
|
||||
|
||||
<VWindow v-model="activeTab" class="mt-5 disable-tab-transition" :touch="false">
|
||||
<!-- 用户 -->
|
||||
@@ -82,13 +87,20 @@ const tabs = [
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
|
||||
<!-- 系统 -->
|
||||
<!-- 连接 -->
|
||||
<VWindowItem value="system">
|
||||
<transition name="fade-slide" appear>
|
||||
<AccountSettingSystem />
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
|
||||
<!-- 目录 -->
|
||||
<VWindowItem value="directory">
|
||||
<transition name="fade-slide" appear>
|
||||
<AccountSettingDirectory />
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
|
||||
<!-- 站点 -->
|
||||
<VWindowItem value="site">
|
||||
<transition name="fade-slide" appear>
|
||||
@@ -123,12 +135,14 @@ const tabs = [
|
||||
<AccountSettingNotification />
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
|
||||
<!-- 词表 -->
|
||||
<VWindowItem value="words">
|
||||
<transition name="fade-slide" appear>
|
||||
<AccountSettingWords />
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
|
||||
<!-- 关于 -->
|
||||
<VWindowItem value="about">
|
||||
<transition name="fade-slide" appear>
|
||||
|
||||
@@ -1,9 +1,44 @@
|
||||
<script setup lang="ts">
|
||||
import SubscribeListView from '@/views/subscribe/SubscribeListView.vue'
|
||||
import SubscribePopularView from '@/views/subscribe/SubscribePopularView.vue'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
// 标签页
|
||||
const tabs = [
|
||||
{
|
||||
title: '我的订阅',
|
||||
tab: 'mysub',
|
||||
},
|
||||
{
|
||||
title: '热门订阅',
|
||||
tab: 'popular',
|
||||
},
|
||||
]
|
||||
|
||||
// 当前标签
|
||||
const activeTab = ref(route.params.tab)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<SubscribeListView type="电影" />
|
||||
<VTabs v-model="activeTab">
|
||||
<VTab v-for="item in tabs" :value="item.tab">
|
||||
<span class="mx-5">{{ item.title }}</span>
|
||||
</VTab>
|
||||
</VTabs>
|
||||
|
||||
<VWindow v-model="activeTab" class="mt-5 disable-tab-transition" :touch="false">
|
||||
<VWindowItem value="mysub">
|
||||
<transition name="fade-slide" appear>
|
||||
<SubscribeListView type="电影" />
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
<VWindowItem value="popular">
|
||||
<transition name="fade-slide" appear>
|
||||
<SubscribePopularView type="电影" />
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
</VWindow>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,9 +1,44 @@
|
||||
<script setup lang="ts">
|
||||
import SubscribeListView from '@/views/subscribe/SubscribeListView.vue'
|
||||
import SubscribePopularView from '@/views/subscribe/SubscribePopularView.vue'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
// 标签页
|
||||
const tabs = [
|
||||
{
|
||||
title: '我的订阅',
|
||||
tab: 'mysub',
|
||||
},
|
||||
{
|
||||
title: '热门订阅',
|
||||
tab: 'popular',
|
||||
},
|
||||
]
|
||||
|
||||
// 当前标签
|
||||
const activeTab = ref(route.params.tab)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<SubscribeListView type="电视剧" />
|
||||
<VTabs v-model="activeTab">
|
||||
<VTab v-for="item in tabs" :value="item.tab">
|
||||
<span class="mx-5">{{ item.title }}</span>
|
||||
</VTab>
|
||||
</VTabs>
|
||||
|
||||
<VWindow v-model="activeTab" class="mt-5 disable-tab-transition" :touch="false">
|
||||
<VWindowItem value="mysub">
|
||||
<transition name="fade-slide" appear>
|
||||
<SubscribeListView type="电视剧" />
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
<VWindowItem value="popular">
|
||||
<transition name="fade-slide" appear>
|
||||
<SubscribePopularView type="电视剧" />
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
</VWindow>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -159,10 +159,11 @@ const router = createRouter({
|
||||
|
||||
// 路由导航守卫
|
||||
router.beforeEach((to, from, next) => {
|
||||
// 总是记录非login路由
|
||||
if (to.fullPath != '/login') store.state.auth.originalPath = to.fullPath
|
||||
const isAuthenticated = store.state.auth.token !== null
|
||||
|
||||
if (to.meta.requiresAuth && !isAuthenticated) {
|
||||
store.state.auth.originalPath = to.fullPath
|
||||
next('/login')
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -130,3 +130,61 @@
|
||||
.v-toast {
|
||||
z-index: 2500 !important;
|
||||
}
|
||||
|
||||
.v-divider {
|
||||
border-color: rgba(var(--v-theme-on-background), var(--v-selected-opacity));
|
||||
opacity:0.75;
|
||||
}
|
||||
|
||||
.apexcharts-title-text {
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity)) !important;
|
||||
}
|
||||
|
||||
.grid-site-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
|
||||
.grid-media-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(9.375rem, 1fr));
|
||||
}
|
||||
|
||||
.grid-backdrop-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
|
||||
.grid-torrent-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
|
||||
.grid-plugin-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
|
||||
.grid-downloading-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
|
||||
.grid-directory-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
|
||||
|
||||
.grid-filterrule-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
|
||||
.grid-subscribe-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(18rem, 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));
|
||||
}
|
||||
|
||||
@@ -6,8 +6,14 @@ import api from '@/api'
|
||||
|
||||
const vuetifyTheme = useTheme()
|
||||
|
||||
const currentTheme = controlledComputed(() => vuetifyTheme.name.value, () => vuetifyTheme.current.value.colors)
|
||||
const variableTheme = controlledComputed(() => vuetifyTheme.name.value, () => vuetifyTheme.current.value.variables)
|
||||
const currentTheme = controlledComputed(
|
||||
() => vuetifyTheme.name.value,
|
||||
() => vuetifyTheme.current.value.colors,
|
||||
)
|
||||
const variableTheme = controlledComputed(
|
||||
() => vuetifyTheme.name.value,
|
||||
() => vuetifyTheme.current.value.variables,
|
||||
)
|
||||
|
||||
// 定时器
|
||||
let refreshTimer: NodeJS.Timer | null = null
|
||||
@@ -22,83 +28,86 @@ const series = ref([
|
||||
// 当前值
|
||||
const current = ref(0)
|
||||
|
||||
const chartOptions = controlledComputed(() => vuetifyTheme.name.value, () => {
|
||||
return {
|
||||
chart: {
|
||||
parentHeightOffset: 0,
|
||||
toolbar: { show: false },
|
||||
animations: { enabled: false },
|
||||
},
|
||||
tooltip: { enabled: false },
|
||||
grid: {
|
||||
borderColor: `rgba(${hexToRgb(String(variableTheme.value['border-color']))},${variableTheme.value['border-opacity']})`,
|
||||
strokeDashArray: 6,
|
||||
const chartOptions = controlledComputed(
|
||||
() => vuetifyTheme.name.value,
|
||||
() => {
|
||||
return {
|
||||
chart: {
|
||||
parentHeightOffset: 0,
|
||||
toolbar: { show: false },
|
||||
animations: { enabled: false },
|
||||
},
|
||||
tooltip: { enabled: false },
|
||||
grid: {
|
||||
borderColor: `rgba(${hexToRgb(String(variableTheme.value['border-color']))},${
|
||||
variableTheme.value['border-opacity']
|
||||
})`,
|
||||
strokeDashArray: 6,
|
||||
xaxis: {
|
||||
lines: { show: false },
|
||||
},
|
||||
yaxis: {
|
||||
lines: { show: true },
|
||||
},
|
||||
padding: {
|
||||
top: -10,
|
||||
left: -7,
|
||||
right: 5,
|
||||
bottom: 5,
|
||||
},
|
||||
},
|
||||
stroke: {
|
||||
width: 3,
|
||||
lineCap: 'butt',
|
||||
curve: 'smooth',
|
||||
},
|
||||
colors: [currentTheme.value.primary],
|
||||
markers: {
|
||||
size: 6,
|
||||
offsetY: 4,
|
||||
offsetX: -2,
|
||||
strokeWidth: 3,
|
||||
colors: ['transparent'],
|
||||
strokeColors: 'transparent',
|
||||
discrete: [
|
||||
{
|
||||
size: 5.5,
|
||||
seriesIndex: 0,
|
||||
strokeColor: currentTheme.value.primary,
|
||||
fillColor: currentTheme.value.surface,
|
||||
},
|
||||
],
|
||||
hover: { size: 7 },
|
||||
},
|
||||
xaxis: {
|
||||
lines: { show: false },
|
||||
labels: { show: false },
|
||||
axisTicks: { show: false },
|
||||
axisBorder: { show: false },
|
||||
},
|
||||
yaxis: {
|
||||
lines: { show: true },
|
||||
labels: { show: false },
|
||||
max: 100,
|
||||
},
|
||||
padding: {
|
||||
top: -10,
|
||||
left: -7,
|
||||
right: 5,
|
||||
bottom: 5,
|
||||
},
|
||||
},
|
||||
stroke: {
|
||||
width: 3,
|
||||
lineCap: 'butt',
|
||||
curve: 'smooth',
|
||||
},
|
||||
colors: [currentTheme.value.primary],
|
||||
markers: {
|
||||
size: 6,
|
||||
offsetY: 4,
|
||||
offsetX: -2,
|
||||
strokeWidth: 3,
|
||||
colors: ['transparent'],
|
||||
strokeColors: 'transparent',
|
||||
discrete: [
|
||||
{
|
||||
size: 5.5,
|
||||
seriesIndex: 0,
|
||||
strokeColor: currentTheme.value.primary,
|
||||
fillColor: currentTheme.value.surface,
|
||||
},
|
||||
],
|
||||
hover: { size: 7 },
|
||||
},
|
||||
xaxis: {
|
||||
labels: { show: false },
|
||||
axisTicks: { show: false },
|
||||
axisBorder: { show: false },
|
||||
},
|
||||
yaxis: {
|
||||
labels: { show: false },
|
||||
max: 100,
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// 调用API接口获取最新CPU使用率
|
||||
async function getCpuUsage() {
|
||||
try {
|
||||
// 请求数据
|
||||
current.value = await api.get('dashboard/cpu') ?? 0
|
||||
current.value = (await api.get('dashboard/cpu')) ?? 0
|
||||
// 添加到序列
|
||||
series.value[0].data.push(current.value)
|
||||
// 序列超过30条记录时,清掉前面的
|
||||
if (series.value[0].data.length > 30)
|
||||
series.value[0].data.shift()
|
||||
}
|
||||
catch (e) {
|
||||
if (series.value[0].data.length > 30) series.value[0].data.shift()
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getCpuUsage()// 启动定时器
|
||||
getCpuUsage() // 启动定时器
|
||||
refreshTimer = setInterval(() => {
|
||||
getCpuUsage()
|
||||
}, 2000)
|
||||
@@ -114,21 +123,21 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard>
|
||||
<VCardText>
|
||||
<h6 class="text-h6">
|
||||
CPU
|
||||
</h6>
|
||||
<VueApexCharts
|
||||
type="line"
|
||||
:options="chartOptions"
|
||||
:series="series"
|
||||
:height="150"
|
||||
/>
|
||||
<VHover>
|
||||
<template #default="hover">
|
||||
<VCard v-bind="hover.props">
|
||||
<VCardItem>
|
||||
<template #append>
|
||||
<VIcon class="cursor-move" v-if="hover.isHovering">mdi-drag</VIcon>
|
||||
</template>
|
||||
<VCardTitle>CPU</VCardTitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VueApexCharts type="line" :options="chartOptions" :series="series" :height="150" />
|
||||
|
||||
<p class="text-center font-weight-medium mb-0">
|
||||
当前:{{ current }}%
|
||||
</p>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
<p class="text-center font-weight-medium mb-0">当前:{{ current }}%</p>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</template>
|
||||
</VHover>
|
||||
</template>
|
||||
|
||||
@@ -42,8 +42,7 @@ async function loadMediaStatistic() {
|
||||
color: 'info',
|
||||
},
|
||||
]
|
||||
}
|
||||
catch (e) {
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
@@ -54,43 +53,37 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>媒体统计</VCardTitle>
|
||||
</VCardItem>
|
||||
<VHover>
|
||||
<template #default="hover">
|
||||
<VCard v-bind="hover.props">
|
||||
<VCardItem>
|
||||
<template #append>
|
||||
<VIcon class="cursor-move" v-if="hover.isHovering">mdi-drag</VIcon>
|
||||
</template>
|
||||
<VCardTitle>媒体统计</VCardTitle>
|
||||
</VCardItem>
|
||||
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol
|
||||
v-for="item in statistics"
|
||||
:key="item.title"
|
||||
cols="6"
|
||||
sm="3"
|
||||
>
|
||||
<div class="d-flex align-center">
|
||||
<div class="me-3">
|
||||
<VAvatar
|
||||
:color="item.color"
|
||||
rounded
|
||||
size="42"
|
||||
class="elevation-1"
|
||||
>
|
||||
<VIcon
|
||||
size="24"
|
||||
:icon="item.icon"
|
||||
/>
|
||||
</VAvatar>
|
||||
</div>
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol v-for="item in statistics" :key="item.title" cols="6" sm="3">
|
||||
<div class="d-flex align-center">
|
||||
<div class="me-3">
|
||||
<VAvatar :color="item.color" rounded size="42" class="elevation-1">
|
||||
<VIcon size="24" :icon="item.icon" />
|
||||
</VAvatar>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-column">
|
||||
<span class="text-caption">
|
||||
{{ item.title }}
|
||||
</span>
|
||||
<span class="text-h6">{{ item.stats }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
<div class="d-flex flex-column">
|
||||
<span class="text-caption">
|
||||
{{ item.title }}
|
||||
</span>
|
||||
<span class="text-h6">{{ item.stats }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</template>
|
||||
</VHover>
|
||||
</template>
|
||||
|
||||
@@ -7,8 +7,14 @@ import { formatBytes } from '@/@core/utils/formatters'
|
||||
|
||||
const vuetifyTheme = useTheme()
|
||||
|
||||
const currentTheme = controlledComputed(() => vuetifyTheme.name.value, () => vuetifyTheme.current.value.colors)
|
||||
const variableTheme = controlledComputed(() => vuetifyTheme.name.value, () => vuetifyTheme.current.value.variables)
|
||||
const currentTheme = controlledComputed(
|
||||
() => vuetifyTheme.name.value,
|
||||
() => vuetifyTheme.current.value.colors,
|
||||
)
|
||||
const variableTheme = controlledComputed(
|
||||
() => vuetifyTheme.name.value,
|
||||
() => vuetifyTheme.current.value.variables,
|
||||
)
|
||||
|
||||
// 定时器
|
||||
let refreshTimer: NodeJS.Timer | null = null
|
||||
@@ -25,79 +31,82 @@ const usedMemory = ref(0)
|
||||
// 内存使用百分比
|
||||
const memoryUsage = ref(0)
|
||||
|
||||
const chartOptions = controlledComputed(() => vuetifyTheme.name.value, () => {
|
||||
return {
|
||||
chart: {
|
||||
parentHeightOffset: 0,
|
||||
toolbar: { show: false },
|
||||
animations: { enabled: false },
|
||||
},
|
||||
tooltip: { enabled: false },
|
||||
grid: {
|
||||
borderColor: `rgba(${hexToRgb(String(variableTheme.value['border-color']))},${variableTheme.value['border-opacity']})`,
|
||||
strokeDashArray: 6,
|
||||
const chartOptions = controlledComputed(
|
||||
() => vuetifyTheme.name.value,
|
||||
() => {
|
||||
return {
|
||||
chart: {
|
||||
parentHeightOffset: 0,
|
||||
toolbar: { show: false },
|
||||
animations: { enabled: false },
|
||||
},
|
||||
tooltip: { enabled: false },
|
||||
grid: {
|
||||
borderColor: `rgba(${hexToRgb(String(variableTheme.value['border-color']))},${
|
||||
variableTheme.value['border-opacity']
|
||||
})`,
|
||||
strokeDashArray: 6,
|
||||
xaxis: {
|
||||
lines: { show: false },
|
||||
},
|
||||
yaxis: {
|
||||
lines: { show: true },
|
||||
},
|
||||
padding: {
|
||||
top: -10,
|
||||
left: -7,
|
||||
right: 5,
|
||||
bottom: 5,
|
||||
},
|
||||
},
|
||||
stroke: {
|
||||
width: 3,
|
||||
lineCap: 'butt',
|
||||
curve: 'smooth',
|
||||
},
|
||||
colors: [currentTheme.value.primary],
|
||||
markers: {
|
||||
size: 6,
|
||||
offsetY: 4,
|
||||
offsetX: -2,
|
||||
strokeWidth: 3,
|
||||
colors: ['transparent'],
|
||||
strokeColors: 'transparent',
|
||||
discrete: [
|
||||
{
|
||||
size: 5.5,
|
||||
seriesIndex: 0,
|
||||
strokeColor: currentTheme.value.primary,
|
||||
fillColor: currentTheme.value.surface,
|
||||
},
|
||||
],
|
||||
hover: { size: 7 },
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false,
|
||||
},
|
||||
xaxis: {
|
||||
lines: { show: false },
|
||||
labels: { show: false },
|
||||
axisTicks: { show: false },
|
||||
axisBorder: { show: false },
|
||||
},
|
||||
yaxis: {
|
||||
lines: { show: true },
|
||||
labels: { show: false },
|
||||
max: 100,
|
||||
},
|
||||
padding: {
|
||||
top: -10,
|
||||
left: -7,
|
||||
right: 5,
|
||||
bottom: 5,
|
||||
},
|
||||
},
|
||||
stroke: {
|
||||
width: 3,
|
||||
lineCap: 'butt',
|
||||
curve: 'smooth',
|
||||
},
|
||||
colors: [currentTheme.value.primary],
|
||||
markers: {
|
||||
size: 6,
|
||||
offsetY: 4,
|
||||
offsetX: -2,
|
||||
strokeWidth: 3,
|
||||
colors: ['transparent'],
|
||||
strokeColors: 'transparent',
|
||||
discrete: [
|
||||
{
|
||||
size: 5.5,
|
||||
seriesIndex: 0,
|
||||
strokeColor: currentTheme.value.primary,
|
||||
fillColor: currentTheme.value.surface,
|
||||
},
|
||||
],
|
||||
hover: { size: 7 },
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false,
|
||||
},
|
||||
xaxis: {
|
||||
labels: { show: false },
|
||||
axisTicks: { show: false },
|
||||
axisBorder: { show: false },
|
||||
},
|
||||
yaxis: {
|
||||
labels: { show: false },
|
||||
max: 100,
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// 调用API接口获取最新内存使用量
|
||||
async function getMemorgUsage() {
|
||||
try {
|
||||
// 请求数据
|
||||
[usedMemory.value, memoryUsage.value] = await api.get('dashboard/memory')
|
||||
;[usedMemory.value, memoryUsage.value] = await api.get('dashboard/memory')
|
||||
series.value[0].data.push(memoryUsage.value)
|
||||
// 序列超过30条记录时,清掉前面的
|
||||
if (series.value[0].data.length > 30)
|
||||
series.value[0].data.shift()
|
||||
}
|
||||
catch (e) {
|
||||
if (series.value[0].data.length > 30) series.value[0].data.shift()
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
@@ -120,21 +129,21 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard>
|
||||
<VCardText>
|
||||
<h6 class="text-h6">
|
||||
内存
|
||||
</h6>
|
||||
<VueApexCharts
|
||||
type="area"
|
||||
:options="chartOptions"
|
||||
:series="series"
|
||||
:height="150"
|
||||
/>
|
||||
<VHover>
|
||||
<template #default="hover">
|
||||
<VCard v-bind="hover.props">
|
||||
<VCardItem>
|
||||
<template #append>
|
||||
<VIcon class="cursor-move" v-if="hover.isHovering">mdi-drag</VIcon>
|
||||
</template>
|
||||
<VCardTitle>内存</VCardTitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VueApexCharts type="area" :options="chartOptions" :series="series" :height="150" />
|
||||
|
||||
<p class="text-center font-weight-medium mb-0">
|
||||
当前:{{ formatBytes(usedMemory) }}
|
||||
</p>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
<p class="text-center font-weight-medium mb-0">当前:{{ formatBytes(usedMemory) }}</p>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</template>
|
||||
</VHover>
|
||||
</template>
|
||||
|
||||
@@ -18,8 +18,7 @@ async function loadProcessList() {
|
||||
const res: Process[] = await api.get('dashboard/processes')
|
||||
|
||||
processList.value = res
|
||||
}
|
||||
catch (e) {
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
@@ -43,47 +42,32 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard title="系统进程">
|
||||
<VTable
|
||||
item-key="fullName"
|
||||
class="table-rounded"
|
||||
hide-default-footer
|
||||
disable-sort
|
||||
>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<template #append>
|
||||
<VIcon class="cursor-move">mdi-drag</VIcon>
|
||||
</template>
|
||||
<VCardTitle>系统进程</VCardTitle>
|
||||
</VCardItem>
|
||||
<VTable item-key="fullName" class="table-rounded" hide-default-footer disable-sort>
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
v-for="header in headers"
|
||||
:id="header"
|
||||
:key="header"
|
||||
>
|
||||
<th v-for="header in headers" :id="header" :key="header">
|
||||
{{ header }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="row in processList"
|
||||
:key="row.pid"
|
||||
>
|
||||
<td
|
||||
class="text-sm"
|
||||
v-text="row.pid"
|
||||
/>
|
||||
<tr v-for="row in processList" :key="row.pid">
|
||||
<td class="text-sm" v-text="row.pid" />
|
||||
<!-- name -->
|
||||
<td>
|
||||
<h6 class="text-sm font-weight-medium">
|
||||
{{ row.name }}
|
||||
</h6>
|
||||
</td>
|
||||
<td
|
||||
class="text-sm"
|
||||
v-text="formatSeconds(row.run_time)"
|
||||
/>
|
||||
<td
|
||||
class="text-sm"
|
||||
v-text="`${row.memory} MB`"
|
||||
/>
|
||||
<td class="text-sm" v-text="formatSeconds(row.run_time)" />
|
||||
<td class="text-sm" v-text="`${row.memory} MB`" />
|
||||
</tr>
|
||||
</tbody>
|
||||
</VTable>
|
||||
|
||||
@@ -14,8 +14,7 @@ async function loadSchedulerList() {
|
||||
const res: ScheduleInfo[] = await api.get('dashboard/schedule')
|
||||
|
||||
schedulerList.value = res
|
||||
}
|
||||
catch (e) {
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
@@ -39,55 +38,49 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>后台任务</VCardTitle>
|
||||
</VCardItem>
|
||||
|
||||
<VCardText>
|
||||
<VList
|
||||
class="card-list"
|
||||
height="250"
|
||||
>
|
||||
<VListItem
|
||||
v-for="item in schedulerList"
|
||||
:key="item.id"
|
||||
>
|
||||
<template #prepend>
|
||||
<VAvatar
|
||||
size="40"
|
||||
variant="tonal"
|
||||
color=""
|
||||
class="me-3"
|
||||
>
|
||||
{{ item.name[0] }}
|
||||
</VAvatar>
|
||||
</template>
|
||||
|
||||
<VListItemTitle class="mb-1">
|
||||
<span class="text-sm font-weight-medium">{{ item.name }}</span>
|
||||
</VListItemTitle>
|
||||
|
||||
<VListItemSubtitle class="text-xs">
|
||||
{{ item.next_run }}
|
||||
</VListItemSubtitle>
|
||||
|
||||
<VHover>
|
||||
<template #default="hover">
|
||||
<VCard v-bind="hover.props">
|
||||
<VCardItem>
|
||||
<template #append>
|
||||
<div>
|
||||
<h4 class="font-weight-medium">
|
||||
{{ item.status }}
|
||||
</h4>
|
||||
</div>
|
||||
<VIcon class="cursor-move" v-if="hover.isHovering">mdi-drag</VIcon>
|
||||
</template>
|
||||
</VListItem>
|
||||
<VListItem v-if="schedulerList.length === 0">
|
||||
<VListItemTitle class="text-center">
|
||||
没有后台服务
|
||||
</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
<VCardTitle>后台任务</VCardTitle>
|
||||
</VCardItem>
|
||||
|
||||
<VCardText>
|
||||
<VList class="card-list" height="250">
|
||||
<VListItem v-for="item in schedulerList" :key="item.id">
|
||||
<template #prepend>
|
||||
<VAvatar size="40" variant="tonal" color="" class="me-3">
|
||||
{{ item.name[0] }}
|
||||
</VAvatar>
|
||||
</template>
|
||||
|
||||
<VListItemTitle class="mb-1">
|
||||
<span class="text-sm font-weight-medium">{{ item.name }}</span>
|
||||
</VListItemTitle>
|
||||
|
||||
<VListItemSubtitle class="text-xs">
|
||||
{{ item.next_run }}
|
||||
</VListItemSubtitle>
|
||||
|
||||
<template #append>
|
||||
<div>
|
||||
<h4 class="font-weight-medium">
|
||||
{{ item.status }}
|
||||
</h4>
|
||||
</div>
|
||||
</template>
|
||||
</VListItem>
|
||||
<VListItem v-if="schedulerList.length === 0">
|
||||
<VListItemTitle class="text-center"> 没有后台服务 </VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</template>
|
||||
</VHover>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -56,8 +56,7 @@ async function loadDownloaderInfo() {
|
||||
amount: formatFileSize(res.free_space),
|
||||
},
|
||||
]
|
||||
}
|
||||
catch (e) {
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
@@ -81,47 +80,44 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>实时速率</VCardTitle>
|
||||
</VCardItem>
|
||||
|
||||
<VCardText class="pt-4">
|
||||
<div>
|
||||
<p class="text-h5 me-2">
|
||||
↑{{ formatFileSize(downloadInfo.upload_speed) }}/s
|
||||
</p>
|
||||
<p class="text-h4 me-2">
|
||||
↓{{ formatFileSize(downloadInfo.download_speed) }}/s
|
||||
</p>
|
||||
</div>
|
||||
<VList class="card-list mt-9">
|
||||
<VListItem
|
||||
v-for="item in infoItems"
|
||||
:key="item.title"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon
|
||||
rounded
|
||||
:icon="item.avatar"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<VListItemTitle class="text-sm font-weight-medium mb-1">
|
||||
{{ item.title }}
|
||||
</VListItemTitle>
|
||||
|
||||
<VHover>
|
||||
<template #default="hover">
|
||||
<VCard v-bind="hover.props">
|
||||
<VCardItem>
|
||||
<template #append>
|
||||
<div>
|
||||
<h6 class="text-sm font-weight-medium mb-2">
|
||||
{{ item.amount }}
|
||||
</h6>
|
||||
</div>
|
||||
<VIcon class="cursor-move" v-if="hover.isHovering">mdi-drag</VIcon>
|
||||
</template>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
<VCardTitle>实时速率</VCardTitle>
|
||||
</VCardItem>
|
||||
|
||||
<VCardText class="pt-4">
|
||||
<div>
|
||||
<p class="text-h5 me-2">↑{{ formatFileSize(downloadInfo.upload_speed) }}/s</p>
|
||||
<p class="text-h4 me-2">↓{{ formatFileSize(downloadInfo.download_speed) }}/s</p>
|
||||
</div>
|
||||
<VList class="card-list mt-9">
|
||||
<VListItem v-for="item in infoItems" :key="item.title">
|
||||
<template #prepend>
|
||||
<VIcon rounded :icon="item.avatar" />
|
||||
</template>
|
||||
|
||||
<VListItemTitle class="text-sm font-weight-medium mb-1">
|
||||
{{ item.title }}
|
||||
</VListItemTitle>
|
||||
|
||||
<template #append>
|
||||
<div>
|
||||
<h6 class="text-sm font-weight-medium mb-2">
|
||||
{{ item.amount }}
|
||||
</h6>
|
||||
</div>
|
||||
</template>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</template>
|
||||
</VHover>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -8,9 +8,7 @@ import triangleLight from '@images/misc/triangle-light.png'
|
||||
|
||||
const { global } = useTheme()
|
||||
|
||||
const triangleBg = computed(() =>
|
||||
global.name.value === 'light' ? triangleLight : triangleDark,
|
||||
)
|
||||
const triangleBg = computed(() => (global.name.value === 'light' ? triangleLight : triangleDark))
|
||||
|
||||
// 总存储空间
|
||||
const storage = ref(0)
|
||||
@@ -30,8 +28,7 @@ async function getStorage() {
|
||||
|
||||
storage.value = res.total_storage
|
||||
used.value = res.used_storage
|
||||
}
|
||||
catch (e) {
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
@@ -42,42 +39,36 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard
|
||||
title="存储空间"
|
||||
subtitle=""
|
||||
class="position-relative"
|
||||
>
|
||||
<VCardText>
|
||||
<h5 class="text-2xl font-weight-medium text-primary">
|
||||
{{ formatFileSize(storage) }}
|
||||
</h5>
|
||||
<p class="mt-2">
|
||||
已使用 {{ usedPercent }}% 🚀
|
||||
</p>
|
||||
<p class="mt-1">
|
||||
<VProgressLinear
|
||||
:model-value="usedPercent"
|
||||
color="primary"
|
||||
/>
|
||||
</p>
|
||||
</VCardText>
|
||||
<VHover>
|
||||
<template #default="hover">
|
||||
<VCard v-bind="hover.props">
|
||||
<!-- Triangle Background -->
|
||||
<VImg :src="triangleBg" class="triangle-bg flip-in-rtl" />
|
||||
<VCardItem>
|
||||
<template #append>
|
||||
<VIcon class="cursor-move" v-if="hover.isHovering">mdi-drag</VIcon>
|
||||
</template>
|
||||
<VCardTitle>存储空间</VCardTitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<h5 class="text-2xl font-weight-medium text-primary">
|
||||
{{ formatFileSize(storage) }}
|
||||
</h5>
|
||||
<p class="mt-2">已使用 {{ usedPercent }}% 🚀</p>
|
||||
<p class="mt-1">
|
||||
<VProgressLinear :model-value="usedPercent" color="primary" />
|
||||
</p>
|
||||
</VCardText>
|
||||
|
||||
<!-- Triangle Background -->
|
||||
<VImg
|
||||
:src="triangleBg"
|
||||
class="triangle-bg flip-in-rtl"
|
||||
/>
|
||||
|
||||
<!-- Trophy -->
|
||||
<VImg
|
||||
:src="trophy"
|
||||
class="trophy"
|
||||
/>
|
||||
</VCard>
|
||||
<!-- Trophy -->
|
||||
<VImg :src="trophy" class="trophy" />
|
||||
</VCard>
|
||||
</template>
|
||||
</VHover>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@use "@layouts/styles/mixins" as layoutsMixins;
|
||||
@use '@layouts/styles/mixins' as layoutsMixins;
|
||||
|
||||
.v-card .triangle-bg {
|
||||
position: absolute;
|
||||
|
||||
@@ -80,8 +80,7 @@ const options = controlledComputed(
|
||||
fontSize: '12px',
|
||||
},
|
||||
|
||||
formatter: (value: number) =>
|
||||
value > 999 ? (value / 1000).toFixed(0) : value,
|
||||
formatter: (value: number) => (value > 999 ? (value / 1000).toFixed(0) : value),
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -100,8 +99,7 @@ async function getWeeklyData() {
|
||||
const res: number[] = await api.get('dashboard/transfer')
|
||||
|
||||
series.value = [{ data: res }]
|
||||
}
|
||||
catch (e) {
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
@@ -112,33 +110,29 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>最近入库</VCardTitle>
|
||||
</VCardItem>
|
||||
<VHover>
|
||||
<template #default="hover">
|
||||
<VCard v-bind="hover.props">
|
||||
<VCardItem>
|
||||
<template #append>
|
||||
<VIcon class="cursor-move" v-if="hover.isHovering">mdi-drag</VIcon>
|
||||
</template>
|
||||
<VCardTitle>最近入库</VCardTitle>
|
||||
</VCardItem>
|
||||
|
||||
<VCardText>
|
||||
<VueApexCharts
|
||||
type="bar"
|
||||
:options="options"
|
||||
:series="series"
|
||||
:height="160"
|
||||
/>
|
||||
<VCardText>
|
||||
<VueApexCharts type="bar" :options="options" :series="series" :height="160" />
|
||||
|
||||
<div class="d-flex align-center mb-3">
|
||||
<h5 class="text-h5 me-4">
|
||||
{{ totalCount }}
|
||||
</h5>
|
||||
<p>最近一周入库了 {{ totalCount }} 部影片 😎</p>
|
||||
</div>
|
||||
<div class="d-flex align-center mb-3">
|
||||
<h5 class="text-h5 me-4">
|
||||
{{ totalCount }}
|
||||
</h5>
|
||||
<p>最近一周入库了 {{ totalCount }} 部影片 😎</p>
|
||||
</div>
|
||||
|
||||
<VBtn
|
||||
v-if="superUser"
|
||||
block
|
||||
to="/history"
|
||||
>
|
||||
查看详情
|
||||
</VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
<VBtn v-if="superUser" block to="/history"> 查看详情 </VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</template>
|
||||
</VHover>
|
||||
</template>
|
||||
|
||||
@@ -10,8 +10,7 @@ const latestList = ref<MediaServerPlayItem[]>([])
|
||||
async function loadLatest() {
|
||||
try {
|
||||
latestList.value = await api.get('mediaserver/latest')
|
||||
}
|
||||
catch (e) {
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
@@ -22,27 +21,20 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>最近添加</VCardTitle>
|
||||
</VCardItem>
|
||||
<VHover>
|
||||
<template #default="hover">
|
||||
<VCard v-bind="hover.props">
|
||||
<VCardItem>
|
||||
<template #append>
|
||||
<VIcon class="cursor-move" v-if="hover.isHovering">mdi-drag</VIcon>
|
||||
</template>
|
||||
<VCardTitle >最近添加</VCardTitle>
|
||||
</VCardItem>
|
||||
|
||||
<div
|
||||
v-if="latestList.length > 0"
|
||||
class="grid gap-4 grid-media-card mx-3 mb-3"
|
||||
tabindex="0"
|
||||
>
|
||||
<PosterCard
|
||||
v-for="data in latestList"
|
||||
:key="data.id"
|
||||
:media="data"
|
||||
/>
|
||||
</div>
|
||||
</VCard>
|
||||
<div v-if="latestList.length > 0" class="grid gap-4 grid-media-card mx-3 mb-3" tabindex="0">
|
||||
<PosterCard v-for="data in latestList" :key="data.id" :media="data" />
|
||||
</div>
|
||||
</VCard>
|
||||
</template>
|
||||
</VHover>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.grid-media-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(9.375rem, 1fr));
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -10,8 +10,7 @@ const libraryList = ref<MediaServerPlayItem[]>([])
|
||||
async function loadLibrary() {
|
||||
try {
|
||||
libraryList.value = await api.get('mediaserver/library')
|
||||
}
|
||||
catch (e) {
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
@@ -22,29 +21,20 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>我的媒体库</VCardTitle>
|
||||
</VCardItem>
|
||||
<VHover>
|
||||
<template #default="hover">
|
||||
<VCard v-bind="hover.props">
|
||||
<VCardItem>
|
||||
<template #append>
|
||||
<VIcon class="cursor-move" v-if="hover.isHovering">mdi-drag</VIcon>
|
||||
</template>
|
||||
<VCardTitle >我的媒体库</VCardTitle>
|
||||
</VCardItem>
|
||||
|
||||
<div
|
||||
v-if="libraryList.length > 0"
|
||||
class="grid gap-4 grid-backdrop-card mx-3"
|
||||
tabindex="0"
|
||||
>
|
||||
<LibraryCard
|
||||
v-for="data in libraryList"
|
||||
:key="data.id"
|
||||
:media="data"
|
||||
height="10rem"
|
||||
/>
|
||||
</div>
|
||||
</VCard>
|
||||
<div v-if="libraryList.length > 0" class="grid gap-4 grid-backdrop-card mx-3" tabindex="0">
|
||||
<LibraryCard v-for="data in libraryList" :key="data.id" :media="data" height="10rem" />
|
||||
</div>
|
||||
</VCard>
|
||||
</template>
|
||||
</VHover>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.grid-backdrop-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -10,8 +10,7 @@ const playingList = ref<MediaServerPlayItem[]>([])
|
||||
async function loadPlayingList() {
|
||||
try {
|
||||
playingList.value = await api.get('mediaserver/playing')
|
||||
}
|
||||
catch (e) {
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
@@ -22,29 +21,20 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>继续观看</VCardTitle>
|
||||
</VCardItem>
|
||||
<VHover>
|
||||
<template #default="hover">
|
||||
<VCard v-bind="hover.props">
|
||||
<VCardItem>
|
||||
<template #append>
|
||||
<VIcon class="cursor-move" v-if="hover.isHovering">mdi-drag</VIcon>
|
||||
</template>
|
||||
<VCardTitle>继续观看</VCardTitle>
|
||||
</VCardItem>
|
||||
|
||||
<div
|
||||
v-if="playingList.length > 0"
|
||||
class="grid gap-4 grid-backdrop-card mx-3"
|
||||
tabindex="0"
|
||||
>
|
||||
<BackdropCard
|
||||
v-for="data in playingList"
|
||||
:key="data.id"
|
||||
:media="data"
|
||||
height="10rem"
|
||||
/>
|
||||
</div>
|
||||
</VCard>
|
||||
<div v-if="playingList.length > 0" class="grid gap-4 grid-backdrop-card mx-3" tabindex="0">
|
||||
<BackdropCard v-for="data in playingList" :key="data.id" :media="data" height="10rem" />
|
||||
</div>
|
||||
</VCard>
|
||||
</template>
|
||||
</VHover>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.grid-backdrop-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -125,7 +125,5 @@ async function fetchData({ done }: { done: any }) {
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.grid-media-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(9.375rem, 1fr));
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -9,6 +9,7 @@ import { doneNProgress, startNProgress } from '@/api/nprogress'
|
||||
import { formatSeason } from '@/@core/utils/formatters'
|
||||
import router from '@/router'
|
||||
import SubscribeEditDialog from '@/components/dialog/SubscribeEditDialog.vue'
|
||||
import { isNullOrEmptyObject } from '@/@core/utils'
|
||||
|
||||
// 输入参数
|
||||
const mediaProps = defineProps({
|
||||
@@ -16,6 +17,8 @@ const mediaProps = defineProps({
|
||||
type: String,
|
||||
})
|
||||
|
||||
const store = useStore()
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
@@ -37,6 +40,9 @@ const isRefreshed = ref(false)
|
||||
// 存储每一季的集信息
|
||||
const seasonEpisodesInfo = ref({} as { [key: number]: TmdbEpisode[] })
|
||||
|
||||
// 存储存在的季集
|
||||
const existsEpisodes = ref({} as { [key: number]: number[] })
|
||||
|
||||
// 各季缺失状态:0-已入库 1-部分缺失 2-全部缺失,没有数据也是已入库
|
||||
const seasonsNotExisted = ref<{ [key: number]: number }>({})
|
||||
|
||||
@@ -51,8 +57,8 @@ function getMediaId() {
|
||||
return mediaDetail.value?.tmdb_id
|
||||
? `tmdb:${mediaDetail.value?.tmdb_id}`
|
||||
: mediaDetail.value?.douban_id
|
||||
? `douban:${mediaDetail.value?.douban_id}`
|
||||
: `bangumi:${mediaDetail.value?.bangumi_id}`
|
||||
? `douban:${mediaDetail.value?.douban_id}`
|
||||
: `bangumi:${mediaDetail.value?.bangumi_id}`
|
||||
}
|
||||
|
||||
// 调用API查询详情
|
||||
@@ -64,36 +70,44 @@ async function getMediaDetail() {
|
||||
},
|
||||
})
|
||||
isRefreshed.value = true
|
||||
if (!mediaDetail.value.tmdb_id && !mediaDetail.value.douban_id && !mediaDetail.value.bangumi_id)
|
||||
return
|
||||
if (!mediaDetail.value.tmdb_id && !mediaDetail.value.douban_id && !mediaDetail.value.bangumi_id) return
|
||||
|
||||
// 检查存在状态
|
||||
checkExists()
|
||||
if (mediaDetail.value.type === '电视剧')
|
||||
checkSeasonsNotExists()
|
||||
if (mediaDetail.value.type === '电视剧') checkSeasonsNotExists()
|
||||
// 检查订阅状态
|
||||
if (mediaDetail.value.type === '电影')
|
||||
checkMovieSubscribed()
|
||||
else
|
||||
checkSeasonsSubscribed()
|
||||
if (mediaDetail.value.type === '电影') checkMovieSubscribed()
|
||||
else checkSeasonsSubscribed()
|
||||
}
|
||||
}
|
||||
|
||||
// 调用API加载季集信息
|
||||
// 调用API加载季集信息(TMDB)
|
||||
async function loadSeasonEpisodes(season: number) {
|
||||
if (seasonEpisodesInfo.value[season])
|
||||
return
|
||||
|
||||
// 加载季集存在信息
|
||||
loadEpisodeExists()
|
||||
// 加载季集信息
|
||||
if (seasonEpisodesInfo.value[season]) return
|
||||
try {
|
||||
const result: TmdbEpisode[] = await api.get(`tmdb/${mediaDetail.value.tmdb_id}/${season}`)
|
||||
seasonEpisodesInfo.value[season] = result || []
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 查询当前媒体是否已入库
|
||||
// 调用API加载季集存在信息(媒体服务器)
|
||||
async function loadEpisodeExists() {
|
||||
// 查询季集存在状态
|
||||
if (!isNullOrEmptyObject(existsEpisodes.value)) return
|
||||
try {
|
||||
const result: { [key: number]: number[] } = await api.post(`mediaserver/exists_remote`, mediaDetail.value)
|
||||
existsEpisodes.value = result || {}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 查询当前媒体是否已入库(数据库)
|
||||
async function checkExists() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('mediaserver/exists', {
|
||||
@@ -106,10 +120,8 @@ async function checkExists() {
|
||||
},
|
||||
})
|
||||
|
||||
if (result.success)
|
||||
existsItemId.value = result.data.item.id
|
||||
}
|
||||
catch (error) {
|
||||
if (result.success) existsItemId.value = result.data.item.id
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
@@ -126,10 +138,8 @@ async function checkSubscribe(season = 0) {
|
||||
},
|
||||
})
|
||||
|
||||
if (result.id)
|
||||
return true
|
||||
}
|
||||
catch (error) {
|
||||
if (result.id) return true
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
|
||||
@@ -138,31 +148,26 @@ async function checkSubscribe(season = 0) {
|
||||
|
||||
// 检查所有季的缺失状态
|
||||
async function checkSeasonsNotExists() {
|
||||
if (mediaDetail.value.type !== '电视剧')
|
||||
return
|
||||
if (mediaDetail.value.type !== '电视剧') return
|
||||
try {
|
||||
const result: NotExistMediaInfo[] = await api.post('mediaserver/notexists', mediaDetail.value)
|
||||
if (result) {
|
||||
result.forEach((item) => {
|
||||
result.forEach(item => {
|
||||
// 0-已入库 1-部分缺失 2-全部缺失
|
||||
let state = 0
|
||||
if (item.episodes.length === 0)
|
||||
state = 2
|
||||
else if (item.episodes.length < item.total_episode)
|
||||
state = 1
|
||||
if (item.episodes.length === 0) state = 2
|
||||
else if (item.episodes.length < item.total_episode) state = 1
|
||||
seasonsNotExisted.value[item.season] = state
|
||||
})
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 检查电影订阅状态
|
||||
async function checkMovieSubscribed() {
|
||||
if (mediaDetail.value.type !== '电影')
|
||||
return
|
||||
if (mediaDetail.value.type !== '电影') return
|
||||
isSubscribed.value = await checkSubscribe()
|
||||
}
|
||||
|
||||
@@ -173,14 +178,12 @@ const getMediaSeasons = computed(() => {
|
||||
|
||||
// 检查所有季的订阅状态
|
||||
async function checkSeasonsSubscribed() {
|
||||
if (mediaDetail.value.type !== '电视剧')
|
||||
return
|
||||
if (mediaDetail.value.type !== '电视剧') return
|
||||
try {
|
||||
mediaDetail.value?.season_info?.forEach(async (item) => {
|
||||
mediaDetail.value?.season_info?.forEach(async item => {
|
||||
seasonsSubscribed.value[item.season_number ?? 0] = await checkSubscribe(item.season_number)
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
@@ -211,18 +214,11 @@ async function addSubscribe(season = 0) {
|
||||
if (result.success) {
|
||||
// 订阅成功
|
||||
isSubscribed.value = true
|
||||
if (season)
|
||||
seasonsSubscribed.value[season] = true
|
||||
if (season) seasonsSubscribed.value[season] = true
|
||||
}
|
||||
|
||||
// 提示
|
||||
showSubscribeAddToast(
|
||||
result.success,
|
||||
mediaDetail.value?.title ?? '',
|
||||
season,
|
||||
result.message,
|
||||
best_version,
|
||||
)
|
||||
showSubscribeAddToast(result.success, mediaDetail.value?.title ?? '', season, result.message, best_version)
|
||||
|
||||
// 显示编辑弹窗
|
||||
if (result.success) {
|
||||
@@ -232,28 +228,20 @@ async function addSubscribe(season = 0) {
|
||||
subscribeEditDialog.value = true
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
doneNProgress()
|
||||
}
|
||||
|
||||
// 弹出添加订阅提示
|
||||
function showSubscribeAddToast(result: boolean,
|
||||
title: string,
|
||||
season: number,
|
||||
message: string,
|
||||
best_version: number) {
|
||||
if (season)
|
||||
title = `${title} ${formatSeason(season.toString())}`
|
||||
function showSubscribeAddToast(result: boolean, title: string, season: number, message: string, best_version: number) {
|
||||
if (season) title = `${title} ${formatSeason(season.toString())}`
|
||||
|
||||
let subname = '订阅'
|
||||
if (best_version > 0)
|
||||
subname = '洗版订阅'
|
||||
if (best_version > 0) subname = '洗版订阅'
|
||||
|
||||
if (!result)
|
||||
$toast.error(`${title} 添加${subname}失败:${message}!`)
|
||||
if (!result) $toast.error(`${title} 添加${subname}失败:${message}!`)
|
||||
}
|
||||
|
||||
// 调用API取消订阅
|
||||
@@ -263,26 +251,20 @@ async function removeSubscribe(season: number) {
|
||||
try {
|
||||
const mediaid = getMediaId()
|
||||
|
||||
const result: { [key: string]: any } = await api.delete(
|
||||
`subscribe/media/${mediaid}`,
|
||||
{
|
||||
params: {
|
||||
season,
|
||||
},
|
||||
const result: { [key: string]: any } = await api.delete(`subscribe/media/${mediaid}`, {
|
||||
params: {
|
||||
season,
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
isSubscribed.value = false
|
||||
if (season)
|
||||
seasonsSubscribed.value[season] = false
|
||||
if (season) seasonsSubscribed.value[season] = false
|
||||
$toast.success(`${mediaDetail.value?.title} 已取消订阅!`)
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
$toast.error(`${mediaDetail.value?.title} 取消订阅失败:${result.message}!`)
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
doneNProgress()
|
||||
@@ -290,10 +272,8 @@ async function removeSubscribe(season: number) {
|
||||
|
||||
// 订阅按钮响应
|
||||
function handleSubscribe(season = 0) {
|
||||
if (isSubscribed.value)
|
||||
removeSubscribe(season)
|
||||
else
|
||||
addSubscribe(season)
|
||||
if (isSubscribed.value) removeSubscribe(season)
|
||||
else addSubscribe(season)
|
||||
}
|
||||
|
||||
// 从genres中获取name,使用、分隔
|
||||
@@ -329,15 +309,13 @@ function getBangumiLink() {
|
||||
|
||||
// 拼装集图片地址
|
||||
function getEpisodeImage(stillPath: string) {
|
||||
if (!stillPath)
|
||||
return ''
|
||||
if (!stillPath) return ''
|
||||
return `https://image.tmdb.org/t/p/w500${stillPath}`
|
||||
}
|
||||
|
||||
// TMDB图片转换为w500大小
|
||||
function getW500Image(url = '') {
|
||||
if (!url)
|
||||
return ''
|
||||
if (!url) return ''
|
||||
return url.replace('original', 'w500')
|
||||
}
|
||||
|
||||
@@ -354,45 +332,33 @@ const getProductionCompanies = computed(() => {
|
||||
// 计算存在状态的颜色
|
||||
function getExistColor(season: number) {
|
||||
const state = seasonsNotExisted.value[season]
|
||||
if (!state)
|
||||
return 'success'
|
||||
if (!state) return 'success'
|
||||
|
||||
if (state === 1)
|
||||
return 'warning'
|
||||
else if (state === 2)
|
||||
return 'error'
|
||||
else
|
||||
return 'success'
|
||||
if (state === 1) return 'warning'
|
||||
else if (state === 2) return 'error'
|
||||
else return 'success'
|
||||
}
|
||||
|
||||
// 计算存在状态的文本
|
||||
function getExistText(season: number) {
|
||||
const state = seasonsNotExisted.value[season]
|
||||
if (!state)
|
||||
return '已入库'
|
||||
if (!state) return '已入库'
|
||||
|
||||
if (state === 1)
|
||||
return '部分缺失'
|
||||
else if (state === 2)
|
||||
return '缺失'
|
||||
else
|
||||
return '已入库'
|
||||
if (state === 1) return '部分缺失'
|
||||
else if (state === 2) return '缺失'
|
||||
else return '已入库'
|
||||
}
|
||||
|
||||
// 计算订阅图标
|
||||
const getSubscribeIcon = computed(() => {
|
||||
if (isSubscribed.value)
|
||||
return 'mdi-heart'
|
||||
else
|
||||
return 'mdi-heart-outline'
|
||||
if (isSubscribed.value) return 'mdi-heart'
|
||||
else return 'mdi-heart-outline'
|
||||
})
|
||||
|
||||
// 计算订阅按钮颜色
|
||||
const getSubscribeColor = computed(() => {
|
||||
if (isSubscribed.value)
|
||||
return 'error'
|
||||
else
|
||||
return 'warning'
|
||||
if (isSubscribed.value) return 'error'
|
||||
else return 'warning'
|
||||
})
|
||||
|
||||
// 使用、拼装数组为字符串
|
||||
@@ -418,36 +384,32 @@ function handleSearch(area: string) {
|
||||
async function handlePlay() {
|
||||
// 获取播放链接地址
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get(
|
||||
`mediaserver/play/${existsItemId.value}`,
|
||||
)
|
||||
const result: { [key: string]: any } = await api.get(`mediaserver/play/${existsItemId.value}`)
|
||||
if (result?.success) {
|
||||
// 打开链接地址
|
||||
setTimeout(() => {
|
||||
window.open(result.data.url, '_blank')
|
||||
}, 100)
|
||||
} else {
|
||||
$toast.error(`获取播放链接失败:${result.message}!`)
|
||||
}
|
||||
else { $toast.error(`获取播放链接失败:${result.message}!`) }
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
async function queryDefaultSubscribeConfig() {
|
||||
// 非管理员不显示
|
||||
if (!store.state.auth.superUser) return false
|
||||
try {
|
||||
let subscribe_config_url = ''
|
||||
if (mediaProps.type === '电影')
|
||||
subscribe_config_url = 'system/setting/DefaultMovieSubscribeConfig'
|
||||
else
|
||||
subscribe_config_url = 'system/setting/DefaultTvSubscribeConfig'
|
||||
if (mediaProps.type === '电影') subscribe_config_url = 'system/setting/DefaultMovieSubscribeConfig'
|
||||
else subscribe_config_url = 'system/setting/DefaultTvSubscribeConfig'
|
||||
|
||||
const result: { [key: string]: any } = await api.get(subscribe_config_url)
|
||||
|
||||
if (result.data?.value)
|
||||
return result.data.value.show_edit_dialog
|
||||
}
|
||||
catch (error) {
|
||||
if (result.data?.value) return result.data.value.show_edit_dialog
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
return false
|
||||
@@ -459,10 +421,7 @@ onBeforeMount(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LoadingBanner
|
||||
v-if="!isRefreshed"
|
||||
class="mt-12"
|
||||
/>
|
||||
<LoadingBanner v-if="!isRefreshed" class="mt-12" />
|
||||
<div v-if="mediaDetail.tmdb_id || mediaDetail.douban_id || mediaDetail.bangumi_id" class="max-w-8xl mx-auto px-4">
|
||||
<template v-if="mediaDetail.backdrop_path || mediaDetail.poster_path">
|
||||
<div class="vue-media-back absolute left-0 top-0 w-full h-96">
|
||||
@@ -473,7 +432,11 @@ onBeforeMount(() => {
|
||||
<div class="media-page">
|
||||
<div class="media-header">
|
||||
<div class="media-poster">
|
||||
<VImg :src="getW500Image(mediaDetail.poster_path)" cover class="object-cover aspect-w-2 aspect-h-3 ring-1 ring-gray-500">
|
||||
<VImg
|
||||
:src="getW500Image(mediaDetail.poster_path)"
|
||||
cover
|
||||
class="object-cover aspect-w-2 aspect-h-3 ring-1 ring-gray-500"
|
||||
>
|
||||
<template #placeholder>
|
||||
<div class="w-full h-full">
|
||||
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
|
||||
@@ -483,7 +446,9 @@ onBeforeMount(() => {
|
||||
</div>
|
||||
<div class="media-title">
|
||||
<div v-if="existsItemId" class="media-status">
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full whitespace-nowrap transition !no-underline bg-green-500 bg-opacity-80 border border-green-500 !text-green-100 hover:bg-green-500 hover:bg-opacity-100 false overflow-hidden">
|
||||
<span
|
||||
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full whitespace-nowrap transition !no-underline bg-green-500 bg-opacity-80 border border-green-500 !text-green-100 hover:bg-green-500 hover:bg-opacity-100 false overflow-hidden"
|
||||
>
|
||||
<div class="relative z-20 flex items-center false"><span>已入库</span></div>
|
||||
</span>
|
||||
</div>
|
||||
@@ -496,45 +461,56 @@ onBeforeMount(() => {
|
||||
</div>
|
||||
</h1>
|
||||
<span class="media-attributes">
|
||||
<span v-if="mediaDetail.runtime || mediaDetail.episode_run_time[0]">{{ mediaDetail.runtime || mediaDetail.episode_run_time[0] }} 分钟</span>
|
||||
<span v-if="(mediaDetail.runtime || mediaDetail.episode_run_time[0]) && mediaDetail.genres" class="mx-1"> | </span>
|
||||
<span v-if="mediaDetail.runtime || mediaDetail.episode_run_time[0]"
|
||||
>{{ mediaDetail.runtime || mediaDetail.episode_run_time[0] }} 分钟</span
|
||||
>
|
||||
<span v-if="(mediaDetail.runtime || mediaDetail.episode_run_time[0]) && mediaDetail.genres" class="mx-1">
|
||||
|
|
||||
</span>
|
||||
<span v-if="mediaDetail.genres">{{ getGenresName(mediaDetail.genres || []) }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="media-actions">
|
||||
<VBtn v-if="(mediaDetail.tmdb_id || mediaDetail.douban_id || mediaDetail.bangumi_id) && mediaDetail.imdb_id" variant="tonal" color="info" class="mb-2">
|
||||
<VBtn
|
||||
v-if="(mediaDetail.tmdb_id || mediaDetail.douban_id || mediaDetail.bangumi_id) && mediaDetail.imdb_id"
|
||||
variant="tonal"
|
||||
color="info"
|
||||
class="mb-2"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-magnify" />
|
||||
</template>
|
||||
搜索资源
|
||||
<VMenu
|
||||
activator="parent"
|
||||
close-on-content-click
|
||||
>
|
||||
<VMenu activator="parent" close-on-content-click>
|
||||
<VList>
|
||||
<VListItem
|
||||
variant="plain"
|
||||
@click="handleSearch('title')"
|
||||
>
|
||||
<VListItem variant="plain" @click="handleSearch('title')">
|
||||
<VListItemTitle>标题</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem
|
||||
v-show="mediaDetail.imdb_id"
|
||||
variant="plain"
|
||||
@click="handleSearch('imdbid')"
|
||||
>
|
||||
<VListItem v-show="mediaDetail.imdb_id" variant="plain" @click="handleSearch('imdbid')">
|
||||
<VListItemTitle>IMDB链接</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</VBtn>
|
||||
<VBtn v-if="(mediaDetail.tmdb_id || mediaDetail.douban_id || mediaDetail.bangumi_id) && !mediaDetail.imdb_id" variant="tonal" color="info" class="mb-2" @click="handleSearch('title')">
|
||||
<VBtn
|
||||
v-if="(mediaDetail.tmdb_id || mediaDetail.douban_id || mediaDetail.bangumi_id) && !mediaDetail.imdb_id"
|
||||
variant="tonal"
|
||||
color="info"
|
||||
class="mb-2"
|
||||
@click="handleSearch('title')"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-magnify" />
|
||||
</template>
|
||||
搜索资源
|
||||
</VBtn>
|
||||
<VBtn v-if="mediaDetail.type === '电影' || mediaDetail.douban_id || mediaDetail.bangumi_id" class="ms-2 mb-2" :color="getSubscribeColor" variant="tonal" @click="handleSubscribe(0)">
|
||||
<VBtn
|
||||
v-if="mediaDetail.type === '电影' || mediaDetail.douban_id || mediaDetail.bangumi_id"
|
||||
class="ms-2 mb-2"
|
||||
:color="getSubscribeColor"
|
||||
variant="tonal"
|
||||
@click="handleSubscribe(0)"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon :icon="getSubscribeIcon" />
|
||||
</template>
|
||||
@@ -553,9 +529,7 @@ onBeforeMount(() => {
|
||||
<div v-if="mediaDetail.tagline" class="tagline">
|
||||
{{ mediaDetail.tagline }}
|
||||
</div>
|
||||
<h2 v-if="mediaDetail.overview">
|
||||
简介
|
||||
</h2>
|
||||
<h2 v-if="mediaDetail.overview">简介</h2>
|
||||
<p>{{ mediaDetail.overview }}</p>
|
||||
<ul v-if="mediaDetail.tmdb_id" class="media-crew">
|
||||
<li v-for="director in mediaDetail.directors" :key="director.id">
|
||||
@@ -572,40 +546,63 @@ onBeforeMount(() => {
|
||||
</li>
|
||||
</ul>
|
||||
<div class="mt-6">
|
||||
<a v-if="mediaDetail.tmdb_id" class="mb-2 mr-2 inline-flex last:mr-0" :href="getTheMovieDbLink()" target="_blank">
|
||||
<div class="inline-flex cursor-pointer items-center rounded-full bg-gray-600 px-2 py-1 text-sm text-gray-200 ring-1 ring-gray-500 transition hover:bg-gray-700">
|
||||
<a
|
||||
v-if="mediaDetail.tmdb_id"
|
||||
class="mb-2 mr-2 inline-flex last:mr-0"
|
||||
:href="getTheMovieDbLink()"
|
||||
target="_blank"
|
||||
>
|
||||
<div
|
||||
class="inline-flex cursor-pointer items-center rounded-full bg-gray-600 px-2 py-1 text-sm text-gray-200 ring-1 ring-gray-500 transition hover:bg-gray-700"
|
||||
>
|
||||
<VIcon icon="mdi-link" />
|
||||
<span class="ms-1">TheMovieDb</span>
|
||||
</div>
|
||||
</a>
|
||||
<a v-if="mediaDetail.douban_id" class="mb-2 mr-2 inline-flex last:mr-0" :href="getDoubanLink()" target="_blank">
|
||||
<div class="inline-flex cursor-pointer items-center rounded-full bg-gray-600 px-2 py-1 text-sm text-gray-200 ring-1 ring-gray-500 transition hover:bg-gray-700">
|
||||
<a
|
||||
v-if="mediaDetail.douban_id"
|
||||
class="mb-2 mr-2 inline-flex last:mr-0"
|
||||
:href="getDoubanLink()"
|
||||
target="_blank"
|
||||
>
|
||||
<div
|
||||
class="inline-flex cursor-pointer items-center rounded-full bg-gray-600 px-2 py-1 text-sm text-gray-200 ring-1 ring-gray-500 transition hover:bg-gray-700"
|
||||
>
|
||||
<VIcon icon="mdi-link" />
|
||||
<span class="ms-1">豆瓣</span>
|
||||
</div>
|
||||
</a>
|
||||
<a v-if="mediaDetail.imdb_id" class="mb-2 mr-2 inline-flex last:mr-0" :href="getImdbLink()" target="_blank">
|
||||
<div class="inline-flex cursor-pointer items-center rounded-full bg-gray-600 px-2 py-1 text-sm text-gray-200 ring-1 ring-gray-500 transition hover:bg-gray-700">
|
||||
<div
|
||||
class="inline-flex cursor-pointer items-center rounded-full bg-gray-600 px-2 py-1 text-sm text-gray-200 ring-1 ring-gray-500 transition hover:bg-gray-700"
|
||||
>
|
||||
<VIcon icon="mdi-link" />
|
||||
<span class="ms-1">IMDb</span>
|
||||
</div>
|
||||
</a>
|
||||
<a v-if="mediaDetail.tvdb_id" class="mb-2 mr-2 inline-flex last:mr-0" :href="getTvdbLink()" target="_blank">
|
||||
<div class="inline-flex cursor-pointer items-center rounded-full bg-gray-600 px-2 py-1 text-sm text-gray-200 ring-1 ring-gray-500 transition hover:bg-gray-700">
|
||||
<div
|
||||
class="inline-flex cursor-pointer items-center rounded-full bg-gray-600 px-2 py-1 text-sm text-gray-200 ring-1 ring-gray-500 transition hover:bg-gray-700"
|
||||
>
|
||||
<VIcon icon="mdi-link" />
|
||||
<span class="ms-1">TheTvDb</span>
|
||||
</div>
|
||||
</a>
|
||||
<a v-if="mediaDetail.bangumi_id" class="mb-2 mr-2 inline-flex last:mr-0" :href="getBangumiLink()" target="_blank">
|
||||
<div class="inline-flex cursor-pointer items-center rounded-full bg-gray-600 px-2 py-1 text-sm text-gray-200 ring-1 ring-gray-500 transition hover:bg-gray-700">
|
||||
<a
|
||||
v-if="mediaDetail.bangumi_id"
|
||||
class="mb-2 mr-2 inline-flex last:mr-0"
|
||||
:href="getBangumiLink()"
|
||||
target="_blank"
|
||||
>
|
||||
<div
|
||||
class="inline-flex cursor-pointer items-center rounded-full bg-gray-600 px-2 py-1 text-sm text-gray-200 ring-1 ring-gray-500 transition hover:bg-gray-700"
|
||||
>
|
||||
<VIcon icon="mdi-link" />
|
||||
<span class="ms-1">Bangumi</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<h2 v-if="mediaDetail.type === '电视剧' && mediaDetail.tmdb_id" class="py-4">
|
||||
季
|
||||
</h2>
|
||||
<h2 v-if="mediaDetail.type === '电视剧' && mediaDetail.tmdb_id" class="py-4">季</h2>
|
||||
<div v-if="mediaDetail.type === '电视剧' && mediaDetail.tmdb_id" class="flex w-full flex-col space-y-2">
|
||||
<VExpansionPanels>
|
||||
<VExpansionPanel
|
||||
@@ -617,22 +614,20 @@ onBeforeMount(() => {
|
||||
<template #default>
|
||||
<div class="flex flex-row items-center justify-between">
|
||||
<span class="font-weight-bold">第 {{ season.season_number }} 季</span>
|
||||
<VChip size="small" class="ms-1">
|
||||
{{ season.episode_count }}集
|
||||
</VChip>
|
||||
<VChip size="small" class="ms-1"> {{ season.episode_count }}集 </VChip>
|
||||
<div class="absolute right-12">
|
||||
<VChip
|
||||
v-if="seasonsNotExisted"
|
||||
:color="getExistColor(season.season_number || 0)"
|
||||
flat
|
||||
>
|
||||
<VChip v-if="seasonsNotExisted" :color="getExistColor(season.season_number || 0)" flat>
|
||||
{{ getExistText(season.season_number || 0) }}
|
||||
</VChip>
|
||||
<IconBtn
|
||||
class="ms-1" :color="seasonsSubscribed[season.season_number || 0] ? 'error' : 'warning'" variant="text"
|
||||
class="ms-1"
|
||||
:color="seasonsSubscribed[season.season_number || 0] ? 'error' : 'warning'"
|
||||
variant="text"
|
||||
@click.stop="handleSubscribe(season.season_number)"
|
||||
>
|
||||
<VIcon :icon="seasonsSubscribed[season.season_number || 0] ? 'mdi-heart' : 'mdi-heart-outline'" />
|
||||
<VIcon
|
||||
:icon="seasonsSubscribed[season.season_number || 0] ? 'mdi-heart' : 'mdi-heart-outline'"
|
||||
/>
|
||||
</IconBtn>
|
||||
</div>
|
||||
</div>
|
||||
@@ -640,26 +635,43 @@ onBeforeMount(() => {
|
||||
</VExpansionPanelTitle>
|
||||
<VExpansionPanelText>
|
||||
<template #default>
|
||||
<LoadingBanner
|
||||
v-if="!seasonEpisodesInfo[season.season_number || 0]"
|
||||
class="mt-3"
|
||||
/>
|
||||
<LoadingBanner v-if="!seasonEpisodesInfo[season.season_number || 0]" class="mt-3" />
|
||||
<div class="flex flex-col justify-center divide-y divide-gray-700">
|
||||
<div v-for="episode in seasonEpisodesInfo[season.season_number || 0]" :key="episode.episode_number" class="flex flex-col space-y-4 py-4 xl:flex-row xl:space-y-4 xl:space-x-4">
|
||||
<div
|
||||
v-for="episode in seasonEpisodesInfo[season.season_number || 0]"
|
||||
:key="episode.episode_number"
|
||||
class="flex flex-col space-y-4 py-4 xl:flex-row xl:space-y-4 xl:space-x-4"
|
||||
>
|
||||
<div class="flex-1">
|
||||
<div class="flex flex-col space-y-2 lg:flex-row lg:items-center lg:space-y-0 lg:space-x-2">
|
||||
<h3 class="text-lg">
|
||||
{{ episode.episode_number }} - {{ episode.name }}
|
||||
</h3>
|
||||
<h3 class="text-lg">{{ episode.episode_number }} - {{ episode.name }}</h3>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full whitespace-nowrap cursor-default bg-gray-700 !text-gray-300">
|
||||
<span
|
||||
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full whitespace-nowrap cursor-default bg-gray-700 !text-gray-300"
|
||||
>
|
||||
{{ episode.air_date }}
|
||||
</span>
|
||||
</div>
|
||||
<VIcon
|
||||
v-if="
|
||||
existsEpisodes[season.season_number || 0] &&
|
||||
existsEpisodes[season.season_number || 0].includes(episode.episode_number || 0)
|
||||
"
|
||||
color="success"
|
||||
icon="mdi-check-circle"
|
||||
class="ms-2"
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
<p>{{ episode.overview }}</p>
|
||||
</div>
|
||||
<VImg cover class="rounded-lg" max-width="15rem" :src="getEpisodeImage(episode.still_path || '')" alt="" />
|
||||
<VImg
|
||||
cover
|
||||
class="rounded-lg"
|
||||
max-width="15rem"
|
||||
:src="getEpisodeImage(episode.still_path || '')"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -671,13 +683,7 @@ onBeforeMount(() => {
|
||||
<div v-if="mediaDetail.tmdb_id" class="media-overview-right">
|
||||
<div class="media-facts">
|
||||
<div v-if="mediaDetail.vote_average" class="media-ratings">
|
||||
<VRating
|
||||
v-model="mediaDetail.vote_average"
|
||||
density="compact"
|
||||
length="10"
|
||||
class="ma-2"
|
||||
readonly
|
||||
/>
|
||||
<VRating v-model="mediaDetail.vote_average" density="compact" length="10" class="ma-2" readonly />
|
||||
</div>
|
||||
<div v-if="mediaDetail.tmdb_id" class="media-fact">
|
||||
<span>ID</span>
|
||||
@@ -695,7 +701,20 @@ onBeforeMount(() => {
|
||||
<span>上映日期</span>
|
||||
<span class="media-fact-value">
|
||||
<span class="flex items-center justify-end">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true" class="h-4 w-4"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 15a4.5 4.5 0 004.5 4.5H18a3.75 3.75 0 001.332-7.257 3 3 0 00-3.758-3.848 5.25 5.25 0 00-10.233 2.33A4.502 4.502 0 002.25 15z" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
class="h-4 w-4"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M2.25 15a4.5 4.5 0 004.5 4.5H18a3.75 3.75 0 001.332-7.257 3 3 0 00-3.758-3.848 5.25 5.25 0 00-10.233 2.33A4.502 4.502 0 002.25 15z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="ml-1.5">{{ mediaDetail.release_date || mediaDetail.first_air_date }}</span>
|
||||
</span>
|
||||
@@ -708,7 +727,11 @@ onBeforeMount(() => {
|
||||
<div v-if="mediaDetail.production_countries" class="media-fact">
|
||||
<span>出品国家</span>
|
||||
<span class="media-fact-value">
|
||||
<span v-for="country in getProductionCountries" :key="country" class="flex items-center justify-end text-end">
|
||||
<span
|
||||
v-for="country in getProductionCountries"
|
||||
:key="country"
|
||||
class="flex items-center justify-end text-end"
|
||||
>
|
||||
{{ country }}
|
||||
</span>
|
||||
</span>
|
||||
@@ -724,13 +747,7 @@ onBeforeMount(() => {
|
||||
<div v-else-if="mediaDetail.douban_id" class="media-overview-right">
|
||||
<div class="media-facts">
|
||||
<div v-if="mediaDetail.vote_average" class="media-ratings">
|
||||
<VRating
|
||||
v-model="mediaDetail.vote_average"
|
||||
density="compact"
|
||||
length="10"
|
||||
class="ma-2"
|
||||
readonly
|
||||
/>
|
||||
<VRating v-model="mediaDetail.vote_average" density="compact" length="10" class="ma-2" readonly />
|
||||
</div>
|
||||
<div v-if="mediaDetail.douban_id" class="media-fact">
|
||||
<span>豆瓣ID</span>
|
||||
@@ -749,7 +766,11 @@ onBeforeMount(() => {
|
||||
<div v-if="mediaDetail.production_countries" class="media-fact border-b-0">
|
||||
<span>出品国家</span>
|
||||
<span class="media-fact-value">
|
||||
<span v-for="country in getProductionCountries" :key="country" class="flex items-center justify-end text-end">
|
||||
<span
|
||||
v-for="country in getProductionCountries"
|
||||
:key="country"
|
||||
class="flex items-center justify-end text-end"
|
||||
>
|
||||
{{ country }}
|
||||
</span>
|
||||
</span>
|
||||
@@ -759,13 +780,7 @@ onBeforeMount(() => {
|
||||
<div v-else-if="mediaDetail.bangumi_id" class="media-overview-right">
|
||||
<div class="media-facts">
|
||||
<div v-if="mediaDetail.vote_average" class="media-ratings">
|
||||
<VRating
|
||||
v-model="mediaDetail.vote_average"
|
||||
density="compact"
|
||||
length="10"
|
||||
class="ma-2"
|
||||
readonly
|
||||
/>
|
||||
<VRating v-model="mediaDetail.vote_average" density="compact" length="10" class="ma-2" readonly />
|
||||
</div>
|
||||
<div v-if="mediaDetail.bangumi_id" class="media-fact">
|
||||
<span>ID</span>
|
||||
@@ -850,20 +865,23 @@ onBeforeMount(() => {
|
||||
:subid="subscribeId"
|
||||
@close="subscribeEditDialog = false"
|
||||
@save="subscribeEditDialog = false"
|
||||
@remove="() => {
|
||||
subscribeEditDialog = false;
|
||||
if (mediaDetail.type === '电影')
|
||||
checkMovieSubscribed()
|
||||
else
|
||||
checkSeasonsSubscribed();
|
||||
}"
|
||||
@remove="
|
||||
() => {
|
||||
subscribeEditDialog = false
|
||||
if (mediaDetail.type === '电影') checkMovieSubscribed()
|
||||
else checkSeasonsSubscribed()
|
||||
}
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.vue-media-back {
|
||||
background-image:
|
||||
linear-gradient(180deg, rgba(var(--v-theme-background), 0) 50%, rgba(var(--v-theme-background), 1) 100%),
|
||||
background-image: linear-gradient(
|
||||
180deg,
|
||||
rgba(var(--v-theme-background), 0) 50%,
|
||||
rgba(var(--v-theme-background), 1) 100%
|
||||
),
|
||||
linear-gradient(90deg, rgba(var(--v-theme-background), 0) 50%, rgba(var(--v-theme-background), 1) 100%),
|
||||
linear-gradient(270deg, rgba(var(--v-theme-background), 0) 50%, rgba(var(--v-theme-background), 1) 100%);
|
||||
box-shadow: 0 0 0 2px rgb(var(--v-theme-background));
|
||||
@@ -908,7 +926,7 @@ onBeforeMount(() => {
|
||||
|
||||
.media-poster {
|
||||
overflow: hidden;
|
||||
border-radius: .25rem;
|
||||
border-radius: 0.25rem;
|
||||
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
||||
inline-size: 8rem;
|
||||
|
||||
@@ -925,7 +943,7 @@ onBeforeMount(() => {
|
||||
|
||||
@media (width >= 768px) {
|
||||
.media-poster {
|
||||
border-radius: .5rem;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
||||
inline-size: 11rem;
|
||||
|
||||
@@ -950,45 +968,45 @@ onBeforeMount(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.media-title>h1 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
line-height: 2rem;
|
||||
.media-title > h1 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
line-height: 2rem;
|
||||
}
|
||||
|
||||
@media (width >= 1280px) {
|
||||
.media-title>h1 {
|
||||
font-size: 2.25rem;
|
||||
line-height: 2.5rem;
|
||||
.media-title > h1 {
|
||||
font-size: 2.25rem;
|
||||
line-height: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
ul.media-crew {
|
||||
display: grid;
|
||||
gap: 1.5rem;
|
||||
grid-template-columns: repeat(2,minmax(0,1fr));
|
||||
margin-block-start: 1.5rem;
|
||||
display: grid;
|
||||
gap: 1.5rem;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
margin-block-start: 1.5rem;
|
||||
}
|
||||
|
||||
@media (width >= 640px) {
|
||||
ul.media-crew {
|
||||
grid-template-columns: repeat(3,minmax(0,1fr));
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
ul.media-crew>li {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-weight: 700;
|
||||
grid-column: span 1/span 1;
|
||||
ul.media-crew > li {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-weight: 700;
|
||||
grid-column: span 1 / span 1;
|
||||
}
|
||||
|
||||
a.crew-name {
|
||||
font-weight: 400;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.media-status {
|
||||
margin-block-end: .5rem;
|
||||
margin-block-end: 0.5rem;
|
||||
}
|
||||
|
||||
.media-attributes {
|
||||
@@ -996,7 +1014,7 @@ a.crew-name {
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-block-start: .25rem;
|
||||
margin-block-start: 0.25rem;
|
||||
}
|
||||
|
||||
@media (width >= 1280px) {
|
||||
@@ -1010,7 +1028,7 @@ a.crew-name {
|
||||
|
||||
@media (width >= 640px) {
|
||||
.media-attributes {
|
||||
font-size: .875rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
}
|
||||
@@ -1061,69 +1079,66 @@ a.crew-name {
|
||||
}
|
||||
|
||||
.media-facts {
|
||||
border-width: 1px;
|
||||
border-color: rgb(55 65 81/var(--tw-border-opacity));
|
||||
border-radius: 0.5rem;
|
||||
font-size: .875rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.25rem;
|
||||
border-width: 1px;
|
||||
border-color: rgb(55 65 81 / var(--tw-border-opacity));
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.25rem;
|
||||
|
||||
--tw-border-opacity: 1;
|
||||
--tw-bg-opacity: 1;
|
||||
--tw-text-opacity: 1;
|
||||
--tw-border-opacity: 1;
|
||||
--tw-bg-opacity: 1;
|
||||
--tw-text-opacity: 1;
|
||||
}
|
||||
|
||||
.media-ratings {
|
||||
border-color: rgb(55 65 81/var(--tw-border-opacity));
|
||||
border-block-end-width: 1px;
|
||||
font-weight: 500;
|
||||
padding-block: 0.5rem;
|
||||
padding-inline: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-color: rgb(55 65 81 / var(--tw-border-opacity));
|
||||
border-block-end-width: 1px;
|
||||
font-weight: 500;
|
||||
padding-block: 0.5rem;
|
||||
padding-inline: 1rem;
|
||||
|
||||
--tw-border-opacity: 1;
|
||||
}
|
||||
|
||||
.media-ratings {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
--tw-border-opacity: 1;
|
||||
}
|
||||
|
||||
.media-fact {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border-color: rgb(55 65 81/var(--tw-border-opacity));
|
||||
border-block-end-width: 1px;
|
||||
padding-block: 0.5rem;
|
||||
padding-inline: 1rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border-color: rgb(55 65 81 / var(--tw-border-opacity));
|
||||
border-block-end-width: 1px;
|
||||
padding-block: 0.5rem;
|
||||
padding-inline: 1rem;
|
||||
|
||||
--tw-border-opacity: 1;
|
||||
--tw-border-opacity: 1;
|
||||
}
|
||||
|
||||
.media-overview h2 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.75rem;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.75rem;
|
||||
}
|
||||
|
||||
@media (width >= 640px) {
|
||||
.media-overview h2 {
|
||||
font-size: 1.5rem;
|
||||
line-height: 2rem;
|
||||
font-size: 1.5rem;
|
||||
line-height: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.tagline {
|
||||
font-size: 1.25rem;
|
||||
font-style: italic;
|
||||
line-height: 1.75rem;
|
||||
margin-block-end: 1rem;
|
||||
font-size: 1.25rem;
|
||||
font-style: italic;
|
||||
line-height: 1.75rem;
|
||||
margin-block-end: 1rem;
|
||||
}
|
||||
|
||||
@media (width >= 1024px) {
|
||||
.tagline {
|
||||
font-size: 1.5rem;
|
||||
line-height: 2rem;
|
||||
font-size: 1.5rem;
|
||||
line-height: 2rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -124,9 +124,3 @@ async function fetchData({ done }: { done: any }) {
|
||||
/>
|
||||
</VInfiniteScroll>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.grid-media-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(9.375rem, 1fr));
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -81,7 +81,7 @@ onMounted(() => {
|
||||
// 数据分组
|
||||
const groupMap = new Map<string, Context[]>()
|
||||
// 遍历数据
|
||||
props.items?.forEach((item) => {
|
||||
props.items?.forEach(item => {
|
||||
const { torrent_info } = item
|
||||
// init options
|
||||
initOptions(item)
|
||||
@@ -91,8 +91,7 @@ onMounted(() => {
|
||||
// 已入库相同标题和大小的分组,将当前上下文信息添加到分组中
|
||||
const group = groupMap.get(key)
|
||||
group?.push(item)
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// 创建新的分组,并将当前上下文信息添加到分组中
|
||||
groupMap.set(key, [item])
|
||||
}
|
||||
@@ -110,32 +109,31 @@ watchEffect(() => {
|
||||
const match = (filter: Array<string>, value: string | undefined) =>
|
||||
filter.length === 0 || (value && filter.includes(value))
|
||||
|
||||
groupedDataList.value?.forEach((value) => {
|
||||
groupedDataList.value?.forEach(value => {
|
||||
if (value.length > 0) {
|
||||
const matchData = value.filter((data) => {
|
||||
const matchData = value.filter(data => {
|
||||
const { meta_info, torrent_info } = data
|
||||
// 季、制作组、视频编码
|
||||
return (
|
||||
// 站点过滤
|
||||
match(filterForm.site, torrent_info.site_name)
|
||||
match(filterForm.site, torrent_info.site_name) &&
|
||||
// 促销状态过滤
|
||||
&& match(filterForm.freeState, torrent_info.volume_factor)
|
||||
match(filterForm.freeState, torrent_info.volume_factor) &&
|
||||
// 季过滤
|
||||
&& match(filterForm.season, meta_info.season_episode)
|
||||
match(filterForm.season, meta_info.season_episode) &&
|
||||
// 制作组过滤
|
||||
&& match(filterForm.releaseGroup, meta_info.resource_team)
|
||||
match(filterForm.releaseGroup, meta_info.resource_team) &&
|
||||
// 视频编码过滤
|
||||
&& match(filterForm.videoCode, meta_info.video_encode)
|
||||
match(filterForm.videoCode, meta_info.video_encode) &&
|
||||
// 分辨率过滤
|
||||
&& match(filterForm.resolution, meta_info.resource_pix)
|
||||
match(filterForm.resolution, meta_info.resource_pix) &&
|
||||
// 质量过滤
|
||||
&& match(filterForm.edition, meta_info.edition)
|
||||
match(filterForm.edition, meta_info.edition)
|
||||
)
|
||||
})
|
||||
if (matchData.length > 0) {
|
||||
const firstData = _.cloneDeepWith(matchData[0]) as SearchTorrent
|
||||
if (matchData.length > 1)
|
||||
firstData.more = matchData.slice(1)
|
||||
if (matchData.length > 1) firstData.more = matchData.slice(1)
|
||||
|
||||
dataList.value.push(firstData)
|
||||
}
|
||||
@@ -235,8 +233,5 @@ watchEffect(() => {
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.grid-torrent-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -27,6 +27,9 @@ const filterForm = reactive({
|
||||
resolution: [] as string[],
|
||||
})
|
||||
|
||||
// 排序字段
|
||||
const sortField = ref('default')
|
||||
|
||||
// 数据列表
|
||||
const dataList = ref<Array<Context>>([])
|
||||
|
||||
@@ -60,7 +63,19 @@ function initOptions(data: Context) {
|
||||
optionValue(resolutionFilterOptions.value, meta_info?.resource_pix)
|
||||
}
|
||||
|
||||
let defer = (_: number) => true
|
||||
// 排序
|
||||
watchEffect(() => {
|
||||
const list = dataList.value
|
||||
if (sortField.value === 'default') {
|
||||
dataList.value = list.sort((a, b) => b.torrent_info.pri_order - a.torrent_info.pri_order)
|
||||
} else if (sortField.value === 'site') {
|
||||
dataList.value = list.sort((a, b) => (a.torrent_info.site_name || '').localeCompare(b.torrent_info.site_name || ''))
|
||||
} else if (sortField.value === 'size') {
|
||||
dataList.value = list.sort((a, b) => b.torrent_info.size - a.torrent_info.size)
|
||||
} else if (sortField.value === 'seeder') {
|
||||
dataList.value = list.sort((a, b) => b.torrent_info.seeders - a.torrent_info.seeders)
|
||||
}
|
||||
})
|
||||
|
||||
// 计算过滤后的列表
|
||||
watchEffect(() => {
|
||||
@@ -70,32 +85,31 @@ watchEffect(() => {
|
||||
const match = (filter: Array<string>, value: string | undefined) =>
|
||||
filter.length === 0 || (value && filter.includes(value))
|
||||
|
||||
props.items?.forEach((data) => {
|
||||
props.items?.forEach(data => {
|
||||
const { meta_info, torrent_info } = data
|
||||
if (
|
||||
// 站点过滤
|
||||
match(filterForm.site, torrent_info.site_name)
|
||||
match(filterForm.site, torrent_info.site_name) &&
|
||||
// 促销状态过滤
|
||||
&& match(filterForm.freeState, torrent_info.volume_factor)
|
||||
match(filterForm.freeState, torrent_info.volume_factor) &&
|
||||
// 季过滤
|
||||
&& match(filterForm.season, meta_info.season_episode)
|
||||
match(filterForm.season, meta_info.season_episode) &&
|
||||
// 制作组过滤
|
||||
&& match(filterForm.releaseGroup, meta_info.resource_team)
|
||||
match(filterForm.releaseGroup, meta_info.resource_team) &&
|
||||
// 视频编码过滤
|
||||
&& match(filterForm.videoCode, meta_info.video_encode)
|
||||
match(filterForm.videoCode, meta_info.video_encode) &&
|
||||
// 分辨率过滤
|
||||
&& match(filterForm.resolution, meta_info.resource_pix)
|
||||
match(filterForm.resolution, meta_info.resource_pix) &&
|
||||
// 质量过滤
|
||||
&& match(filterForm.edition, meta_info.edition)
|
||||
match(filterForm.edition, meta_info.edition)
|
||||
)
|
||||
dataList.value.push(data)
|
||||
})
|
||||
defer = useDefer(dataList.value.length)
|
||||
})
|
||||
|
||||
// 初始化过滤选项
|
||||
onMounted(() => {
|
||||
props.items?.forEach((item) => {
|
||||
props.items?.forEach(item => {
|
||||
initOptions(item)
|
||||
})
|
||||
})
|
||||
@@ -104,22 +118,37 @@ onMounted(() => {
|
||||
<template>
|
||||
<VRow>
|
||||
<VCol>
|
||||
<VList v-if="dataList.length === 0" lines="three" class="rounded p-0">
|
||||
<VList v-if="dataList.length === 0" lines="three" class="rounded p-0 shadow-lg">
|
||||
<VListItem>
|
||||
<VListItemTitle>没有附合当前过滤条件的资源。</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
<VList v-if="dataList.length !== 0" lines="three" class="rounded p-0">
|
||||
<div v-for="(item, index) in dataList" :key="`${index}_${item.torrent_info.title}_${item.torrent_info.site}`">
|
||||
<TorrentItem v-if="defer(index)" :torrent="item" />
|
||||
</div>
|
||||
<VList v-if="dataList.length !== 0" lines="three" class="rounded p-0 torrent-list-vscroll shadow-lg">
|
||||
<VVirtualScroll :items="dataList">
|
||||
<template #default="{ item }">
|
||||
<TorrentItem :torrent="item" :key="`${item.torrent_info.title}_${item.torrent_info.site}`" />
|
||||
</template>
|
||||
</VVirtualScroll>
|
||||
</VList>
|
||||
</VCol>
|
||||
<VCol xl="2" md="3" class="d-none d-md-block">
|
||||
<VList lines="one" class="rounded">
|
||||
<VListSubheader v-if="siteFilterOptions.length > 0">
|
||||
站点
|
||||
</VListSubheader>
|
||||
<VList lines="one" class="rounded torrent-list-vscroll shadow-lg">
|
||||
<VListSubheader> 排序 </VListSubheader>
|
||||
<VListItem>
|
||||
<VChipGroup column v-model="sortField">
|
||||
<VChip :color="sortField == 'default' ? 'primary' : ''" filter variant="outlined" value="default">
|
||||
默认
|
||||
</VChip>
|
||||
<VChip :color="sortField == 'site' ? 'primary' : ''" filter variant="outlined" value="site"> 站点 </VChip>
|
||||
<VChip :color="sortField == 'size' ? 'primary' : ''" filter variant="outlined" value="size">
|
||||
文件大小
|
||||
</VChip>
|
||||
<VChip :color="sortField == 'seeder' ? 'primary' : ''" filter variant="outlined" value="seeder">
|
||||
做种数
|
||||
</VChip>
|
||||
</VChipGroup>
|
||||
</VListItem>
|
||||
<VListSubheader v-if="siteFilterOptions.length > 0"> 站点 </VListSubheader>
|
||||
<VListItem>
|
||||
<VChipGroup v-model="filterForm.site" column multiple>
|
||||
<VChip
|
||||
@@ -134,9 +163,7 @@ onMounted(() => {
|
||||
</VChip>
|
||||
</VChipGroup>
|
||||
</VListItem>
|
||||
<VListSubheader v-if="editionFilterOptions.length > 0">
|
||||
质量
|
||||
</VListSubheader>
|
||||
<VListSubheader v-if="editionFilterOptions.length > 0"> 质量 </VListSubheader>
|
||||
<VListItem>
|
||||
<VChipGroup v-model="filterForm.edition" column multiple>
|
||||
<VChip
|
||||
@@ -151,9 +178,7 @@ onMounted(() => {
|
||||
</VChip>
|
||||
</VChipGroup>
|
||||
</VListItem>
|
||||
<VListSubheader v-if="resolutionFilterOptions.length > 0">
|
||||
分辨率
|
||||
</VListSubheader>
|
||||
<VListSubheader v-if="resolutionFilterOptions.length > 0"> 分辨率 </VListSubheader>
|
||||
<VListItem>
|
||||
<VChipGroup v-model="filterForm.resolution" column multiple>
|
||||
<VChip
|
||||
@@ -168,9 +193,7 @@ onMounted(() => {
|
||||
</VChip>
|
||||
</VChipGroup>
|
||||
</VListItem>
|
||||
<VListSubheader v-if="releaseGroupFilterOptions.length > 0">
|
||||
制作组
|
||||
</VListSubheader>
|
||||
<VListSubheader v-if="releaseGroupFilterOptions.length > 0"> 制作组 </VListSubheader>
|
||||
<VListItem>
|
||||
<VChipGroup v-model="filterForm.releaseGroup" column multiple>
|
||||
<VChip
|
||||
@@ -185,9 +208,7 @@ onMounted(() => {
|
||||
</VChip>
|
||||
</VChipGroup>
|
||||
</VListItem>
|
||||
<VListSubheader v-if="videoCodeFilterOptions.length > 0">
|
||||
视频编码
|
||||
</VListSubheader>
|
||||
<VListSubheader v-if="videoCodeFilterOptions.length > 0"> 视频编码 </VListSubheader>
|
||||
<VListItem>
|
||||
<VChipGroup v-model="filterForm.videoCode" column multiple>
|
||||
<VChip
|
||||
@@ -202,9 +223,7 @@ onMounted(() => {
|
||||
</VChip>
|
||||
</VChipGroup>
|
||||
</VListItem>
|
||||
<VListSubheader v-if="freeStateFilterOptions.length > 0">
|
||||
促销状态
|
||||
</VListSubheader>
|
||||
<VListSubheader v-if="freeStateFilterOptions.length > 0"> 促销状态 </VListSubheader>
|
||||
<VListItem>
|
||||
<VChipGroup v-model="filterForm.freeState" column multiple>
|
||||
<VChip
|
||||
@@ -219,9 +238,7 @@ onMounted(() => {
|
||||
</VChip>
|
||||
</VChipGroup>
|
||||
</VListItem>
|
||||
<VListSubheader v-if="seasonFilterOptions.length > 0">
|
||||
季集
|
||||
</VListSubheader>
|
||||
<VListSubheader v-if="seasonFilterOptions.length > 0"> 季集 </VListSubheader>
|
||||
<VListItem>
|
||||
<VChipGroup v-model="filterForm.season" column multiple>
|
||||
<VChip
|
||||
@@ -240,3 +257,15 @@ onMounted(() => {
|
||||
</VCol>
|
||||
</VRow>
|
||||
</template>
|
||||
<style lang="scss">
|
||||
.torrent-list-vscroll {
|
||||
block-size: calc(100vh - 6rem);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@media (width <= 768px) {
|
||||
.orrent-list-vscroll {
|
||||
block-size: calc(100vh - 10rem);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -16,7 +16,6 @@ const route = useRoute()
|
||||
const display = useDisplay()
|
||||
|
||||
// 延迟加载
|
||||
let defer = (_: number) => true
|
||||
let deferApp = (_: number) => true
|
||||
|
||||
// 当前标签
|
||||
@@ -210,7 +209,6 @@ async function fetchInstalledPlugins() {
|
||||
state: 'installed',
|
||||
},
|
||||
})
|
||||
defer = useDefer(dataList.value.length)
|
||||
isRefreshed.value = true
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
@@ -337,18 +335,15 @@ onBeforeMount(async () => {
|
||||
</VTab>
|
||||
</VTabs>
|
||||
|
||||
<VDivider />
|
||||
|
||||
<VWindow v-model="activeTab" class="mt-5 disable-tab-transition" :touch="false">
|
||||
<!-- 我的插件 -->
|
||||
<VWindowItem value="myplugin">
|
||||
<transition name="fade-slide" appear>
|
||||
<div>
|
||||
<LoadingBanner v-if="!isRefreshed" class="mt-12" />
|
||||
<div v-if="dataList.length > 0" class="grid gap-4 grid-plugin-card">
|
||||
<div v-if="dataList.length > 0" class="grid gap-4 grid-plugin-card items-start">
|
||||
<template v-for="(data, index) in dataList" :key="`${data.id}_v${data.plugin_version}`">
|
||||
<PluginCard
|
||||
v-if="defer(index)"
|
||||
:count="PluginStatistics[data.id || '0']"
|
||||
:plugin="data"
|
||||
:action="pluginActions[data.id || '0']"
|
||||
@@ -416,7 +411,7 @@ onBeforeMount(async () => {
|
||||
</VCol>
|
||||
</VRow>
|
||||
</div>
|
||||
<div v-if="isAppMarketLoaded" class="grid gap-4 grid-plugin-card">
|
||||
<div v-if="isAppMarketLoaded" class="grid gap-4 grid-plugin-card items-start">
|
||||
<template v-for="(data, index) in sortedUninstalledList" :key="`${data.id}_v${data.plugin_version}`">
|
||||
<PluginAppCard
|
||||
v-if="deferApp(index)"
|
||||
@@ -521,10 +516,3 @@ onBeforeMount(async () => {
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.grid-plugin-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -93,10 +93,3 @@ onUnmounted(() => {
|
||||
/>
|
||||
</PullRefresh>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.grid-downloading-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import api from '@/api'
|
||||
import { MediaDirectory } from '@/api/types'
|
||||
import FileBrowser from '@/components/FileBrowser.vue'
|
||||
|
||||
const endpoints = {
|
||||
@@ -29,42 +30,53 @@ const endpoints = {
|
||||
},
|
||||
}
|
||||
|
||||
// 读取下载目录
|
||||
// 当前目录
|
||||
const path: Ref<string | undefined> = ref()
|
||||
|
||||
// 调用API,加载当前系统环境设置
|
||||
function loadSystemSettings(): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
api
|
||||
.get('system/env')
|
||||
.then((result: any) => {
|
||||
let path = '/'
|
||||
if (result.success)
|
||||
path = result.data?.DOWNLOAD_PATH || '/'
|
||||
// 下载目录列表
|
||||
const downloadDirectories = ref<MediaDirectory[]>([])
|
||||
|
||||
if (!path.endsWith('/'))
|
||||
path += '/'
|
||||
|
||||
resolve(path)
|
||||
})
|
||||
.catch(error => reject(error))
|
||||
})
|
||||
// 计算公共路径
|
||||
function findCommonPath(paths: string[]): string {
|
||||
if (!paths || paths.length === 0) return ''
|
||||
if (paths.length === 1) return paths[0]
|
||||
const normalizedPaths = paths.map(path => path.replace(/\\/g, '/'))
|
||||
const splitPaths = normalizedPaths.map(path => path.split('/'))
|
||||
let commonParts: string[] = []
|
||||
for (let i = 0; i < splitPaths[0].length; i++) {
|
||||
const part = splitPaths[0][i]
|
||||
if (splitPaths.every(pathParts => pathParts[i] === part)) {
|
||||
commonParts.push(part)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
let commonPath = commonParts.join('/')
|
||||
if (commonPath.includes(':')) {
|
||||
commonPath = commonPath.replace('/', '\\')
|
||||
}
|
||||
return commonPath.length > 0 ? commonPath : paths[0][0] === '/' ? '/' : ''
|
||||
}
|
||||
|
||||
// 查询下载目录
|
||||
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
|
||||
path.value = findCommonPath(downloadDirectories.value.map(item => item.path) as string[])
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 目录变化
|
||||
function pathChanged(_path: string) {
|
||||
path.value = _path
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadSystemSettings()
|
||||
.then((res) => {
|
||||
path.value = res
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
path.value = '/'
|
||||
})
|
||||
})
|
||||
onBeforeMount(loadDownloadDirectories)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -19,9 +19,6 @@ const currentHistory = ref<TransferHistory>()
|
||||
// 重新整理IDS
|
||||
const redoIds = ref<number[]>([])
|
||||
|
||||
// 重新整理target
|
||||
const redoTarget = ref('')
|
||||
|
||||
// 已选中的数据
|
||||
const selected = ref<TransferHistory[]>([])
|
||||
|
||||
@@ -271,19 +268,6 @@ async function retransferBatch() {
|
||||
currentHistory.value = undefined
|
||||
// 重新整理IDS
|
||||
redoIds.value = selected.value.map(item => item.id)
|
||||
// 重新整理target
|
||||
if (selected.value.length === 1) {
|
||||
// 目的目录
|
||||
const dest = selected.value[0].dest ?? ''
|
||||
// 类型
|
||||
const mediaType = selected.value[0].type ?? ''
|
||||
// 分类
|
||||
const category = selected.value[0].category ?? ''
|
||||
// 计算根路径
|
||||
redoTarget.value = getRootPath(dest, mediaType, category)
|
||||
} else {
|
||||
redoTarget.value = ''
|
||||
}
|
||||
// 打开识别弹窗
|
||||
redoDialog.value = true
|
||||
}
|
||||
@@ -297,7 +281,6 @@ const dropdownItems = ref([
|
||||
prependIcon: 'mdi-redo-variant',
|
||||
click: (item: TransferHistory) => {
|
||||
redoIds.value = [item.id]
|
||||
redoTarget.value = getRootPath(item.dest ?? '', item.type ?? '', item.category ?? '')
|
||||
redoDialog.value = true
|
||||
},
|
||||
},
|
||||
@@ -456,7 +439,6 @@ onMounted(fetchData)
|
||||
v-if="redoDialog"
|
||||
v-model="redoDialog"
|
||||
:logids="redoIds"
|
||||
:target="redoTarget"
|
||||
@done="
|
||||
() => {
|
||||
redoDialog = false
|
||||
|
||||
@@ -85,10 +85,8 @@ async function loadAccountInfo() {
|
||||
const user: User = await api.get('user/current')
|
||||
console.log(user)
|
||||
accountInfo.value = user
|
||||
if (!accountInfo.value.avatar)
|
||||
accountInfo.value.avatar = avatar1
|
||||
}
|
||||
catch (error) {
|
||||
if (!accountInfo.value.avatar) accountInfo.value.avatar = avatar1
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
@@ -105,12 +103,9 @@ async function saveAccountInfo() {
|
||||
}
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.put('user/', accountInfo.value)
|
||||
if (result.success)
|
||||
$toast.success('用户信息保存成功!')
|
||||
else
|
||||
$toast.error(`用户信息保存失败:${result.message}!`)
|
||||
}
|
||||
catch (error) {
|
||||
if (result.success) $toast.success('用户信息保存成功!')
|
||||
else $toast.error(`用户信息保存失败:${result.message}!`)
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
@@ -121,8 +116,7 @@ async function loadAllUsers() {
|
||||
const result: User[] = await api.get('/user/')
|
||||
|
||||
allUsers.value = result
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
@@ -134,12 +128,10 @@ async function deleteUser(user: User) {
|
||||
if (result.success) {
|
||||
$toast.success('用户删除成功!')
|
||||
loadAllUsers()
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
$toast.error(`用户删除失败:${result.message}!`)
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
@@ -153,12 +145,10 @@ async function deactivateUser(user: User) {
|
||||
if (result.success) {
|
||||
$toast.success('用户冻结成功!')
|
||||
loadAllUsers()
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
$toast.error(`用户冻结失败:${result.message}!`)
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
@@ -175,12 +165,10 @@ async function addUser() {
|
||||
$toast.success('用户新增成功!')
|
||||
loadAllUsers()
|
||||
addUserDialog.value = false
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
$toast.error(`用户新增失败:${result.message}!`)
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
@@ -194,12 +182,10 @@ async function getOtpUri() {
|
||||
secret.value = result.data.secret
|
||||
qrCode.value = result.data.uri
|
||||
otpDialog.value = true
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
$toast.error(`获取otp uri失败:${result.message}!`)
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
@@ -211,12 +197,10 @@ async function disableOtp() {
|
||||
if (result.success) {
|
||||
accountInfo.value.is_otp = false
|
||||
$toast.success('关闭登录双重验证成功!')
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
$toast.error(`关闭otp失败:${result.message}!`)
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
@@ -228,18 +212,19 @@ async function judgeOtpPassword() {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post('user/otp/judge', { uri: otpUri.value, otpPassword: otpPassword.value })
|
||||
const result: { [key: string]: any } = await api.post('user/otp/judge', {
|
||||
uri: otpUri.value,
|
||||
otpPassword: otpPassword.value,
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
$toast.success('开启登录双重验证成功!')
|
||||
otpDialog.value = false
|
||||
accountInfo.value.is_otp = true
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
$toast.error(`开启otp失败:${result.message}!`)
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
@@ -258,23 +243,13 @@ onMounted(() => {
|
||||
<VCard title="个人信息">
|
||||
<VCardText class="d-flex">
|
||||
<!-- 👉 Avatar -->
|
||||
<VAvatar
|
||||
rounded="lg"
|
||||
size="100"
|
||||
class="me-6"
|
||||
:image="accountInfo.avatar"
|
||||
/>
|
||||
<VAvatar rounded="lg" size="100" class="me-6" :image="accountInfo.avatar" />
|
||||
|
||||
<!-- 👉 Upload Photo -->
|
||||
<form class="d-flex flex-column justify-center gap-5">
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<VBtn
|
||||
color="primary"
|
||||
@click="refInputEl?.click()"
|
||||
>
|
||||
<VIcon
|
||||
icon="mdi-cloud-upload-outline"
|
||||
/>
|
||||
<VBtn color="primary" @click="refInputEl?.click()">
|
||||
<VIcon icon="mdi-cloud-upload-outline" />
|
||||
<span class="d-none d-sm-block ms-2">上传头像</span>
|
||||
</VBtn>
|
||||
|
||||
@@ -285,17 +260,10 @@ onMounted(() => {
|
||||
accept=".jpeg,.png,.jpg,GIF"
|
||||
hidden
|
||||
@input="changeAvatar"
|
||||
>
|
||||
/>
|
||||
|
||||
<VBtn
|
||||
type="reset"
|
||||
color="error"
|
||||
variant="tonal"
|
||||
@click="resetAvatar"
|
||||
>
|
||||
<VIcon
|
||||
icon="mdi-refresh"
|
||||
/>
|
||||
<VBtn type="reset" color="error" variant="tonal" @click="resetAvatar">
|
||||
<VIcon icon="mdi-refresh" />
|
||||
<span class="d-none d-sm-block ms-2">重置</span>
|
||||
</VBtn>
|
||||
|
||||
@@ -304,16 +272,12 @@ onMounted(() => {
|
||||
variant="tonal"
|
||||
@click.stop="accountInfo.is_otp ? disableOtp() : getOtpUri()"
|
||||
>
|
||||
<VIcon
|
||||
icon="mdi-account-key"
|
||||
/>
|
||||
<span class="d-none d-sm-block ms-2">{{ accountInfo.is_otp ? "关闭验证" : "双重验证" }}</span>
|
||||
<VIcon icon="mdi-account-key" />
|
||||
<span class="d-none d-sm-block ms-2">{{ accountInfo.is_otp ? '关闭验证' : '双重验证' }}</span>
|
||||
</VBtn>
|
||||
</div>
|
||||
|
||||
<p class="text-body-1 mb-0">
|
||||
允许 JPG、GIF 或 PNG 格式, 最大尺寸 800K。
|
||||
</p>
|
||||
<p class="text-body-1 mb-0">允许 JPG、GIF 或 PNG 格式, 最大尺寸 800K。</p>
|
||||
</form>
|
||||
</VCardText>
|
||||
|
||||
@@ -324,33 +288,16 @@ onMounted(() => {
|
||||
<VForm class="mt-6">
|
||||
<VRow>
|
||||
<!-- 👉 Name -->
|
||||
<VCol
|
||||
md="6"
|
||||
cols="12"
|
||||
>
|
||||
<VTextField
|
||||
v-model="accountInfo.name"
|
||||
readonly
|
||||
label="用户名"
|
||||
/>
|
||||
<VCol md="6" cols="12">
|
||||
<VTextField v-model="accountInfo.name" readonly label="用户名" />
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Email -->
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VTextField
|
||||
v-model="accountInfo.email"
|
||||
label="邮箱"
|
||||
type="email"
|
||||
/>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField v-model="accountInfo.email" label="邮箱" type="email" />
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VCol cols="12" md="6">
|
||||
<!-- 👉 new password -->
|
||||
<VTextField
|
||||
v-model="newPassword"
|
||||
@@ -362,32 +309,20 @@ onMounted(() => {
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VCol cols="12" md="6">
|
||||
<!-- 👉 confirm password -->
|
||||
<VTextField
|
||||
v-model="confirmPassword"
|
||||
:type="isConfirmPasswordVisible ? 'text' : 'password'"
|
||||
:append-inner-icon="
|
||||
isConfirmPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'
|
||||
"
|
||||
:append-inner-icon="isConfirmPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
|
||||
label="确认新密码"
|
||||
@click:append-inner="
|
||||
isConfirmPasswordVisible = !isConfirmPasswordVisible
|
||||
"
|
||||
@click:append-inner="isConfirmPasswordVisible = !isConfirmPasswordVisible"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Form Actions -->
|
||||
<VCol
|
||||
cols="12"
|
||||
class="d-flex flex-wrap gap-4"
|
||||
>
|
||||
<VBtn @click="saveAccountInfo">
|
||||
保存
|
||||
</VBtn>
|
||||
<VCol cols="12" class="d-flex flex-wrap gap-4">
|
||||
<VBtn @click="saveAccountInfo"> 保存 </VBtn>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
@@ -395,10 +330,7 @@ onMounted(() => {
|
||||
</VCard>
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
v-if="accountInfo.is_superuser"
|
||||
cols="12"
|
||||
>
|
||||
<VCol v-if="accountInfo.is_superuser" cols="12">
|
||||
<!-- 👉 Accounts -->
|
||||
<VCard title="所有用户">
|
||||
<template #append>
|
||||
@@ -409,76 +341,38 @@ onMounted(() => {
|
||||
<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"
|
||||
/>
|
||||
<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"
|
||||
>
|
||||
<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>
|
||||
<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>{{ 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
|
||||
>
|
||||
<VMenu activator="parent" close-on-content-click>
|
||||
<VList>
|
||||
<VListItem
|
||||
variant="plain"
|
||||
@click="deactivateUser(user)"
|
||||
>
|
||||
<VListItem variant="plain" @click="deactivateUser(user)">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-lock" />
|
||||
</template>
|
||||
<VListItemTitle>
|
||||
{{
|
||||
user.is_active ? "冻结" : "解冻"
|
||||
}}
|
||||
{{ user.is_active ? '冻结' : '解冻' }}
|
||||
</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem
|
||||
variant="plain"
|
||||
base-color="error"
|
||||
@click="deleteUser(user)"
|
||||
>
|
||||
<VListItem variant="plain" base-color="error" @click="deleteUser(user)">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-delete" />
|
||||
</template>
|
||||
@@ -495,85 +389,50 @@ onMounted(() => {
|
||||
</VCol>
|
||||
</VRow>
|
||||
<!-- =弹窗 -->
|
||||
<VDialog
|
||||
v-model="addUserDialog"
|
||||
max-width="50rem"
|
||||
persistent
|
||||
z-index="1010"
|
||||
>
|
||||
<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 cols="12" md="6">
|
||||
<VTextField v-model="userForm.name" label="用户名" :rules="[requiredValidator]" />
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<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'
|
||||
"
|
||||
: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 cols="12" md="6">
|
||||
<VTextField v-model="userForm.email" :rules="[requiredValidator]" label="邮箱" />
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VBtn @click="addUserDialog = false">
|
||||
取消
|
||||
</VBtn>
|
||||
<VBtn @click="addUserDialog = false"> 取消 </VBtn>
|
||||
<VSpacer />
|
||||
<VBtn @click="addUser">
|
||||
确定
|
||||
</VBtn>
|
||||
<VBtn @click="addUser"> 确定 </VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
|
||||
<!-- 双重验证弹窗 -->
|
||||
<VDialog
|
||||
v-model="otpDialog"
|
||||
max-width="45rem"
|
||||
persistent
|
||||
z-index="1010"
|
||||
>
|
||||
<VDialog v-model="otpDialog" max-width="45rem" persistent z-index="1010">
|
||||
<!-- 开启双重验证弹窗内容 -->
|
||||
<VCard>
|
||||
<DialogCloseBtn @click="otpDialog = false" />
|
||||
<VCardText>
|
||||
<h4 class="text-h4 text-center mb-6 mt-5">
|
||||
登录双重验证
|
||||
</h4><h5 class="text-h5 font-weight-medium mb-2">
|
||||
身份验证器
|
||||
</h5>
|
||||
<h4 class="text-h4 text-center mb-6 mt-5">登录双重验证</h4>
|
||||
<h5 class="text-h5 font-weight-medium mb-2">身份验证器</h5>
|
||||
<p class="mb-6">
|
||||
使用像Google Authenticator、Microsoft Authenticator、Authy或1Password这样的身份验证器应用程序,扫描二维码。它将为您生成一个6位数的代码,供您在下方输入。
|
||||
使用像Google Authenticator、Microsoft
|
||||
Authenticator、Authy或1Password这样的身份验证器应用程序,扫描二维码。它将为您生成一个6位数的代码,供您在下方输入。
|
||||
</p>
|
||||
<div class="my-6">
|
||||
<QrcodeVue class="mx-auto" :value="qrCode" :size="200" max-width="25rem" />
|
||||
@@ -597,14 +456,12 @@ onMounted(() => {
|
||||
variant="outlined"
|
||||
/>
|
||||
<div class="d-flex justify-end flex-wrap gap-4">
|
||||
<VBtn variant="outlined" color="secondary" @click="otpDialog = false">
|
||||
取消
|
||||
</VBtn>
|
||||
<VBtn variant="outlined" color="secondary" @click="otpDialog = false"> 取消 </VBtn>
|
||||
<VBtn @click="judgeOtpPassword">
|
||||
确定
|
||||
<template #append>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-check" />
|
||||
</template>
|
||||
确定
|
||||
</VBtn>
|
||||
</div>
|
||||
</VForm>
|
||||
|
||||
321
src/views/setting/AccountSettingDirectory.vue
Normal file
321
src/views/setting/AccountSettingDirectory.vue
Normal file
@@ -0,0 +1,321 @@
|
||||
<!-- 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 { MediaDirectory } from '@/api/types'
|
||||
import MediaDirectoryCard from '@/components/cards/MediaDirectoryCard.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 libraryDirectories = ref<MediaDirectory[]>([])
|
||||
|
||||
// 二级分类策略
|
||||
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() {
|
||||
// 更新所有目录的优先级
|
||||
downloadDirectories.value.forEach((item, index) => {
|
||||
item.priority = index
|
||||
})
|
||||
}
|
||||
|
||||
// 移动结束
|
||||
function orderLibraryCards() {
|
||||
// 更新所有目录的优先级
|
||||
libraryDirectories.value.forEach((item, index) => {
|
||||
item.priority = index
|
||||
})
|
||||
}
|
||||
|
||||
// 关闭目录卡片
|
||||
function libraryCardClose(name: string) {
|
||||
libraryDirectories.value = libraryDirectories.value.filter(item => item.name !== name)
|
||||
}
|
||||
|
||||
// 关闭下载卡片
|
||||
function downloadCardClose(name: string) {
|
||||
downloadDirectories.value = downloadDirectories.value.filter(item => item.name !== name)
|
||||
}
|
||||
|
||||
// 查询下载目录
|
||||
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('保存媒体库目录设置失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 添加媒体库目录
|
||||
function addLibraryDirectory() {
|
||||
libraryDirectories.value.push({
|
||||
name: `媒体库目录${libraryDirectories.value.length + 1}`,
|
||||
path: '',
|
||||
media_type: '全部',
|
||||
category: '',
|
||||
scrape: true,
|
||||
})
|
||||
}
|
||||
|
||||
// 调用API查询自动分类配置
|
||||
async function loadMediaCategories() {
|
||||
try {
|
||||
mediaCategories.value = await api.get('media/category')
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
onMounted(() => {
|
||||
loadTransferSettings()
|
||||
loadMediaCategories()
|
||||
loadDownloadDirectories()
|
||||
loadLibraryDirectories()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>下载目录</VCardTitle>
|
||||
<VCardSubtitle>设置下载目录路径和分类,按顺序依次匹配使用。</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<draggable
|
||||
v-model="downloadDirectories"
|
||||
handle=".cursor-move"
|
||||
item-key="pri"
|
||||
tag="div"
|
||||
@end="orderDownloadCards"
|
||||
:component-data="{ 'class': 'grid gap-3 grid-directory-card' }"
|
||||
>
|
||||
<template #item="{ element }">
|
||||
<MediaDirectoryCard
|
||||
type="download"
|
||||
:directory="element"
|
||||
:categories="mediaCategories"
|
||||
@close="downloadCardClose(element.name)"
|
||||
/>
|
||||
</template>
|
||||
</draggable>
|
||||
</VCardText>
|
||||
<VCardText>
|
||||
<VBtn type="submit" class="me-2" @click="saveDownloadDirectories"> 保存 </VBtn>
|
||||
<VBtn color="success" variant="tonal" @click="addDownloadDirectory">
|
||||
<VIcon icon="mdi-plus" />
|
||||
</VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>媒体库目录</VCardTitle>
|
||||
<VCardSubtitle>设置媒体文件整理后存储目录和分类,按顺序依次匹配使用。</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<draggable
|
||||
v-model="libraryDirectories"
|
||||
handle=".cursor-move"
|
||||
item-key="pri"
|
||||
tag="div"
|
||||
@end="orderLibraryCards"
|
||||
:component-data="{ 'class': 'grid gap-3 grid-directory-card' }"
|
||||
>
|
||||
<template #item="{ element }">
|
||||
<MediaDirectoryCard
|
||||
type="library"
|
||||
:directory="element"
|
||||
:categories="mediaCategories"
|
||||
@close="libraryCardClose(element.name)"
|
||||
/>
|
||||
</template>
|
||||
</draggable>
|
||||
</VCardText>
|
||||
<VCardText>
|
||||
<VBtn type="submit" class="me-2" @click="saveLibraryDirectories"> 保存 </VBtn>
|
||||
<VBtn color="success" variant="tonal" @click="addLibraryDirectory">
|
||||
<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="硬链接需要确保下载目录和媒体库目录不跨盘、不跨共享目录、不分别映射;rclone需要手动在容器中完成配置,且配置名为:`MP`"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSelect
|
||||
v-model="transferSettings.OVERWRITE_MODE"
|
||||
:items="overwriteModeItems"
|
||||
label="覆盖模式"
|
||||
hint="从不覆盖:不覆盖已存在的文件;按大小覆盖:大文件将覆盖小文件;总是覆盖:总是覆盖已存在的文件;仅保留最新版本:保留最新版本的文件,删除其它版本的文件"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="transferSettings.TRANSFER_SAME_DISK"
|
||||
label="同盘/同根目录优先"
|
||||
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>
|
||||
@@ -67,8 +67,7 @@ async function loadNotificationSwitchs() {
|
||||
const result: NotificationSwitch[] = await api.get('message/switchs')
|
||||
|
||||
messagemTypes.value = result
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
@@ -76,17 +75,11 @@ async function loadNotificationSwitchs() {
|
||||
// 调用API保存消息开关
|
||||
async function saveNotificationSwitchs() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post(
|
||||
'message/switchs',
|
||||
messagemTypes.value,
|
||||
)
|
||||
const result: { [key: string]: any } = await api.post('message/switchs', messagemTypes.value)
|
||||
|
||||
if (result.success)
|
||||
$toast.success('保存通知消息设置成功')
|
||||
else
|
||||
$toast.error('保存通知消息设置失败!')
|
||||
}
|
||||
catch (error) {
|
||||
if (result.success) $toast.success('保存通知消息设置成功')
|
||||
else $toast.error('保存通知消息设置失败!')
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
@@ -143,8 +136,7 @@ async function loadNotificationSettings() {
|
||||
VOCECHAT_CHANNEL_ID,
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
@@ -152,23 +144,17 @@ async function loadNotificationSettings() {
|
||||
// 调用API保存消息渠道设置
|
||||
async function saveNotificationSettings() {
|
||||
try {
|
||||
const result1: { [key: string]: any } = await api.post(
|
||||
'system/setting/MESSAGER',
|
||||
selectedChannels.value.join(','),
|
||||
)
|
||||
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,
|
||||
)
|
||||
const result2: { [key: string]: any } = await api.post('system/env', notificationSettings.value)
|
||||
|
||||
if (result1.success && result2.success) {
|
||||
$toast.success('保存通知渠道设置成功')
|
||||
reloadModule()
|
||||
} else {
|
||||
$toast.error('保存通知渠道设置失败!')
|
||||
}
|
||||
else { $toast.error('保存通知渠道设置失败!') }
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
@@ -177,12 +163,9 @@ async function saveNotificationSettings() {
|
||||
async function reloadModule() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/reload')
|
||||
if (result.success)
|
||||
$toast.success('重新加载模块成功')
|
||||
else
|
||||
$toast.error('重新加载模块失败!')
|
||||
}
|
||||
catch (error) {
|
||||
if (result.success) $toast.success('重新加载模块成功')
|
||||
else $toast.error('重新加载模块失败!')
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
@@ -197,8 +180,11 @@ onMounted(() => {
|
||||
<template>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VCard title="通知渠道">
|
||||
<VCardSubtitle>只有选中的渠道才会发送消息。</VCardSubtitle>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>通知渠道</VCardTitle>
|
||||
<VCardSubtitle>只有选中的渠道才会发送消息。</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VForm>
|
||||
<VRow>
|
||||
@@ -215,31 +201,14 @@ onMounted(() => {
|
||||
</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 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"
|
||||
>
|
||||
<VWindow v-model="messagerTab" class="mt-5 disable-tab-transition" :touch="false">
|
||||
<VWindowItem value="wechat">
|
||||
<VForm>
|
||||
<VRow>
|
||||
@@ -386,10 +355,7 @@ onMounted(() => {
|
||||
<VForm>
|
||||
<VRow>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="notificationSettings.VOCECHAT_HOST"
|
||||
label="地址"
|
||||
/>
|
||||
<VTextField v-model="notificationSettings.VOCECHAT_HOST" label="地址" />
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
@@ -417,12 +383,7 @@ onMounted(() => {
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<div class="d-flex flex-wrap gap-4 mt-4">
|
||||
<VBtn
|
||||
mtype="submit"
|
||||
@click="saveNotificationSettings"
|
||||
>
|
||||
保存
|
||||
</VBtn>
|
||||
<VBtn mtype="submit" @click="saveNotificationSettings"> 保存 </VBtn>
|
||||
</div>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
@@ -431,36 +392,24 @@ onMounted(() => {
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VCard title="消息类型">
|
||||
<VCardSubtitle> 对应消息类型只会发送给选中的消息渠道。 </VCardSubtitle>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>消息类型</VCardTitle>
|
||||
<VCardSubtitle>对应消息类型只会发送给选中的消息渠道。</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VTable class="text-no-wrap">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">
|
||||
消息类型
|
||||
</th>
|
||||
<th scope="col">
|
||||
微信
|
||||
</th>
|
||||
<th scope="col">
|
||||
Telegram
|
||||
</th>
|
||||
<th scope="col">
|
||||
Slack
|
||||
</th>
|
||||
<th scope="col">
|
||||
SynologyChat
|
||||
</th>
|
||||
<th scope="col">
|
||||
VoceChat
|
||||
</th>
|
||||
<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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="message in messagemTypes"
|
||||
:key="message.mtype"
|
||||
>
|
||||
<tr v-for="message in messagemTypes" :key="message.mtype">
|
||||
<td>
|
||||
{{ message.mtype }}
|
||||
</td>
|
||||
@@ -481,26 +430,15 @@ onMounted(() => {
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="messagemTypes.length === 0">
|
||||
<td
|
||||
colspan="6"
|
||||
class="text-center"
|
||||
>
|
||||
没有设置任何通知渠道
|
||||
</td>
|
||||
<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="saveNotificationSwitchs"> 保存 </VBtn>
|
||||
</div>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<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'
|
||||
@@ -170,42 +171,12 @@ async function saveSelectedSites() {
|
||||
}
|
||||
}
|
||||
|
||||
// 上调优先级
|
||||
function onLevelUp(pri: string) {
|
||||
// 找到当前卡片
|
||||
const card = filterCards.value.find(card => card.pri === pri)
|
||||
if (!card) return
|
||||
|
||||
// 找到当前卡片的上一张卡片
|
||||
const prevCard = filterCards.value.find(card => card.pri === (parseInt(pri) - 1).toString())
|
||||
if (!prevCard) return
|
||||
|
||||
// 交换两张卡片的优先级
|
||||
const temp = card.pri
|
||||
card.pri = prevCard.pri
|
||||
prevCard.pri = temp
|
||||
|
||||
// 卡片重新按优先级排序
|
||||
filterCards.value.sort((a, b) => parseInt(a.pri) - parseInt(b.pri))
|
||||
}
|
||||
|
||||
// 下调优先级
|
||||
function onLevelDown(pri: string) {
|
||||
// 找到当前卡片
|
||||
const card = filterCards.value.find(card => card.pri === pri)
|
||||
if (!card) return
|
||||
|
||||
// 找到当前卡片的下一张卡片
|
||||
const nextCard = filterCards.value.find(card => card.pri === (parseInt(pri) + 1).toString())
|
||||
if (!nextCard) return
|
||||
|
||||
// 交换两张卡片的优先级
|
||||
const temp = card.pri
|
||||
card.pri = nextCard.pri
|
||||
nextCard.pri = temp
|
||||
|
||||
// 卡片重新按优先级排序
|
||||
filterCards.value.sort((a, b) => parseInt(a.pri) - parseInt(b.pri))
|
||||
// 根据列表的拖动顺序更新优先级
|
||||
function dragOrderEnd() {
|
||||
filterCards.value = filterCards.value.map((card, index) => {
|
||||
card.pri = (index + 1).toString()
|
||||
return card
|
||||
})
|
||||
}
|
||||
|
||||
// 查询包含与排除规则
|
||||
@@ -309,8 +280,11 @@ onMounted(() => {
|
||||
<template>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VCard title="媒体数据源">
|
||||
<VCardSubtitle> 设定搜索时展示哪些源的媒体信息。</VCardSubtitle>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>媒体数据源</VCardTitle>
|
||||
<VCardSubtitle>设定搜索时展示哪些源的媒体信息。</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -325,17 +299,18 @@ onMounted(() => {
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
|
||||
<VCardItem>
|
||||
<VCardText>
|
||||
<VBtn type="submit" @click="saveMediaSourceSetting"> 保存 </VBtn>
|
||||
</VCardItem>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VCard title="搜索站点">
|
||||
<VCardSubtitle> 只有选中的站点才会在搜索中使用。</VCardSubtitle>
|
||||
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>搜索站点</VCardTitle>
|
||||
<VCardSubtitle> 只有选中的站点才会在搜索中使用。</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VChipGroup v-model="selectedSites" column multiple>
|
||||
<VChip
|
||||
v-for="site in allSites"
|
||||
@@ -348,63 +323,73 @@ onMounted(() => {
|
||||
{{ site.name }}
|
||||
</VChip>
|
||||
</VChipGroup>
|
||||
</VCardItem>
|
||||
|
||||
<VCardItem>
|
||||
</VCardText>
|
||||
<VCardText>
|
||||
<VBtn type="submit" @click="saveSelectedSites"> 保存 </VBtn>
|
||||
</VCardItem>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VCard title="搜索优先级">
|
||||
<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>
|
||||
<VCardSubtitle> 设置在搜索时默认使用的优先级排序,未在优先级中的资源将不在搜索结果中显示。 </VCardSubtitle>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<div class="grid gap-3 grid-filterrule-card">
|
||||
<FilterRuleCard
|
||||
v-for="(card, index) in filterCards"
|
||||
:key="index"
|
||||
:pri="card.pri"
|
||||
:maxpri="filterCards.length.toString()"
|
||||
:rules="card.rules"
|
||||
@changed="updateFilterCardValue"
|
||||
@close="filterCardClose(card.pri)"
|
||||
@leveldown="onLevelDown"
|
||||
@levelup="onLevelUp"
|
||||
/>
|
||||
</div>
|
||||
<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>
|
||||
<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>
|
||||
</VCardItem>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VCard title="默认过滤规则">
|
||||
<VCardSubtitle> 设置在搜索时默认使用的过滤规则。 </VCardSubtitle>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>默认过滤规则</VCardTitle>
|
||||
<VCardSubtitle>设置在搜索时默认使用的过滤规则。</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VForm>
|
||||
<VRow>
|
||||
@@ -445,9 +430,9 @@ onMounted(() => {
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
<VCardItem>
|
||||
<VCardText>
|
||||
<VBtn type="submit" @click="saveDefaultFilter"> 保存 </VBtn>
|
||||
</VCardItem>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -455,10 +440,3 @@ onMounted(() => {
|
||||
<ImportCodeDialog v-model="importCodeString" title="导入优先级规则" @close="importCodeDialog = false" />
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.grid-filterrule-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -18,8 +18,7 @@ async function loadSchedulerList() {
|
||||
const res: ScheduleInfo[] = await api.get('dashboard/schedule')
|
||||
|
||||
schedulerList.value = res
|
||||
}
|
||||
catch (e) {
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
@@ -52,8 +51,7 @@ function runCommand(id: string) {
|
||||
setTimeout(() => {
|
||||
loadSchedulerList()
|
||||
}, 1000)
|
||||
}
|
||||
catch (e) {
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
@@ -77,32 +75,23 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard title="定时作业">
|
||||
<VCardSubtitle> 包含系统内置服务以及插件提供的服务,手动执行不会影响作业正常的时间表。 </VCardSubtitle>
|
||||
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>定时作业</VCardTitle>
|
||||
<VCardSubtitle>包含系统内置服务以及插件提供的服务,手动执行不会影响作业正常的时间表。</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VTable class="text-no-wrap">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">
|
||||
提供者
|
||||
</th>
|
||||
<th scope="col">
|
||||
任务名称
|
||||
</th>
|
||||
<th scope="col">
|
||||
任务状态
|
||||
</th>
|
||||
<th scope="col">
|
||||
下一次执行时间
|
||||
</th>
|
||||
<th scope="col">提供者</th>
|
||||
<th scope="col">任务名称</th>
|
||||
<th scope="col">任务状态</th>
|
||||
<th scope="col">下一次执行时间</th>
|
||||
<th scope="col" />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="scheduler in schedulerList"
|
||||
:key="scheduler.id"
|
||||
>
|
||||
<tr v-for="scheduler in schedulerList" :key="scheduler.id">
|
||||
<td>
|
||||
{{ scheduler.provider }}
|
||||
</td>
|
||||
@@ -118,11 +107,7 @@ onUnmounted(() => {
|
||||
{{ scheduler.next_run }}
|
||||
</td>
|
||||
<td>
|
||||
<VBtn
|
||||
size="small"
|
||||
:disabled="scheduler.status === '正在运行'"
|
||||
@click="runCommand(scheduler.id)"
|
||||
>
|
||||
<VBtn size="small" :disabled="scheduler.status === '正在运行'" @click="runCommand(scheduler.id)">
|
||||
<template #prepend>
|
||||
<VIcon>mdi-play</VIcon>
|
||||
</template>
|
||||
@@ -131,12 +116,7 @@ onUnmounted(() => {
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="schedulerList.length === 0">
|
||||
<td
|
||||
colspan="4"
|
||||
class="text-center"
|
||||
>
|
||||
没有后台服务
|
||||
</td>
|
||||
<td colspan="4" class="text-center">没有后台服务</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</VTable>
|
||||
|
||||
@@ -51,16 +51,12 @@ async function resetSites() {
|
||||
resetSitesText.value = '正在重置...'
|
||||
|
||||
const result: { [key: string]: any } = await api.get('site/reset')
|
||||
if (result.success)
|
||||
$toast.success('站点重置成功,请等待CookieCloud同步完成!')
|
||||
|
||||
else
|
||||
$toast.error('站点重置失败!')
|
||||
if (result.success) $toast.success('站点重置成功,请等待CookieCloud同步完成!')
|
||||
else $toast.error('站点重置失败!')
|
||||
|
||||
resetSitesDisabled.value = false
|
||||
resetSitesText.value = '重置站点数据'
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
@@ -68,13 +64,10 @@ async function resetSites() {
|
||||
// 查询种子优先规则
|
||||
async function queryTorrentPriority() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get(
|
||||
'system/setting/TorrentsPriority',
|
||||
)
|
||||
const result: { [key: string]: any } = await api.get('system/setting/TorrentsPriority')
|
||||
|
||||
selectedTorrentPriority.value = result.data?.value
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
@@ -88,12 +81,9 @@ async function saveTorrentPriority() {
|
||||
selectedTorrentPriority.value,
|
||||
)
|
||||
|
||||
if (result.success)
|
||||
$toast.success('优先规则保存成功')
|
||||
else
|
||||
$toast.error('优先规则保存失败!')
|
||||
}
|
||||
catch (error) {
|
||||
if (result.success) $toast.success('优先规则保存成功')
|
||||
else $toast.error('优先规则保存失败!')
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
@@ -120,8 +110,7 @@ async function loadCookieCloudSettings() {
|
||||
COOKIECLOUD_ENABLE_LOCAL,
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
@@ -129,17 +118,11 @@ async function loadCookieCloudSettings() {
|
||||
// 调用API保存CookieCloud设置
|
||||
async function saveCookieCloudetting() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post(
|
||||
'system/env',
|
||||
cookieCloudSetting.value,
|
||||
)
|
||||
const result: { [key: string]: any } = await api.post('system/env', cookieCloudSetting.value)
|
||||
|
||||
if (result.success)
|
||||
$toast.success('保存站点同步设置成功')
|
||||
else
|
||||
$toast.error('保存站点同步设置失败!')
|
||||
}
|
||||
catch (error) {
|
||||
if (result.success) $toast.success('保存站点同步设置成功')
|
||||
else $toast.error('保存站点同步设置失败!')
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
@@ -154,8 +137,11 @@ onMounted(() => {
|
||||
<template>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VCard title="站点同步">
|
||||
<VCardSubtitle> 从CookieCloud快速同步站点数据。 </VCardSubtitle>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>站点同步</VCardTitle>
|
||||
<VCardSubtitle>从CookieCloud快速同步站点数据。</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VForm>
|
||||
<VRow>
|
||||
@@ -210,19 +196,17 @@ onMounted(() => {
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
<VCardItem>
|
||||
<VBtn
|
||||
type="submit"
|
||||
@click="saveCookieCloudetting"
|
||||
>
|
||||
保存
|
||||
</VBtn>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VBtn type="submit" @click="saveCookieCloudetting"> 保存 </VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VCard title="下载优先规则">
|
||||
<VCardSubtitle> 按站点或做种数量优先下载。 </VCardSubtitle>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>下载优先规则</VCardTitle>
|
||||
<VCardSubtitle>按站点或做种数量优先下载。</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VForm>
|
||||
<VRow>
|
||||
@@ -237,14 +221,9 @@ onMounted(() => {
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
<VCardItem>
|
||||
<VBtn
|
||||
type="submit"
|
||||
@click="saveTorrentPriority"
|
||||
>
|
||||
保存
|
||||
</VBtn>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VBtn type="submit" @click="saveTorrentPriority"> 保存 </VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang='ts' setup>
|
||||
<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'
|
||||
@@ -42,7 +43,7 @@ const defaultFilterRules = ref({
|
||||
movie_size: '',
|
||||
tv_size: '',
|
||||
min_seeders: 0,
|
||||
min_seeders_time: 0
|
||||
min_seeders_time: 0,
|
||||
})
|
||||
|
||||
// 订阅模式选择项
|
||||
@@ -80,8 +81,7 @@ async function querySelectedRssSites() {
|
||||
const result: { [key: string]: any } = await api.get('system/setting/RssSites')
|
||||
|
||||
selectedRssSites.value = result.data?.value ?? []
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
@@ -89,31 +89,23 @@ async function querySelectedRssSites() {
|
||||
// 保存用户选中的订阅站点
|
||||
async function saveSelectedRssSites() {
|
||||
try {
|
||||
const result1: { [key: string]: any } = await api.post(
|
||||
'system/setting/RssSites',
|
||||
selectedRssSites.value)
|
||||
const result1: { [key: string]: any } = await api.post('system/setting/RssSites', selectedRssSites.value)
|
||||
|
||||
const result2: { [key: string]: any } = await api.post(
|
||||
'system/setting/SUBSCRIBE_SEARCH',
|
||||
enableIntervalSearch.value ? 'True' : 'False',
|
||||
)
|
||||
|
||||
const result3: { [key: string]: any } = await api.post(
|
||||
'system/setting/SUBSCRIBE_MODE',
|
||||
selectedSubscribeMode.value,
|
||||
)
|
||||
const result3: { [key: string]: any } = await api.post('system/setting/SUBSCRIBE_MODE', selectedSubscribeMode.value)
|
||||
|
||||
const result4: { [key: string]: any } = await api.post(
|
||||
'system/setting/SUBSCRIBE_RSS_INTERVAL',
|
||||
selectedRssInterval.value,
|
||||
)
|
||||
|
||||
if (result1.success && result2.success && result3.success && result4.success)
|
||||
$toast.success('订阅站点保存成功')
|
||||
else
|
||||
$toast.error('订阅站点保存失败!')
|
||||
}
|
||||
catch (error) {
|
||||
if (result1.success && result2.success && result3.success && result4.success) $toast.success('订阅站点保存成功')
|
||||
else $toast.error('订阅站点保存失败!')
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
@@ -129,18 +121,14 @@ async function querySites() {
|
||||
|
||||
// 查询订阅搜索开关
|
||||
const result: { [key: string]: any } = await api.get('system/setting/SUBSCRIBE_SEARCH')
|
||||
if (result.success)
|
||||
enableIntervalSearch.value = result.data?.value
|
||||
if (result.success) enableIntervalSearch.value = result.data?.value
|
||||
// 查询订阅模式
|
||||
const result2: { [key: string]: any } = await api.get('system/setting/SUBSCRIBE_MODE')
|
||||
if (result2.success)
|
||||
selectedSubscribeMode.value = result2.data?.value
|
||||
if (result2.success) selectedSubscribeMode.value = result2.data?.value
|
||||
// 查询站点RSS周期
|
||||
const result3: { [key: string]: any } = await api.get('system/setting/SUBSCRIBE_RSS_INTERVAL')
|
||||
if (result3.success)
|
||||
selectedRssInterval.value = result3.data?.value
|
||||
}
|
||||
catch (error) {
|
||||
if (result3.success) selectedRssInterval.value = result3.data?.value
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
@@ -162,8 +150,7 @@ async function queryCustomFilters(ruleType: string) {
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
@@ -182,35 +169,27 @@ async function saveCustomFilters(ruleType: string) {
|
||||
.join('>')
|
||||
}
|
||||
// 保存
|
||||
const result: { [key: string]: any } = await api.post(
|
||||
`system/setting/${ruleType}`,
|
||||
value,
|
||||
)
|
||||
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) {
|
||||
if (result.success) $toast.success(`${msg}保存成功`)
|
||||
else $toast.error(`${msg}保存失败!`)
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 更新规则卡片的值
|
||||
function updateFilterCardValue(pri: string, rules: string[]) {
|
||||
function updateSubscribeFilterCardValue(pri: string, rules: string[]) {
|
||||
const card = subscribeFilterCards.value.find(card => card.pri === pri)
|
||||
if (card)
|
||||
card.rules = rules
|
||||
if (card) card.rules = rules
|
||||
}
|
||||
|
||||
// 更新洗版规则卡片的值
|
||||
function updateFilterCardValue2(pri: string, rules: string[]) {
|
||||
function updateBestVersionFilterCardValue(pri: string, rules: string[]) {
|
||||
const card = bestVersionFilterCards.value.find(card => card.pri === pri)
|
||||
if (card)
|
||||
card.rules = rules
|
||||
if (card) card.rules = rules
|
||||
}
|
||||
|
||||
// 移除卡片
|
||||
@@ -223,10 +202,8 @@ function filterCardClose(ruleType: string, pri: string) {
|
||||
return card
|
||||
})
|
||||
// 更新 subscribeFilterCards.value
|
||||
if (ruleType === 'SubscribeFilterRules')
|
||||
subscribeFilterCards.value = updatedCards
|
||||
else
|
||||
bestVersionFilterCards.value = updatedCards
|
||||
if (ruleType === 'SubscribeFilterRules') subscribeFilterCards.value = updatedCards
|
||||
else bestVersionFilterCards.value = updatedCards
|
||||
}
|
||||
|
||||
// 增加卡片
|
||||
@@ -242,58 +219,22 @@ function addFilterCard(ruleType: string) {
|
||||
cards.value.push(newCard)
|
||||
}
|
||||
|
||||
// 上调优先级
|
||||
function onLevelUp(filterCards: FilterCard[], pri: string) {
|
||||
// 找到当前卡片
|
||||
const card = filterCards.find(card => card.pri === pri)
|
||||
if (!card)
|
||||
return
|
||||
|
||||
// 找到当前卡片的上一张卡片
|
||||
const prevCard = filterCards.find(card => card.pri === (parseInt(pri) - 1).toString())
|
||||
if (!prevCard)
|
||||
return
|
||||
|
||||
// 交换两张卡片的优先级
|
||||
const temp = card.pri
|
||||
card.pri = prevCard.pri
|
||||
prevCard.pri = temp
|
||||
|
||||
// 卡片重新按优先级排序
|
||||
filterCards.sort((a, b) => parseInt(a.pri) - parseInt(b.pri))
|
||||
}
|
||||
|
||||
// 下调优先级
|
||||
function onLevelDown(filterCards: FilterCard[], pri: string) {
|
||||
// 找到当前卡片
|
||||
const card = filterCards.find(card => card.pri === pri)
|
||||
if (!card)
|
||||
return
|
||||
|
||||
// 找到当前卡片的下一张卡片
|
||||
const nextCard = filterCards.find(card => card.pri === (parseInt(pri) + 1).toString())
|
||||
if (!nextCard)
|
||||
return
|
||||
|
||||
// 交换两张卡片的优先级
|
||||
const temp = card.pri
|
||||
card.pri = nextCard.pri
|
||||
nextCard.pri = temp
|
||||
|
||||
// 卡片重新按优先级排序
|
||||
filterCards.sort((a, b) => parseInt(a.pri) - parseInt(b.pri))
|
||||
// 根据列表的拖动顺序更新优先级
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -301,16 +242,10 @@ async function queryDefaultFilter() {
|
||||
// 保存包含与排除规则
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -318,13 +253,10 @@ async function saveDefaultFilter() {
|
||||
// 分享规则
|
||||
function shareRules(ruleType: string) {
|
||||
let filterCards: Ref<FilterCard[]>
|
||||
if (ruleType === 'SubscribeFilterRules')
|
||||
filterCards = subscribeFilterCards
|
||||
else
|
||||
filterCards = bestVersionFilterCards
|
||||
if (ruleType === 'SubscribeFilterRules') filterCards = subscribeFilterCards
|
||||
else filterCards = bestVersionFilterCards
|
||||
// 有值才处理
|
||||
if (filterCards.value.length === 0)
|
||||
return
|
||||
if (filterCards.value.length === 0) return
|
||||
|
||||
// 将卡片规则接装为字符串
|
||||
const value = filterCards.value
|
||||
@@ -336,8 +268,7 @@ function shareRules(ruleType: string) {
|
||||
try {
|
||||
copyToClipboard(value)
|
||||
$toast.success('优先级规则已复制到剪贴板')
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
$toast.error('优先级规则复制失败!')
|
||||
}
|
||||
}
|
||||
@@ -351,20 +282,14 @@ async function importRules(ruleType: string) {
|
||||
|
||||
// 监听导入代码变化
|
||||
watchEffect(() => {
|
||||
if (!importCodeString.value)
|
||||
return
|
||||
if (!currentRuleType.value)
|
||||
return
|
||||
if (!importCodeString.value) return
|
||||
if (!currentRuleType.value) return
|
||||
// 导入代码需要以空格开头和结束,没有则拼接
|
||||
if (!importCodeString.value.startsWith(' '))
|
||||
importCodeString.value = ` ${importCodeString.value}`
|
||||
if (!importCodeString.value.endsWith(' '))
|
||||
importCodeString.value = `${importCodeString.value} `
|
||||
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
|
||||
if (currentRuleType.value === 'SubscribeFilterRules') filterCards = subscribeFilterCards
|
||||
else filterCards = bestVersionFilterCards
|
||||
// 将导入的代码转换为规则卡片
|
||||
const groups = importCodeString.value.split('>')
|
||||
filterCards.value = groups.map((group: string, index: number) => {
|
||||
@@ -386,10 +311,12 @@ onMounted(() => {
|
||||
<template>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VCard title="订阅站点">
|
||||
<VCardSubtitle> 只有选中的站点才会在订阅中使用。</VCardSubtitle>
|
||||
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>订阅站点</VCardTitle>
|
||||
<VCardSubtitle>只有选中的站点才会在订阅中使用。</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VChipGroup v-model="selectedRssSites" column multiple>
|
||||
<VChip
|
||||
v-for="site in allSites"
|
||||
@@ -402,7 +329,7 @@ onMounted(() => {
|
||||
{{ site.name }}
|
||||
</VChip>
|
||||
</VChipGroup>
|
||||
</VCardItem>
|
||||
</VCardText>
|
||||
<VCardText>
|
||||
<VForm>
|
||||
<VRow>
|
||||
@@ -434,148 +361,127 @@ onMounted(() => {
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
<VCardItem>
|
||||
<VBtn type="submit" @click="saveSelectedRssSites">
|
||||
保存
|
||||
</VBtn>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VBtn type="submit" @click="saveSelectedRssSites"> 保存 </VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VCard title="订阅优先级">
|
||||
<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>
|
||||
<VCardSubtitle> 设置在正常订阅时默认使用的优先级,未在优先级中的资源将不会自动下载。</VCardSubtitle>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<div class="grid gap-3 grid-filterrule-card">
|
||||
<FilterRuleCard
|
||||
v-for="(card, index) in subscribeFilterCards"
|
||||
:key="index"
|
||||
:pri="card.pri"
|
||||
:maxpri="subscribeFilterCards.length.toString()"
|
||||
:rules="card.rules"
|
||||
@changed="updateFilterCardValue"
|
||||
@close="filterCardClose('SubscribeFilterRules', card.pri)"
|
||||
@leveldown="onLevelDown(subscribeFilterCards, card.pri)"
|
||||
@levelup="onLevelUp(subscribeFilterCards, card.pri)"
|
||||
/>
|
||||
</div>
|
||||
<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>
|
||||
</VCardItem>
|
||||
<VCardItem>
|
||||
<VBtn
|
||||
type="submit"
|
||||
class="me-2"
|
||||
@click="saveCustomFilters('SubscribeFilterRules')"
|
||||
>
|
||||
保存
|
||||
</VBtn>
|
||||
<VBtn
|
||||
color="success"
|
||||
variant="tonal"
|
||||
@click="addFilterCard('SubscribeFilterRules')"
|
||||
<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>
|
||||
</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>
|
||||
</VCardItem>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VCard title="洗版优先级">
|
||||
<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>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<div class="grid gap-3 grid-filterrule-card">
|
||||
<FilterRuleCard
|
||||
v-for="(card, index) in bestVersionFilterCards"
|
||||
:key="index"
|
||||
:pri="card.pri"
|
||||
:maxpri="bestVersionFilterCards.length.toString()"
|
||||
:rules="card.rules"
|
||||
@changed="updateFilterCardValue2"
|
||||
@close="filterCardClose('BestVersionFilterRules', card.pri)"
|
||||
@leveldown="onLevelDown(bestVersionFilterCards, card.pri)"
|
||||
@levelup="onLevelUp(bestVersionFilterCards, card.pri)"
|
||||
/>
|
||||
</div>
|
||||
<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>
|
||||
<VCardItem>
|
||||
<VBtn
|
||||
type="submit"
|
||||
class="me-2"
|
||||
@click="saveCustomFilters('BestVersionFilterRules')"
|
||||
>
|
||||
保存
|
||||
</VBtn>
|
||||
<VBtn
|
||||
color="success"
|
||||
variant="tonal"
|
||||
@click="addFilterCard('BestVersionFilterRules')"
|
||||
<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>
|
||||
</VCardItem>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VCard title="默认过滤规则">
|
||||
<VCardSubtitle> 设置在订阅时默认使用的过滤规则。</VCardSubtitle>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>默认过滤规则</VCardTitle>
|
||||
<VCardSubtitle> 设置在订阅时默认使用的过滤规则。</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VForm>
|
||||
<VRow>
|
||||
@@ -634,33 +540,13 @@ onMounted(() => {
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
<VCardItem>
|
||||
<VBtn
|
||||
type="submit"
|
||||
@click="saveDefaultFilter"
|
||||
>
|
||||
保存
|
||||
</VBtn>
|
||||
</VCardItem>
|
||||
<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 v-model="importCodeDialog" width="60rem" scrollable>
|
||||
<ImportCodeDialog v-model="importCodeString" title="导入优先级规则" @close="importCodeDialog = false" />
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style lang='scss'>
|
||||
.grid-filterrule-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import { VRow } from 'vuetify/lib/components/index.mjs'
|
||||
import api from '@/api'
|
||||
import { requiredValidator } from '@/@validators'
|
||||
|
||||
// 选中的媒体服务器
|
||||
const selectedMediaServers = ref([])
|
||||
@@ -17,23 +16,6 @@ const downloaderTab = ref('qbittorrent')
|
||||
// 媒体服务器选中标签页
|
||||
const mediaserverTab = ref('emby')
|
||||
|
||||
// 媒体库设置项
|
||||
const mediaSettings = ref({
|
||||
SCRAP_METADATA: true,
|
||||
DOWNLOAD_PATH: '',
|
||||
DOWNLOAD_MOVIE_PATH: '',
|
||||
DOWNLOAD_TV_PATH: '',
|
||||
DOWNLOAD_ANIME_PATH: '',
|
||||
DOWNLOAD_CATEGORY: false,
|
||||
TRANSFER_TYPE: 'copy',
|
||||
OVERWRITE_MODE: 'size',
|
||||
LIBRARY_PATH: '',
|
||||
LIBRARY_MOVIE_NAME: '',
|
||||
LIBRARY_TV_NAME: '',
|
||||
LIBRARY_ANIME_NAME: '',
|
||||
LIBRARY_CATEGORY: false,
|
||||
})
|
||||
|
||||
// 下载器设置项
|
||||
const downloaderSettings = ref({
|
||||
DOWNLOADER_MONITOR: true,
|
||||
@@ -92,24 +74,6 @@ const MediaServers = [
|
||||
},
|
||||
]
|
||||
|
||||
// 转移方式字典
|
||||
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 syncIntervalItems = [
|
||||
{ title: '从不', value: 0 },
|
||||
@@ -123,72 +87,11 @@ const syncIntervalItems = [
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
// 加载媒体库设置
|
||||
async function loadMediaSettings() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/env')
|
||||
if (result.success) {
|
||||
const {
|
||||
SCRAP_METADATA,
|
||||
DOWNLOAD_PATH,
|
||||
DOWNLOAD_MOVIE_PATH,
|
||||
DOWNLOAD_TV_PATH,
|
||||
DOWNLOAD_ANIME_PATH,
|
||||
DOWNLOAD_CATEGORY,
|
||||
TRANSFER_TYPE,
|
||||
OVERWRITE_MODE,
|
||||
LIBRARY_PATH,
|
||||
LIBRARY_MOVIE_NAME,
|
||||
LIBRARY_TV_NAME,
|
||||
LIBRARY_ANIME_NAME,
|
||||
LIBRARY_CATEGORY,
|
||||
} = result.data
|
||||
mediaSettings.value = {
|
||||
SCRAP_METADATA,
|
||||
DOWNLOAD_PATH,
|
||||
DOWNLOAD_MOVIE_PATH,
|
||||
DOWNLOAD_TV_PATH,
|
||||
DOWNLOAD_ANIME_PATH,
|
||||
DOWNLOAD_CATEGORY,
|
||||
TRANSFER_TYPE,
|
||||
OVERWRITE_MODE,
|
||||
LIBRARY_PATH,
|
||||
LIBRARY_MOVIE_NAME,
|
||||
LIBRARY_TV_NAME,
|
||||
LIBRARY_ANIME_NAME,
|
||||
LIBRARY_CATEGORY,
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 调用API保存媒体设置
|
||||
async function saveMediaSetting() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post(
|
||||
'system/env',
|
||||
mediaSettings.value,
|
||||
)
|
||||
|
||||
if (result.success)
|
||||
$toast.success('保存媒体库设置成功')
|
||||
else
|
||||
$toast.error('保存媒体库设置失败!')
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 调用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(',')
|
||||
if (result1.success) selectedDownloaders.value = result1.data?.value?.split(',')
|
||||
|
||||
const result2: { [key: string]: any } = await api.get('system/env')
|
||||
if (result2.success) {
|
||||
@@ -219,8 +122,7 @@ async function loadDownloaderSetting() {
|
||||
TR_PASSWORD,
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
@@ -232,18 +134,15 @@ async function saveDownloaderSetting() {
|
||||
'system/setting/DOWNLOADER',
|
||||
selectedDownloaders.value.join(','),
|
||||
)
|
||||
const result2: { [key: string]: any } = await api.post(
|
||||
'system/env',
|
||||
downloaderSettings.value,
|
||||
)
|
||||
const result2: { [key: string]: any } = await api.post('system/env', downloaderSettings.value)
|
||||
|
||||
if (result1.success && result2.success) {
|
||||
$toast.success('保存下载器设置成功')
|
||||
reloadModule()
|
||||
} else {
|
||||
$toast.error('保存下载器设置失败!')
|
||||
}
|
||||
else { $toast.error('保存下载器设置失败!') }
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
@@ -252,8 +151,7 @@ async function saveDownloaderSetting() {
|
||||
async function loadMediaServerSetting() {
|
||||
try {
|
||||
const result1: { [key: string]: any } = await api.get('system/setting/MEDIASERVER')
|
||||
if (result1.success)
|
||||
selectedMediaServers.value = result1.data?.value?.split(',')
|
||||
if (result1.success) selectedMediaServers.value = result1.data?.value?.split(',')
|
||||
|
||||
const result2: { [key: string]: any } = await api.get('system/env')
|
||||
if (result2.success) {
|
||||
@@ -284,8 +182,7 @@ async function loadMediaServerSetting() {
|
||||
PLEX_TOKEN,
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
@@ -298,18 +195,15 @@ async function saveMediaServerSetting() {
|
||||
selectedMediaServers.value.join(','),
|
||||
)
|
||||
|
||||
const result2: { [key: string]: any } = await api.post(
|
||||
'system/env',
|
||||
mediaServerSettings.value,
|
||||
)
|
||||
const result2: { [key: string]: any } = await api.post('system/env', mediaServerSettings.value)
|
||||
|
||||
if (result1.success && result2.success) {
|
||||
$toast.success('保存媒体服务器设置成功')
|
||||
reloadModule()
|
||||
} else {
|
||||
$toast.error('保存媒体服务器设置失败!')
|
||||
}
|
||||
else { $toast.error('保存媒体服务器设置失败!') }
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
@@ -318,12 +212,9 @@ async function saveMediaServerSetting() {
|
||||
async function reloadModule() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/reload')
|
||||
if (result.success)
|
||||
$toast.success('重新加载模块成功')
|
||||
else
|
||||
$toast.error('重新加载模块失败!')
|
||||
}
|
||||
catch (error) {
|
||||
if (result.success) $toast.success('重新加载模块成功')
|
||||
else $toast.error('重新加载模块失败!')
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
@@ -332,15 +223,17 @@ async function reloadModule() {
|
||||
onMounted(() => {
|
||||
loadDownloaderSetting()
|
||||
loadMediaServerSetting()
|
||||
loadMediaSettings()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VCard title="下载器">
|
||||
<VCardSubtitle>只有选中的第1个下载器才会被默认使用。</VCardSubtitle>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>下载器</VCardTitle>
|
||||
<VCardSubtitle>只有选中的第1个下载器才会被默认使用。</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VForm>
|
||||
<VRow>
|
||||
@@ -373,22 +266,11 @@ onMounted(() => {
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol>
|
||||
<VTabs
|
||||
v-model="downloaderTab"
|
||||
stacked
|
||||
>
|
||||
<VTab value="qbittorrent">
|
||||
Qbittorrent
|
||||
</VTab>
|
||||
<VTab value="transmission">
|
||||
Transmission
|
||||
</VTab>
|
||||
<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"
|
||||
>
|
||||
<VWindow v-model="downloaderTab" class="mt-5 disable-tab-transition" :touch="false">
|
||||
<VWindowItem value="qbittorrent">
|
||||
<VForm>
|
||||
<VRow>
|
||||
@@ -478,12 +360,7 @@ onMounted(() => {
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<div class="d-flex flex-wrap gap-4 mt-4">
|
||||
<VBtn
|
||||
mtype="submit"
|
||||
@click="saveDownloaderSetting"
|
||||
>
|
||||
保存
|
||||
</VBtn>
|
||||
<VBtn mtype="submit" @click="saveDownloaderSetting"> 保存 </VBtn>
|
||||
</div>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
@@ -492,8 +369,11 @@ onMounted(() => {
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VCard title="媒体服务器">
|
||||
<VCardSubtitle>只有选中的媒体服务器才会被默认使用。</VCardSubtitle>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>媒体服务器</VCardTitle>
|
||||
<VCardSubtitle>只有选中的媒体服务器才会被默认使用。</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VForm>
|
||||
<VRow>
|
||||
@@ -526,25 +406,12 @@ onMounted(() => {
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol>
|
||||
<VTabs
|
||||
v-model="mediaserverTab"
|
||||
stacked
|
||||
>
|
||||
<VTab value="emby">
|
||||
Emby
|
||||
</VTab>
|
||||
<VTab value="jellyfin">
|
||||
Jellyfin
|
||||
</VTab>
|
||||
<VTab value="plex">
|
||||
Plex
|
||||
</vtab>
|
||||
<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"
|
||||
>
|
||||
<VWindow v-model="mediaserverTab" class="mt-5 disable-tab-transition" :touch="false">
|
||||
<VWindowItem value="emby">
|
||||
<VForm>
|
||||
<VRow>
|
||||
@@ -640,140 +507,7 @@ onMounted(() => {
|
||||
<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>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VCard title="媒体库">
|
||||
<VCardSubtitle>设置下载目录、媒体库目录以及整理方式。</VCardSubtitle>
|
||||
<VCardText>
|
||||
<VForm>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="mediaSettings.DOWNLOAD_PATH"
|
||||
label="下载目录"
|
||||
:rules="[requiredValidator]"
|
||||
hint="MoviePilot添加的下载任务的默认保存目录,必须设置"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="mediaSettings.DOWNLOAD_MOVIE_PATH"
|
||||
label="电影下载目录"
|
||||
hint="为电影设置单独的下载保存目录,不设置则使用下载目录"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="mediaSettings.DOWNLOAD_TV_PATH"
|
||||
label="电视剧下载目录"
|
||||
hint="为电视剧设置单独的下载保存目录,不设置则使用下载目录"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="mediaSettings.DOWNLOAD_ANIME_PATH"
|
||||
label="动漫下载目录"
|
||||
hint="为动漫设置单独的下载保存目录,不设置则使用下载目录"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="mediaSettings.DOWNLOAD_CATEGORY"
|
||||
label="下载目录自动分类"
|
||||
hint="开启后,下载任务保存目录将根据二级分类策略自动分类存放到下载目录的二级子目录中,二级分类策略需要编辑配置文件目录下的`category.yml`文件,插件市场有提供文件编辑插件"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VSelect
|
||||
v-model="mediaSettings.TRANSFER_TYPE"
|
||||
:items="transferTypeItems"
|
||||
label="整理方式"
|
||||
hint="硬链接需要确保下载目录和媒体库目录不跨盘、不跨共享目录、不分别映射;rclone需要手动在容器中完成配置,且配置名为:`MP`"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSelect
|
||||
v-model="mediaSettings.OVERWRITE_MODE"
|
||||
:items="overwriteModeItems"
|
||||
label="覆盖模式"
|
||||
hint="从不覆盖:不覆盖已存在的文件;按大小覆盖:大文件将覆盖小文件;总是覆盖:总是覆盖已存在的文件;仅保留最新版本:保留最新版本的文件,删除其它版本的文件"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="mediaSettings.SCRAP_METADATA"
|
||||
label="自动刮削媒体信息"
|
||||
hint="开启后,整理完成后将自动刮削媒体信息,如海报、简介等"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="mediaSettings.LIBRARY_PATH"
|
||||
label="媒体库目录"
|
||||
placeholder="多个目录使用,分隔"
|
||||
:rules="[requiredValidator]"
|
||||
hint="整理完成后的媒体文件存放的根目录,所有整理场景下未设定目的目录时都将整理到该目录下,必须设置"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="mediaSettings.LIBRARY_MOVIE_NAME"
|
||||
label="电影目录名称"
|
||||
placeholder="电影"
|
||||
hint="设置电影的存放一级目录名称,不设置则使用使用`电影`做为目录名称"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="mediaSettings.LIBRARY_TV_NAME"
|
||||
label="电视剧目录名称"
|
||||
placeholder="电视剧"
|
||||
hint="设置电视剧的存放一级目录名称,不设置则使用使用`电视剧`做为目录名称"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="mediaSettings.LIBRARY_ANIME_NAME"
|
||||
label="动漫目录名称"
|
||||
placeholder="动漫"
|
||||
hint="设置动漫的存放一级目录名称,不设置则使用使用`动漫`做为目录名称"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="mediaSettings.LIBRARY_CATEGORY"
|
||||
label="媒体库目录自动分类"
|
||||
hint="开启后,整理完成后的媒体文件将根据二级分类策略自动分类存放到媒体库一级目录的二级子目录中,二级分类策略需要编辑配置文件目录下的`category.yml`文件,插件市场有提供文件编辑插件"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<div class="d-flex flex-wrap gap-4 mt-4">
|
||||
<VBtn
|
||||
mtype="submit"
|
||||
@click="saveMediaSetting"
|
||||
>
|
||||
保存
|
||||
</VBtn>
|
||||
<VBtn mtype="submit" @click="saveMediaServerSetting"> 保存 </VBtn>
|
||||
</div>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
|
||||
@@ -20,13 +20,10 @@ const transferExcludeWords = ref('')
|
||||
// 查询已设置的识别词
|
||||
async function queryCustomIdentifiers() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get(
|
||||
'system/setting/CustomIdentifiers',
|
||||
)
|
||||
const result: { [key: string]: any } = await api.get('system/setting/CustomIdentifiers')
|
||||
|
||||
customIdentifiers.value = result.data?.value.join('\n')
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
@@ -34,13 +31,10 @@ async function queryCustomIdentifiers() {
|
||||
// 查询已设置的制作组
|
||||
async function queryCustomReleaseGroups() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get(
|
||||
'system/setting/CustomReleaseGroups',
|
||||
)
|
||||
const result: { [key: string]: any } = await api.get('system/setting/CustomReleaseGroups')
|
||||
|
||||
customReleaseGroups.value = result.data?.value.join('\n')
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
@@ -48,13 +42,10 @@ async function queryCustomReleaseGroups() {
|
||||
// 查询已设置的自定义占位符
|
||||
async function queryCustomization() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get(
|
||||
'system/setting/Customization',
|
||||
)
|
||||
const result: { [key: string]: any } = await api.get('system/setting/Customization')
|
||||
|
||||
customization.value = result.data?.value.join('\n')
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
@@ -62,13 +53,10 @@ async function queryCustomization() {
|
||||
// 查询已设置的屏蔽词
|
||||
async function queryTransferExcludeWords() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get(
|
||||
'system/setting/TransferExcludeWords',
|
||||
)
|
||||
const result: { [key: string]: any } = await api.get('system/setting/TransferExcludeWords')
|
||||
|
||||
transferExcludeWords.value = result.data?.value.join('\n')
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
@@ -82,12 +70,9 @@ async function saveCustomIdentifiers() {
|
||||
customIdentifiers.value.split('\n'),
|
||||
)
|
||||
|
||||
if (result.success)
|
||||
$toast.success('自定义识别词保存成功')
|
||||
else
|
||||
$toast.error('自定义识别词保存失败!')
|
||||
}
|
||||
catch (error) {
|
||||
if (result.success) $toast.success('自定义识别词保存成功')
|
||||
else $toast.error('自定义识别词保存失败!')
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
@@ -101,12 +86,9 @@ async function saveCustomReleaseGroups() {
|
||||
customReleaseGroups.value.split('\n'),
|
||||
)
|
||||
|
||||
if (result.success)
|
||||
$toast.success('自定义制作组/字幕组保存成功')
|
||||
else
|
||||
$toast.error('自定义制作组/字幕组保存失败!')
|
||||
}
|
||||
catch (error) {
|
||||
if (result.success) $toast.success('自定义制作组/字幕组保存成功')
|
||||
else $toast.error('自定义制作组/字幕组保存失败!')
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
@@ -120,12 +102,9 @@ async function saveCustomization() {
|
||||
customization.value.split('\n'),
|
||||
)
|
||||
|
||||
if (result.success)
|
||||
$toast.success('自定义占位符保存成功')
|
||||
else
|
||||
$toast.error('自定义占位符保存失败!')
|
||||
}
|
||||
catch (error) {
|
||||
if (result.success) $toast.success('自定义占位符保存成功')
|
||||
else $toast.error('自定义占位符保存失败!')
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
@@ -139,12 +118,9 @@ async function saveTransferExcludeWords() {
|
||||
transferExcludeWords.value.split('\n'),
|
||||
)
|
||||
|
||||
if (result.success)
|
||||
$toast.success('文件整理屏蔽词保存成功')
|
||||
else
|
||||
$toast.error('文件整理屏蔽词保存失败!')
|
||||
}
|
||||
catch (error) {
|
||||
if (result.success) $toast.success('文件整理屏蔽词保存成功')
|
||||
else $toast.error('文件整理屏蔽词保存失败!')
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
@@ -160,104 +136,94 @@ onMounted(() => {
|
||||
<template>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VCard title="自定义识别词">
|
||||
<VCardSubtitle> 添加规则对种子名或者文件名进行预处理以校正识别。 </VCardSubtitle>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>自定义识别词</VCardTitle>
|
||||
<VCardSubtitle> 添加规则对种子名或者文件名进行预处理以校正识别。 </VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VTextarea
|
||||
v-model="customIdentifiers"
|
||||
auto-grow
|
||||
placeholder="支持正则表达式,特殊字符需要\转义,一行为一组"
|
||||
hint="支持正则表达式,特殊字符需要\转义,一行为一组"
|
||||
/>
|
||||
</VCardItem>
|
||||
<VCardItem>
|
||||
<VAlert
|
||||
type="info"
|
||||
variant="tonal"
|
||||
title="支持的配置格式(注意空格):"
|
||||
>
|
||||
</VCardText>
|
||||
<VCardText>
|
||||
<VAlert type="info" variant="tonal" title="支持的配置格式(注意空格):">
|
||||
<span
|
||||
v-html="`
|
||||
v-html="
|
||||
`
|
||||
屏蔽词<br>
|
||||
被替换词 => 替换词<br>
|
||||
前定位词 <> 后定位词 >> 集偏移量(EP)<br>
|
||||
被替换词 => 替换词 && 前定位词 <> 后定位词 >> 集偏移量(EP)<br>
|
||||
其中替换词支持格式:{[tmdbid/doubanid=xxx;type=movie/tv;s=xxx;e=xxx]} 直接指定TMDBID/豆瓣ID识别,其中s、e为季数和集数(可选)<br>
|
||||
`"
|
||||
`
|
||||
"
|
||||
/>
|
||||
</VAlert>
|
||||
</VCardItem>
|
||||
<VCardItem>
|
||||
<VBtn
|
||||
type="submit"
|
||||
@click="saveCustomIdentifiers"
|
||||
>
|
||||
保存
|
||||
</VBtn>
|
||||
</VCardItem>
|
||||
</VCardText>
|
||||
<VCardText>
|
||||
<VBtn type="submit" @click="saveCustomIdentifiers"> 保存 </VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VCard title="自定义制作组/字幕组">
|
||||
<VCardSubtitle> 添加无法识别的制作组/字幕组。 </VCardSubtitle>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>自定义制作组/字幕组</VCardTitle>
|
||||
<VCardSubtitle> 添加无法识别的制作组/字幕组。 </VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VTextarea
|
||||
v-model="customReleaseGroups"
|
||||
auto-grow
|
||||
placeholder="支持正则表达式,特殊字符需要\转义,一行代表一个制作组/字幕组"
|
||||
hint="支持正则表达式,特殊字符需要\转义,一行代表一个制作组/字幕组"
|
||||
/>
|
||||
</VCardItem>
|
||||
<VCardItem>
|
||||
<VBtn
|
||||
type="submit"
|
||||
@click="saveCustomReleaseGroups"
|
||||
>
|
||||
保存
|
||||
</VBtn>
|
||||
</VCardItem>
|
||||
</VCardText>
|
||||
<VCardText>
|
||||
<VBtn type="submit" @click="saveCustomReleaseGroups"> 保存 </VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VCard title="自定义占位符">
|
||||
<VCardSubtitle> 添加自定义占位符识别正则,重命名格式中添加{customization}使用。 </VCardSubtitle>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>自定义占位符</VCardTitle>
|
||||
<VCardSubtitle> 添加自定义占位符识别正则,重命名格式中添加{customization}使用。 </VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VTextarea
|
||||
v-model="customization"
|
||||
auto-grow
|
||||
placeholder="多个匹配对象请换行分隔,支持正则表达式,特殊字符注意转义"
|
||||
hint="多个匹配对象请换行分隔,支持正则表达式,特殊字符注意转义"
|
||||
/>
|
||||
</VCardItem>
|
||||
<VCardItem>
|
||||
<VBtn
|
||||
type="submit"
|
||||
@click="saveCustomization"
|
||||
>
|
||||
保存
|
||||
</VBtn>
|
||||
</VCardItem>
|
||||
</VCardText>
|
||||
<VCardText>
|
||||
<VBtn type="submit" @click="saveCustomization"> 保存 </VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VCard title="文件整理屏蔽词">
|
||||
<VCardSubtitle> 目录名或文件名中包含屏蔽词时不进行整理。 </VCardSubtitle>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>文件整理屏蔽词</VCardTitle>
|
||||
<VCardSubtitle> 目录名或文件名中包含屏蔽词时不进行整理。 </VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VTextarea
|
||||
v-model="transferExcludeWords"
|
||||
auto-grow
|
||||
placeholder="支持正则表达式,特殊字符需要\转义,一行代表一个屏蔽词"
|
||||
hint="支持正则表达式,特殊字符需要\转义,一行代表一个屏蔽词"
|
||||
/>
|
||||
</VCardItem>
|
||||
<VCardItem>
|
||||
<VBtn
|
||||
type="submit"
|
||||
@click="saveTransferExcludeWords"
|
||||
>
|
||||
保存
|
||||
</VBtn>
|
||||
</VCardItem>
|
||||
</VCardText>
|
||||
<VCardText>
|
||||
<VBtn type="submit" @click="saveTransferExcludeWords"> 保存 </VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script lang="ts" setup>
|
||||
import draggable from 'vuedraggable'
|
||||
import api from '@/api'
|
||||
import type { Site } from '@/api/types'
|
||||
import SiteCard from '@/components/cards/SiteCard.vue'
|
||||
import NoDataFound from '@/components/NoDataFound.vue'
|
||||
import SiteAddEditDialog from '@/components/dialog/SiteAddEditDialog.vue'
|
||||
import { useDefer } from '@/@core/utils/dom'
|
||||
|
||||
// 数据列表
|
||||
const dataList = ref<Site[]>([])
|
||||
@@ -15,15 +15,25 @@ const isRefreshed = ref(false)
|
||||
// 新增站点对话框
|
||||
const siteAddDialog = ref(false)
|
||||
|
||||
// 延迟加载
|
||||
let defer = (_: number) => true
|
||||
|
||||
// 获取站点列表数据
|
||||
async function fetchData() {
|
||||
try {
|
||||
dataList.value = await api.get('site/')
|
||||
isRefreshed.value = true
|
||||
defer = useDefer(dataList.value.length)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存站点排序
|
||||
async function savaSitesPriority() {
|
||||
// 重新排序
|
||||
const priorities = dataList.value.map((site, index) => ({ id: site.id, pri: index + 1 }))
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post('site/priorities', priorities)
|
||||
if (result.success) {
|
||||
fetchData()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
@@ -35,10 +45,20 @@ onBeforeMount(fetchData)
|
||||
|
||||
<template>
|
||||
<LoadingBanner v-if="!isRefreshed" class="mt-12" />
|
||||
<div v-if="dataList.length > 0" class="grid gap-3 grid-site-card">
|
||||
<div v-for="(data, index) in dataList" :key="index">
|
||||
<SiteCard v-if="defer(index)" :key="data.id" :site="data" @remove="fetchData" @update="fetchData" />
|
||||
</div>
|
||||
<div>
|
||||
<draggable
|
||||
v-if="dataList.length > 0"
|
||||
v-model="dataList"
|
||||
@end="savaSitesPriority"
|
||||
handle=".cursor-move"
|
||||
item-key="id"
|
||||
tag="div"
|
||||
:component-data="{ 'class': 'grid gap-3 grid-site-card' }"
|
||||
>
|
||||
<template #item="{ element }">
|
||||
<SiteCard :site="element" @remove="fetchData" @update="fetchData" />
|
||||
</template>
|
||||
</draggable>
|
||||
</div>
|
||||
<NoDataFound
|
||||
v-if="dataList.length === 0 && isRefreshed"
|
||||
@@ -62,10 +82,3 @@ onBeforeMount(fetchData)
|
||||
@close="siteAddDialog = false"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.grid-site-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -30,8 +30,7 @@ async function fetchData() {
|
||||
try {
|
||||
dataList.value = await api.get('subscribe/')
|
||||
isRefreshed.value = true
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
@@ -51,29 +50,18 @@ function onRefresh() {
|
||||
|
||||
// 过滤数据,管理员用户显示全部,非管理员只显示自己的订阅
|
||||
const filteredDataList = computed(() => {
|
||||
// 从Vuex Store中获取用户信息
|
||||
// 从Vuex Store中获取用户信息
|
||||
const superUser = store.state.auth.superUser
|
||||
const userName = store.state.auth.userName
|
||||
if (superUser)
|
||||
return dataList.value.filter(data => data.type === props.type)
|
||||
else
|
||||
return dataList.value.filter(data => data.type === props.type && (data.username === userName))
|
||||
if (superUser) return dataList.value.filter(data => data.type === props.type)
|
||||
else return dataList.value.filter(data => data.type === props.type && data.username === userName)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LoadingBanner
|
||||
v-if="!isRefreshed"
|
||||
class="mt-12"
|
||||
/>
|
||||
<PullRefresh
|
||||
v-model="loading"
|
||||
@refresh="onRefresh"
|
||||
>
|
||||
<div
|
||||
v-if="filteredDataList.length > 0"
|
||||
class="grid gap-3 grid-subscribe-card p-1"
|
||||
>
|
||||
<LoadingBanner v-if="!isRefreshed" class="mt-12" />
|
||||
<PullRefresh v-model="loading" @refresh="onRefresh">
|
||||
<div v-if="filteredDataList.length > 0" class="mx-3 grid gap-4 grid-subscribe-card p-1">
|
||||
<SubscribeCard
|
||||
v-for="data in filteredDataList"
|
||||
:key="data.id"
|
||||
@@ -91,7 +79,8 @@ const filteredDataList = computed(() => {
|
||||
</PullRefresh>
|
||||
<!-- 底部操作按钮 -->
|
||||
<VFab
|
||||
icon="mdi-file-document-edit"
|
||||
v-if="store.state.auth.superUser"
|
||||
icon="mdi-clipboard-edit"
|
||||
location="bottom end"
|
||||
size="x-large"
|
||||
fixed
|
||||
@@ -100,6 +89,7 @@ const filteredDataList = computed(() => {
|
||||
@click="subscribeEditDialog = true"
|
||||
/>
|
||||
<VFab
|
||||
v-if="store.state.auth.superUser"
|
||||
icon="mdi-history"
|
||||
color="info"
|
||||
location="bottom end"
|
||||
@@ -120,18 +110,16 @@ const filteredDataList = computed(() => {
|
||||
@close="subscribeEditDialog = false"
|
||||
/>
|
||||
<!-- 历史记录弹窗 -->
|
||||
<SubscribeHistoryDialog
|
||||
<SubscribeHistoryDialog
|
||||
v-if="historyDialog"
|
||||
v-model="historyDialog"
|
||||
:type="props.type"
|
||||
@close="historyDialog = false"
|
||||
@save="() => {historyDialog = false; fetchData()}"
|
||||
@save="
|
||||
() => {
|
||||
historyDialog = false
|
||||
fetchData()
|
||||
}
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.grid-subscribe-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
131
src/views/subscribe/SubscribePopularView.vue
Normal file
131
src/views/subscribe/SubscribePopularView.vue
Normal file
@@ -0,0 +1,131 @@
|
||||
<script lang="ts" setup>
|
||||
import api from '@/api'
|
||||
import type { MediaInfo } from '@/api/types'
|
||||
import MediaCard from '@/components/cards/MediaCard.vue'
|
||||
import NoDataFound from '@/components/NoDataFound.vue'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
type: String,
|
||||
})
|
||||
|
||||
// 判断是否有滚动条
|
||||
function hasScroll() {
|
||||
return document.body.scrollHeight - (window.innerHeight || document.documentElement.clientHeight) > 2
|
||||
}
|
||||
|
||||
// API
|
||||
const apipath = 'subscribe/popular'
|
||||
|
||||
// 当前页码
|
||||
const page = ref(1)
|
||||
|
||||
// 是否加载中
|
||||
const loading = ref(false)
|
||||
|
||||
// 是否加载完成
|
||||
const isRefreshed = ref(false)
|
||||
|
||||
// 数据列表
|
||||
const dataList = ref<MediaInfo[]>([])
|
||||
const currData = ref<MediaInfo[]>([])
|
||||
|
||||
// 拼装参数
|
||||
function getParams() {
|
||||
let params = {
|
||||
stype: props.type,
|
||||
page: page.value,
|
||||
count: 30,
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
// 获取列表数据
|
||||
async function fetchData({ done }: { done: any }) {
|
||||
try {
|
||||
// 如果正在加载中,直接返回
|
||||
if (loading.value) {
|
||||
done('ok')
|
||||
return
|
||||
}
|
||||
|
||||
// 加载到满屏或者加载出错
|
||||
if (!hasScroll()) {
|
||||
// 加载多次
|
||||
while (!hasScroll()) {
|
||||
// 设置加载中
|
||||
loading.value = true
|
||||
// 请求API
|
||||
currData.value = await api.get(apipath, {
|
||||
params: getParams(),
|
||||
})
|
||||
// 取消加载中
|
||||
loading.value = false
|
||||
// 标计为已请求完成
|
||||
isRefreshed.value = true
|
||||
if (currData.value.length === 0) {
|
||||
// 如果没有数据,跳出
|
||||
done('empty')
|
||||
return
|
||||
}
|
||||
// 合并数据
|
||||
dataList.value = [...dataList.value, ...currData.value]
|
||||
// 页码+1
|
||||
page.value++
|
||||
// 返回加载成功
|
||||
done('ok')
|
||||
}
|
||||
} else {
|
||||
// 加载一次
|
||||
// 设置加载中
|
||||
loading.value = true
|
||||
// 请求API
|
||||
currData.value = await api.get(apipath, {
|
||||
params: getParams(),
|
||||
})
|
||||
// 标计为已请求完成
|
||||
isRefreshed.value = true
|
||||
if (currData.value.length === 0) {
|
||||
// 如果没有数据,跳出
|
||||
done('empty')
|
||||
} else {
|
||||
// 合并数据
|
||||
dataList.value = [...dataList.value, ...currData.value]
|
||||
// 页码+1
|
||||
page.value++
|
||||
// 返回加载成功
|
||||
done('ok')
|
||||
}
|
||||
}
|
||||
// 取消加载中
|
||||
loading.value = false
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
// 返回加载失败
|
||||
done('error')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LoadingBanner v-if="!isRefreshed" class="mt-12" />
|
||||
<VInfiniteScroll mode="intersect" side="end" :items="dataList" class="overflow-hidden" @load="fetchData">
|
||||
<template #loading />
|
||||
<template #empty />
|
||||
<div v-if="dataList.length > 0" class="grid gap-4 grid-media-card mx-3" tabindex="0">
|
||||
<div v-for="data in dataList" :key="data.tmdb_id || data.douban_id">
|
||||
<MediaCard :media="data" />
|
||||
<div class="mt-2 flex flex-row justify-center align-center text-subtitle-2">
|
||||
<VIcon icon="mdi-fire" color="error" />
|
||||
<span> {{ data.popularity }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<NoDataFound
|
||||
v-if="dataList.length === 0 && isRefreshed"
|
||||
error-code="404"
|
||||
error-title="没有数据"
|
||||
error-description="未获取到热门订阅数据,未开启数据分享或服务器无法连接。"
|
||||
/>
|
||||
</VInfiniteScroll>
|
||||
</template>
|
||||
@@ -2,24 +2,34 @@
|
||||
import api from '@/api'
|
||||
|
||||
// 定义所有的模块ID、名称列表
|
||||
const modules = ref([
|
||||
{ id: 'FileTransferModule', name: '媒体目录', state: '', errmsg: '', loading: false },
|
||||
{ id: 'IndexerModule', name: '站点索引', state: '', errmsg: '', loading: false },
|
||||
{ id: 'DoubanModule', name: '豆瓣', state: '', errmsg: '', loading: false },
|
||||
{ id: 'TheMovieDbModule', name: 'TheMovieDb', state: '', errmsg: '', loading: false },
|
||||
{ id: 'TheTvDbModule', name: 'TheTvDb', state: '', errmsg: '', loading: false },
|
||||
{ id: 'FanartModule', name: 'Fanart', state: '', errmsg: '', loading: false },
|
||||
{ id: 'EmbyModule', name: 'Emby', state: '', errmsg: '', loading: false },
|
||||
{ id: 'JellyfinModule', name: 'Jellyfin', state: '', errmsg: '', loading: false },
|
||||
{ id: 'PlexModule', name: 'Plex', state: '', errmsg: '', loading: false },
|
||||
{ id: 'WechatModule', name: '微信', state: '', errmsg: '', loading: false },
|
||||
{ id: 'TelegramModule', name: 'Telegram', state: '', errmsg: '', loading: false },
|
||||
{ id: 'SlackModule', name: 'Slack', state: '', errmsg: '', loading: false },
|
||||
{ id: 'SynologyChatModule', name: 'Synology Chat', state: '', errmsg: '', loading: false },
|
||||
{ id: 'VoceChatModule', name: 'VoceChat', state: '', errmsg: '', loading: false },
|
||||
{ id: 'QbittorrentModule', name: 'Qbittorrent', state: '', errmsg: '', loading: false },
|
||||
{ id: 'TransmissionModule', name: 'Transmission', state: '', errmsg: '', loading: false },
|
||||
])
|
||||
const modules = ref<
|
||||
{
|
||||
id: string
|
||||
name: string
|
||||
state: 'success' | 'error' | 'warning' | 'info' | undefined
|
||||
errmsg: string
|
||||
loading: boolean
|
||||
}[]
|
||||
>([])
|
||||
|
||||
// 调用API查询模块列表
|
||||
async function getModules() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/modulelist')
|
||||
if (result.success) {
|
||||
const moduleList = result.data?.modules
|
||||
if (moduleList) {
|
||||
moduleList.forEach((module: { id: string; name: string }) => {
|
||||
modules.value.push({ id: module.id, name: module.name, state: undefined, errmsg: '', loading: false })
|
||||
})
|
||||
// 逐个检查所有模块
|
||||
for (let i = 0; i < modules.value.length; i++) await moduleTest(i)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 调用API测试模块
|
||||
async function moduleTest(index: number) {
|
||||
@@ -33,7 +43,7 @@ async function moduleTest(index: number) {
|
||||
target.state = 'success'
|
||||
target.name = `${target.name} - 正常`
|
||||
} else if (result.message?.includes('模块未加载')) {
|
||||
target.state = ''
|
||||
target.state = undefined
|
||||
target.name = `${target.name} - 未启用`
|
||||
} else {
|
||||
target.state = 'error'
|
||||
@@ -44,11 +54,9 @@ async function moduleTest(index: number) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载
|
||||
onMounted(async () => {
|
||||
// 逐个检查所有模块
|
||||
for (let i = 0; i < modules.value.length; i++) await moduleTest(i)
|
||||
})
|
||||
onMounted(getModules)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
199
yarn.lock
199
yarn.lock
@@ -1057,18 +1057,6 @@
|
||||
"@babel/helper-validator-identifier" "^7.22.20"
|
||||
to-fast-properties "^2.0.0"
|
||||
|
||||
"@casl/ability@^6.2.0":
|
||||
version "6.7.1"
|
||||
resolved "https://registry.yarnpkg.com/@casl/ability/-/ability-6.7.1.tgz#89691083aafd1cfc4ae9519ffbcb0e7cb77ac201"
|
||||
integrity sha512-e+Vgrehd1/lzOSwSqKHtmJ6kmIuZbGBlM2LBS5IuYGGKmVHuhUuyh3XgTn1VIw9+TO4gqU+uptvxfIRBUEdJuw==
|
||||
dependencies:
|
||||
"@ucast/mongo2js" "^1.3.0"
|
||||
|
||||
"@casl/vue@^2.2.0":
|
||||
version "2.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@casl/vue/-/vue-2.2.2.tgz#d02907b867a52a7dc4e13383a16af8724ee1bd57"
|
||||
integrity sha512-xWy4i5+3+WuBgENVesPalRTKpSJZ2cEMXtbqjWjqj7FDvoeso7jT1pBVk9ujKlIRhgfVWGdCRb7XzeISi2VLcA==
|
||||
|
||||
"@csstools/css-parser-algorithms@^2.6.1":
|
||||
version "2.6.1"
|
||||
resolved "https://registry.yarnpkg.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.6.1.tgz#c45440d1efa2954006748a01697072dae5881bcd"
|
||||
@@ -1272,7 +1260,7 @@
|
||||
dependencies:
|
||||
"@floating-ui/utils" "^0.2.1"
|
||||
|
||||
"@floating-ui/dom@1.6.3", "@floating-ui/dom@^1.5.1":
|
||||
"@floating-ui/dom@^1.5.1":
|
||||
version "1.6.3"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.6.3.tgz#954e46c1dd3ad48e49db9ada7218b0985cee75ef"
|
||||
integrity sha512-RnDthu3mzPlQ31Ss/BTwQ1zjzIhr3lk1gZB1OC56h/1vEtaXkESrOqL5fQVMfXpwGtRwX+YsZBdyHtJMQnkArw==
|
||||
@@ -1401,15 +1389,7 @@
|
||||
source-map-js "^1.0.1"
|
||||
yaml-eslint-parser "^1.2.2"
|
||||
|
||||
"@intlify/core-base@9.11.0":
|
||||
version "9.11.0"
|
||||
resolved "https://registry.yarnpkg.com/@intlify/core-base/-/core-base-9.11.0.tgz#e7dfd1265672f9c75cb26fd270483f5e43663300"
|
||||
integrity sha512-cveOqAstjLZIiyatcP/HrzrQ87cZI8ScPQna3yvoM8zjcjcIRK1MRvmxUNlPdg0rTNJMZw7rixPVM58O5aHVPA==
|
||||
dependencies:
|
||||
"@intlify/message-compiler" "9.11.0"
|
||||
"@intlify/shared" "9.11.0"
|
||||
|
||||
"@intlify/message-compiler@9.11.0", "@intlify/message-compiler@^9.4.0":
|
||||
"@intlify/message-compiler@^9.4.0":
|
||||
version "9.11.0"
|
||||
resolved "https://registry.yarnpkg.com/@intlify/message-compiler/-/message-compiler-9.11.0.tgz#82e1f5c79a1182033cc18d0802299121a5441f52"
|
||||
integrity sha512-x31Gl7cscnoI4UUY1yaIy8e7vVMVW1VVlTXZz4SIHKqoSEUkfmgqK8NAx1e7RcoHEbICR7uyCbud0ZL1s4OGXQ==
|
||||
@@ -1492,11 +1472,6 @@
|
||||
"@jridgewell/resolve-uri" "^3.1.0"
|
||||
"@jridgewell/sourcemap-codec" "^1.4.14"
|
||||
|
||||
"@kurkle/color@^0.3.0":
|
||||
version "0.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@kurkle/color/-/color-0.3.2.tgz#5acd38242e8bde4f9986e7913c8fdf49d3aa199f"
|
||||
integrity sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==
|
||||
|
||||
"@lokesh.dhakar/quantize@^1.3.0":
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@lokesh.dhakar/quantize/-/quantize-1.3.0.tgz#04476889953aca94614fbc79e9a43adc7979179a"
|
||||
@@ -2015,34 +1990,6 @@
|
||||
"@typescript-eslint/types" "7.5.0"
|
||||
eslint-visitor-keys "^3.4.1"
|
||||
|
||||
"@ucast/core@^1.0.0", "@ucast/core@^1.4.1", "@ucast/core@^1.6.1":
|
||||
version "1.10.2"
|
||||
resolved "https://registry.yarnpkg.com/@ucast/core/-/core-1.10.2.tgz#30b6b893479823265368e528b61b042f752f2c92"
|
||||
integrity sha512-ons5CwXZ/51wrUPfoduC+cO7AS1/wRb0ybpQJ9RrssossDxVy4t49QxWoWgfBDvVKsz9VXzBk9z0wqTdZ+Cq8g==
|
||||
|
||||
"@ucast/js@^3.0.0":
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@ucast/js/-/js-3.0.4.tgz#c57ec2182505c9ad63a5b08ff5911f89ac605262"
|
||||
integrity sha512-TgG1aIaCMdcaEyckOZKQozn1hazE0w90SVdlpIJ/er8xVumE11gYAtSbw/LBeUnA4fFnFWTcw3t6reqseeH/4Q==
|
||||
dependencies:
|
||||
"@ucast/core" "^1.0.0"
|
||||
|
||||
"@ucast/mongo2js@^1.3.0":
|
||||
version "1.3.4"
|
||||
resolved "https://registry.yarnpkg.com/@ucast/mongo2js/-/mongo2js-1.3.4.tgz#579f9e5eb074cba54640d5c70c71c500580f3af3"
|
||||
integrity sha512-ahazOr1HtelA5AC1KZ9x0UwPMqqimvfmtSm/PRRSeKKeE5G2SCqTgwiNzO7i9jS8zA3dzXpKVPpXMkcYLnyItA==
|
||||
dependencies:
|
||||
"@ucast/core" "^1.6.1"
|
||||
"@ucast/js" "^3.0.0"
|
||||
"@ucast/mongo" "^2.4.0"
|
||||
|
||||
"@ucast/mongo@^2.4.0":
|
||||
version "2.4.3"
|
||||
resolved "https://registry.yarnpkg.com/@ucast/mongo/-/mongo-2.4.3.tgz#92b1dd7c0ab06a907f2ab1422aa3027518ccc05e"
|
||||
integrity sha512-XcI8LclrHWP83H+7H2anGCEeDq0n+12FU2mXCTz6/Tva9/9ddK/iacvvhCyW6cijAAOILmt0tWplRyRhVyZLsA==
|
||||
dependencies:
|
||||
"@ucast/core" "^1.4.1"
|
||||
|
||||
"@vitejs/plugin-vue-jsx@^3.0.0":
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue-jsx/-/plugin-vue-jsx-3.1.0.tgz#9953fd9456539e1f0f253bf0fcd1289e66c67cd1"
|
||||
@@ -2166,7 +2113,7 @@
|
||||
"@vue/compiler-dom" "3.4.21"
|
||||
"@vue/shared" "3.4.21"
|
||||
|
||||
"@vue/devtools-api@^6.0.0-beta.11", "@vue/devtools-api@^6.5.0", "@vue/devtools-api@^6.5.1":
|
||||
"@vue/devtools-api@^6.0.0-beta.11", "@vue/devtools-api@^6.5.1":
|
||||
version "6.6.1"
|
||||
resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.6.1.tgz#7c14346383751d9f6ad4bea0963245b30220ef83"
|
||||
integrity sha512-LgPscpE3Vs0x96PzSSB4IGVSZXZBZHpfxs+ZA1d+VEPwHdOXowy/Y2CsvCAIFrf+ssVU1pD1jidj505EpUnfbA==
|
||||
@@ -2228,7 +2175,7 @@
|
||||
dependencies:
|
||||
upath "^2.0.1"
|
||||
|
||||
"@vueuse/core@^10.0.0", "@vueuse/core@^10.1.2":
|
||||
"@vueuse/core@^10.1.2":
|
||||
version "10.9.0"
|
||||
resolved "https://registry.yarnpkg.com/@vueuse/core/-/core-10.9.0.tgz#7d779a95cf0189de176fee63cee4ba44b3c85d64"
|
||||
integrity sha512-/1vjTol8SXnx6xewDEKfS0Ra//ncg4Hb0DaZiwKf7drgfMsKFExQ+FnnENcN6efPen+1kIzhLQoGSy0eDUVOMg==
|
||||
@@ -2523,14 +2470,6 @@ aws4@^1.8.0:
|
||||
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.12.0.tgz#ce1c9d143389679e253b314241ea9aa5cec980d3"
|
||||
integrity sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==
|
||||
|
||||
axios-mock-adapter@^1.21.4:
|
||||
version "1.22.0"
|
||||
resolved "https://registry.yarnpkg.com/axios-mock-adapter/-/axios-mock-adapter-1.22.0.tgz#0f3e6be0fc9b55baab06f2d49c0b71157e7c053d"
|
||||
integrity sha512-dmI0KbkyAhntUR05YY96qg2H6gg0XMl2+qTW0xmYg6Up+BFBAJYRLROMXRdDEL06/Wqwa0TJThAYvFtSFdRCZw==
|
||||
dependencies:
|
||||
fast-deep-equal "^3.1.3"
|
||||
is-buffer "^2.0.5"
|
||||
|
||||
axios@1.6.8, axios@^1.6.8:
|
||||
version "1.6.8"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.8.tgz#66d294951f5d988a00e87a0ffb955316a619ea66"
|
||||
@@ -2736,13 +2675,6 @@ character-reference-invalid@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz#083329cda0eae272ab3dbbf37e9a382c13af1560"
|
||||
integrity sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==
|
||||
|
||||
chart.js@^4.1.2:
|
||||
version "4.4.2"
|
||||
resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-4.4.2.tgz#95962fa6430828ed325a480cc2d5f2b4e385ac31"
|
||||
integrity sha512-6GD7iKwFpP5kbSD4MeRRRlTnQvxfQREy36uEtm1hzHzcOqwWx0YEHuspuoNlslu+nciLIB7fjjsHkUv/FzFcOg==
|
||||
dependencies:
|
||||
"@kurkle/color" "^0.3.0"
|
||||
|
||||
cheerio-select@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/cheerio-select/-/cheerio-select-2.1.0.tgz#4d8673286b8126ca2a8e42740d5e3c4884ae21b4"
|
||||
@@ -2864,11 +2796,6 @@ commander@^7.2.0:
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
|
||||
integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
|
||||
|
||||
commander@^9.0.0:
|
||||
version "9.5.0"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-9.5.0.tgz#bc08d1eb5cedf7ccb797a96199d41c7bc3e60d30"
|
||||
integrity sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==
|
||||
|
||||
comment-parser@1.4.1:
|
||||
version "1.4.1"
|
||||
resolved "https://registry.yarnpkg.com/comment-parser/-/comment-parser-1.4.1.tgz#bdafead37961ac079be11eb7ec65c4d021eaf9cc"
|
||||
@@ -4099,11 +4026,6 @@ flat-cache@^4.0.0:
|
||||
flatted "^3.2.9"
|
||||
keyv "^4.5.4"
|
||||
|
||||
flatpickr@^4.6.13:
|
||||
version "4.6.13"
|
||||
resolved "https://registry.yarnpkg.com/flatpickr/-/flatpickr-4.6.13.tgz#8a029548187fd6e0d670908471e43abe9ad18d94"
|
||||
integrity sha512-97PMG/aywoYpB4IvbvUJi0RQi8vearvU0oov1WW3k0WZPBMrTQVqekSX5CjSG/M4Q3i6A/0FKXC7RyAoAUUSPw==
|
||||
|
||||
flatted@^3.2.9:
|
||||
version "3.3.1"
|
||||
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a"
|
||||
@@ -4324,17 +4246,6 @@ glob@^7.1.6:
|
||||
once "^1.3.0"
|
||||
path-is-absolute "^1.0.0"
|
||||
|
||||
glob@^8.0.3:
|
||||
version "8.1.0"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e"
|
||||
integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==
|
||||
dependencies:
|
||||
fs.realpath "^1.0.0"
|
||||
inflight "^1.0.4"
|
||||
inherits "2"
|
||||
minimatch "^5.0.1"
|
||||
once "^1.3.0"
|
||||
|
||||
global-modules@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780"
|
||||
@@ -4656,11 +4567,6 @@ is-buffer@^1.0.2:
|
||||
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
|
||||
integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
|
||||
|
||||
is-buffer@^2.0.5:
|
||||
version "2.0.5"
|
||||
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191"
|
||||
integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==
|
||||
|
||||
is-builtin-module@^3.2.1:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-3.2.1.tgz#f03271717d8654cfcaf07ab0463faa3571581169"
|
||||
@@ -5020,11 +4926,6 @@ jsprim@^1.2.2:
|
||||
json-schema "0.4.0"
|
||||
verror "1.10.0"
|
||||
|
||||
jwt-decode@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-4.0.0.tgz#2270352425fd413785b2faf11f6e755c5151bd4b"
|
||||
integrity sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==
|
||||
|
||||
keyv@^4.5.4:
|
||||
version "4.5.4"
|
||||
resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93"
|
||||
@@ -5828,13 +5729,6 @@ postcss-nested@^6.0.1:
|
||||
dependencies:
|
||||
postcss-selector-parser "^6.0.11"
|
||||
|
||||
postcss-purgecss@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/postcss-purgecss/-/postcss-purgecss-5.0.0.tgz#50c18314058e4b2a9febf8fa681fcdf53e43dca6"
|
||||
integrity sha512-qmvyvcy9ph0Fgsjq4z8ilm3+/B/EG52XKgITe5J7Txhk7EpfRo2hDl6dXDOlp8uEUO8TLnGkxfLPnEejT+/nAQ==
|
||||
dependencies:
|
||||
purgecss "^5.0.0"
|
||||
|
||||
postcss-resolve-nested-selector@^0.1.1:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.1.tgz#29ccbc7c37dedfac304e9fff0bf1596b3f6a0e4e"
|
||||
@@ -5855,7 +5749,7 @@ postcss-scss@^4.0.9:
|
||||
resolved "https://registry.yarnpkg.com/postcss-scss/-/postcss-scss-4.0.9.tgz#a03c773cd4c9623cb04ce142a52afcec74806685"
|
||||
integrity sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==
|
||||
|
||||
postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.0.15, postcss-selector-parser@^6.0.16, postcss-selector-parser@^6.0.7:
|
||||
postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.0.15, postcss-selector-parser@^6.0.16:
|
||||
version "6.0.16"
|
||||
resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz#3b88b9f5c5abd989ef4e2fc9ec8eedd34b20fb04"
|
||||
integrity sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==
|
||||
@@ -5873,7 +5767,7 @@ postcss-value-parser@^4.0.0, postcss-value-parser@^4.2.0:
|
||||
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
|
||||
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
|
||||
|
||||
postcss@8, postcss@^8.4.0, postcss@^8.4.23, postcss@^8.4.32, postcss@^8.4.35, postcss@^8.4.38, postcss@^8.4.4:
|
||||
postcss@8, postcss@^8.4.0, postcss@^8.4.23, postcss@^8.4.32, postcss@^8.4.35, postcss@^8.4.38:
|
||||
version "8.4.38"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e"
|
||||
integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==
|
||||
@@ -5902,11 +5796,6 @@ pretty-bytes@^6.1.1:
|
||||
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-6.1.1.tgz#38cd6bb46f47afbf667c202cfc754bffd2016a3b"
|
||||
integrity sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==
|
||||
|
||||
prismjs@^1.29.0:
|
||||
version "1.29.0"
|
||||
resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.29.0.tgz#f113555a8fa9b57c35e637bba27509dcf802dd12"
|
||||
integrity sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==
|
||||
|
||||
proxy-addr@~2.0.7:
|
||||
version "2.0.7"
|
||||
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025"
|
||||
@@ -5943,16 +5832,6 @@ punycode@^2.1.0, punycode@^2.1.1:
|
||||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
|
||||
integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
|
||||
|
||||
purgecss@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/purgecss/-/purgecss-5.0.0.tgz#08526ba3fef95e42c54503ca59d3f2ee8d6e5189"
|
||||
integrity sha512-RAnuxrGuVyLLTr8uMbKaxDRGWMgK5CCYDfRyUNNcaz5P3kGgD2b7ymQGYEyo2ST7Tl/ScwFgf5l3slKMxHSbrw==
|
||||
dependencies:
|
||||
commander "^9.0.0"
|
||||
glob "^8.0.3"
|
||||
postcss "^8.4.4"
|
||||
postcss-selector-parser "^6.0.7"
|
||||
|
||||
qrcode.vue@^3.4.1:
|
||||
version "3.4.1"
|
||||
resolved "https://registry.yarnpkg.com/qrcode.vue/-/qrcode.vue-3.4.1.tgz#dd8141da9c4ea07ee56b111cd13eadf123af822a"
|
||||
@@ -5975,11 +5854,6 @@ queue-microtask@^1.2.2:
|
||||
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
|
||||
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
|
||||
|
||||
ramda@>=0.28.0:
|
||||
version "0.30.0"
|
||||
resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.30.0.tgz#3cc4f0ddddfa6334dad2f371bd72c33237d92cd0"
|
||||
integrity sha512-13Y0iMhIQuAm/wNGBL/9HEqIfRGmNmjKnTPlKWfA9f7dnDkr8d45wQ+S7+ZLh/Pq9PdcGxkqKUEA7ySu1QSd9Q==
|
||||
|
||||
randombytes@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
|
||||
@@ -6165,11 +6039,6 @@ reusify@^1.0.4:
|
||||
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
|
||||
integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
|
||||
|
||||
roboto-fontface@^0.10.0:
|
||||
version "0.10.0"
|
||||
resolved "https://registry.yarnpkg.com/roboto-fontface/-/roboto-fontface-0.10.0.tgz#7eee40cfa18b1f7e4e605eaf1a2740afb6fd71b0"
|
||||
integrity sha512-OlwfYEgA2RdboZohpldlvJ1xngOins5d7ejqnIBWr9KaMxsnBqotpptRXTyfNRLnFpqzX6sTDt+X+a+6udnU8g==
|
||||
|
||||
rollup-plugin-terser@^7.0.0:
|
||||
version "7.0.2"
|
||||
resolved "https://registry.yarnpkg.com/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz#e8fbba4869981b2dc35ae7e8a502d5c6c04d324d"
|
||||
@@ -6218,13 +6087,6 @@ run-parallel@^1.1.9:
|
||||
dependencies:
|
||||
queue-microtask "^1.2.2"
|
||||
|
||||
rxjs@^7.8.0:
|
||||
version "7.8.1"
|
||||
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543"
|
||||
integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==
|
||||
dependencies:
|
||||
tslib "^2.1.0"
|
||||
|
||||
safe-array-concat@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.2.tgz#81d77ee0c4e8b863635227c721278dd524c20edb"
|
||||
@@ -6407,6 +6269,11 @@ slice-ansi@^4.0.0:
|
||||
astral-regex "^2.0.0"
|
||||
is-fullwidth-code-point "^3.0.0"
|
||||
|
||||
sortablejs@1.14.0:
|
||||
version "1.14.0"
|
||||
resolved "https://registry.yarnpkg.com/sortablejs/-/sortablejs-1.14.0.tgz#6d2e17ccbdb25f464734df621d4f35d4ab35b3d8"
|
||||
integrity sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==
|
||||
|
||||
"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.1, source-map-js@^1.0.2, source-map-js@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af"
|
||||
@@ -7008,7 +6875,7 @@ tslib@^1.8.1:
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
|
||||
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
||||
|
||||
tslib@^2.1.0, tslib@^2.3.1:
|
||||
tslib@^2.3.1:
|
||||
version "2.6.2"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
|
||||
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
|
||||
@@ -7377,11 +7244,6 @@ vite@^5.2.8:
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.3"
|
||||
|
||||
vue-chartjs@^5.2.0:
|
||||
version "5.3.0"
|
||||
resolved "https://registry.yarnpkg.com/vue-chartjs/-/vue-chartjs-5.3.0.tgz#59920a07d72f37a2375d495256e486b92813bf6e"
|
||||
integrity sha512-8XqX0JU8vFZ+WA2/knz4z3ThClduni2Nm0BMe2u0mXgTfd9pXrmJ07QBI+WAij5P/aPmPMX54HCE1seWL37ZdQ==
|
||||
|
||||
vue-demi@>=0.14.7:
|
||||
version "0.14.7"
|
||||
resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.14.7.tgz#8317536b3ef74c5b09f268f7782e70194567d8f2"
|
||||
@@ -7400,27 +7262,6 @@ vue-eslint-parser@^9.4.2:
|
||||
lodash "^4.17.21"
|
||||
semver "^7.3.6"
|
||||
|
||||
vue-flatpickr-component@11.0.5:
|
||||
version "11.0.5"
|
||||
resolved "https://registry.yarnpkg.com/vue-flatpickr-component/-/vue-flatpickr-component-11.0.5.tgz#a9300718a7556cec2ed09c8eabb6e0c3de5114ca"
|
||||
integrity sha512-Vfwg5uVU+sanKkkLzUGC5BUlWd5wlqAMq/UpQ6lI2BCZq0DDrXhOMX7hrevt8bEgglIq2QUv0K2Nl84Me/VnlA==
|
||||
dependencies:
|
||||
flatpickr "^4.6.13"
|
||||
|
||||
vue-i18n@^9.2.2:
|
||||
version "9.11.0"
|
||||
resolved "https://registry.yarnpkg.com/vue-i18n/-/vue-i18n-9.11.0.tgz#50e0c82fa3ab331a770b02af7ca3cdd45ae8eb88"
|
||||
integrity sha512-vU4gY6lu8Pdfs9BgKGiDAJmFDf88cceR47KcSB0VW4xJzUrXR/7qwqM7A8dQ2nedhoIDxoOm5Ro4pFd2KvJqbA==
|
||||
dependencies:
|
||||
"@intlify/core-base" "9.11.0"
|
||||
"@intlify/shared" "9.11.0"
|
||||
"@vue/devtools-api" "^6.5.0"
|
||||
|
||||
vue-prism-component@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/vue-prism-component/-/vue-prism-component-2.0.0.tgz#eec89c5fe1ea3d8b55b8721d823b29d8b73b2b6d"
|
||||
integrity sha512-1ofrL+GCZOv4HqtX5W3EgkhSAgadSeuD8FDTXbwhLy8kS+28RCR8t2S5VTeM9U/peAaXLBpSgRt3J25ao8KTeg==
|
||||
|
||||
vue-router@^4.2.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-4.3.0.tgz#d5913f27bf68a0a178ee798c3c88be471811a235"
|
||||
@@ -7457,15 +7298,6 @@ vue-tsc@^2.0.10:
|
||||
"@vue/language-core" "2.0.10"
|
||||
semver "^7.5.4"
|
||||
|
||||
vue-virtual-scroll-grid@^1.11.0:
|
||||
version "1.11.0"
|
||||
resolved "https://registry.yarnpkg.com/vue-virtual-scroll-grid/-/vue-virtual-scroll-grid-1.11.0.tgz#83daa6af439f0b66283356faa4d45156eda2ffaf"
|
||||
integrity sha512-f3hBqQgdbSVg8srhun/7nDkGBE/GOAXzyZH3fcgqO59y2iCfBoll2kUe9T42GAqXvslHzlTwNSPcwEyMsiKz3A==
|
||||
dependencies:
|
||||
"@vueuse/core" "^10.0.0"
|
||||
ramda ">=0.28.0"
|
||||
rxjs "^7.8.0"
|
||||
|
||||
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"
|
||||
@@ -7496,6 +7328,13 @@ vue@^3.3.2:
|
||||
"@vue/server-renderer" "3.4.21"
|
||||
"@vue/shared" "3.4.21"
|
||||
|
||||
vuedraggable@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/vuedraggable/-/vuedraggable-4.1.0.tgz#edece68adb8a4d9e06accff9dfc9040e66852270"
|
||||
integrity sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==
|
||||
dependencies:
|
||||
sortablejs "1.14.0"
|
||||
|
||||
vuetify-use-dialog@^0.6.11:
|
||||
version "0.6.11"
|
||||
resolved "https://registry.yarnpkg.com/vuetify-use-dialog/-/vuetify-use-dialog-0.6.11.tgz#8800cc56b234dae1dfa44a7f06a6bb1a33ad4b39"
|
||||
|
||||
Reference in New Issue
Block a user