mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-05-09 07:52:39 +08:00
Compare commits
159 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
adc718b751 | ||
|
|
df9981d0c9 | ||
|
|
f58b661b1b | ||
|
|
ec1926ba60 | ||
|
|
e853851933 | ||
|
|
3705ce3b90 | ||
|
|
7ad73ff251 | ||
|
|
6c23e8892a | ||
|
|
58efafac71 | ||
|
|
abf2364bf6 | ||
|
|
0650f35dbb | ||
|
|
cc593634d2 | ||
|
|
79a3b9de8a | ||
|
|
ceb46ec974 | ||
|
|
a7e2893a57 | ||
|
|
2efe8efde0 | ||
|
|
31047b0d44 | ||
|
|
7c2b724d10 | ||
|
|
ca5670f06b | ||
|
|
427e05871d | ||
|
|
bef56bdb56 | ||
|
|
d450d02e18 | ||
|
|
85a766cc7b | ||
|
|
a473f356c9 | ||
|
|
52b5fdf383 | ||
|
|
b886f02043 | ||
|
|
61963ea497 | ||
|
|
2f9b27ad9e | ||
|
|
9334109767 | ||
|
|
2bc52576d9 | ||
|
|
700d2c4a51 | ||
|
|
103bdb32c8 | ||
|
|
92b745e180 | ||
|
|
a2007083b8 | ||
|
|
36a5f7ff29 | ||
|
|
f727aea51d | ||
|
|
936ca24328 | ||
|
|
62f49b6087 | ||
|
|
e9ddbf9962 | ||
|
|
196cf522e6 | ||
|
|
3fce3bf4a7 | ||
|
|
1cfee25695 | ||
|
|
5711285a77 | ||
|
|
e6f537ca3a | ||
|
|
3b5220af57 | ||
|
|
fa6b4b1d2d | ||
|
|
7968e5374b | ||
|
|
64997ebe45 | ||
|
|
f8592b01e2 | ||
|
|
087474f514 | ||
|
|
1725088f05 | ||
|
|
ec1b756a3d | ||
|
|
76a06e0817 | ||
|
|
02fb608d7b | ||
|
|
e17fc2fc12 | ||
|
|
4f6c317652 | ||
|
|
46c198be26 | ||
|
|
8552203d43 | ||
|
|
139eaa7016 | ||
|
|
d81120ab8f | ||
|
|
6353d56beb | ||
|
|
aa05496b42 | ||
|
|
dc15e537d8 | ||
|
|
6fbd41f40a | ||
|
|
0181f614e1 | ||
|
|
fded7b0b28 | ||
|
|
7e637f835a | ||
|
|
deaaf1834d | ||
|
|
139c870f99 | ||
|
|
4cc2350bc6 | ||
|
|
8b31a118da | ||
|
|
cca26acb78 | ||
|
|
245edbd2f6 | ||
|
|
903d22c622 | ||
|
|
8b1805628e | ||
|
|
11c8c488da | ||
|
|
4dd4e0e148 | ||
|
|
21f352aa64 | ||
|
|
6c4beffdb7 | ||
|
|
43d3efa838 | ||
|
|
1c99839ab4 | ||
|
|
c9e05ce5b1 | ||
|
|
3fe7ed0e1d | ||
|
|
b3bff5c6f5 | ||
|
|
e357bac70f | ||
|
|
ad51d4e4f3 | ||
|
|
912d8ced93 | ||
|
|
8334999e98 | ||
|
|
5e23ea7809 | ||
|
|
b62d291aab | ||
|
|
a34dd8148f | ||
|
|
ba13e6ac35 | ||
|
|
8efa5f7a28 | ||
|
|
f0ef9565e2 | ||
|
|
78688ab63c | ||
|
|
e90b30bf63 | ||
|
|
5312b82ba7 | ||
|
|
bc705f2560 | ||
|
|
6477f43de1 | ||
|
|
bdc0fdd076 | ||
|
|
1f09e1ff93 | ||
|
|
4bcc89d9da | ||
|
|
8f93b49dde | ||
|
|
74eeae900e | ||
|
|
63424bb134 | ||
|
|
1c5e410881 | ||
|
|
f79cc41f3c | ||
|
|
49cccbe69e | ||
|
|
c4a02f7497 | ||
|
|
59e12c5e96 | ||
|
|
a347bdc412 | ||
|
|
3f3c1ecd02 | ||
|
|
d5d9c78c91 | ||
|
|
5b0d8d902b | ||
|
|
2978e46d02 | ||
|
|
54e0633d77 | ||
|
|
ab3db66195 | ||
|
|
17e19da3d8 | ||
|
|
f22aca0c5d | ||
|
|
c257e11ee3 | ||
|
|
8b23f0bb2e | ||
|
|
a82a89afd3 | ||
|
|
5c0d0d5a95 | ||
|
|
9dbd090482 | ||
|
|
e25583dff9 | ||
|
|
d997dc0394 | ||
|
|
6b6353ed41 | ||
|
|
e73d906564 | ||
|
|
7e3e850e21 | ||
|
|
56b2dc4ebf | ||
|
|
9444b0e518 | ||
|
|
bcb72118f5 | ||
|
|
c59be8d981 | ||
|
|
8466a40455 | ||
|
|
f435b4fc52 | ||
|
|
5686c6fe65 | ||
|
|
6810112eda | ||
|
|
11a2d07935 | ||
|
|
02cd2f1570 | ||
|
|
924c1d72ea | ||
|
|
5d9b2e1919 | ||
|
|
f7fa440f9a | ||
|
|
d4aaa46968 | ||
|
|
93ac5e1b3b | ||
|
|
c7a8c68e14 | ||
|
|
77afb4d736 | ||
|
|
141796ab24 | ||
|
|
30d733f55d | ||
|
|
6a39e65b6b | ||
|
|
c27013b7ad | ||
|
|
582ce496fa | ||
|
|
5b4dbb82d5 | ||
|
|
011a0d16ab | ||
|
|
ac5539194d | ||
|
|
6b7e1b3c4e | ||
|
|
30c3d00139 | ||
|
|
36d460cd74 | ||
|
|
11cb2eb0f8 | ||
|
|
4dce1c94a3 |
1
.github/workflows/build.yml
vendored
1
.github/workflows/build.yml
vendored
@@ -44,6 +44,7 @@ jobs:
|
||||
|
||||
- name: Delete Release
|
||||
uses: dev-drprasad/delete-tag-and-release@v1.1
|
||||
continue-on-error: true
|
||||
with:
|
||||
tag_name: ${{ env.frontend_version }}
|
||||
delete_release: true
|
||||
|
||||
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@@ -106,5 +106,8 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"vue3snippets.enable-compile-vue-file-on-did-save-code": false
|
||||
"vue3snippets.enable-compile-vue-file-on-did-save-code": false,
|
||||
"i18n-ally.localesPaths": [
|
||||
"src/locales"
|
||||
]
|
||||
}
|
||||
|
||||
1
components.d.ts
vendored
1
components.d.ts
vendored
@@ -8,6 +8,7 @@ export {}
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
ConfirmDialog: typeof import('./src/@core/components/ConfirmDialog.vue')['default']
|
||||
DialogCloseBtn: typeof import('./src/@core/components/DialogCloseBtn.vue')['default']
|
||||
ErrorHeader: typeof import('./src/@core/components/ErrorHeader.vue')['default']
|
||||
ExistIcon: typeof import('./src/@core/components/ExistIcon.vue')['default']
|
||||
|
||||
@@ -9,7 +9,7 @@ MoviePilot前端采用模块联邦(Module Federation)技术实现插件的动态
|
||||
|
||||
## 2. 技术要求
|
||||
|
||||
- Node.js 16+
|
||||
- Node.js 20+
|
||||
- Vue 3
|
||||
- Vite 4+
|
||||
- TypeScript 5+
|
||||
@@ -80,13 +80,6 @@ export default defineConfig({
|
||||
target: 'esnext', // 必须设置为esnext以支持顶层await
|
||||
minify: false, // 开发阶段建议关闭混淆
|
||||
cssCodeSplit: true, // 改为true以便能分离样式文件
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: {
|
||||
'vuetify-lib': ['vuetify'] // 将vuetify单独分离出来
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
@@ -271,7 +264,10 @@ const props = defineProps({
|
||||
yarn build
|
||||
```
|
||||
|
||||
将生成的dist文件夹上传到插件后端目录下(默认为`dist/assets`)
|
||||
- 将生成的dist文件夹上传到插件后端目录下(默认为`dist/assets`)
|
||||
|
||||
**注意: `__federation_shared_vuetify` 目录以及 `index-`、`date-`、`runtime-` 开头的文件不需要上传**,只需要上传以下命名格式文件:`__federation_*`、`_plugin-vue_export-helper-*`、`remoteEntry.js`
|
||||
|
||||
|
||||
- 在插件的后端python代码中,实现以下方法来集成远程组件:
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "moviepilot",
|
||||
"version": "2.4.5",
|
||||
"version": "2.5.3",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"bin": "dist/service.js",
|
||||
@@ -62,7 +62,6 @@
|
||||
"vue3-perfect-scrollbar": "^2.0.0",
|
||||
"vuedraggable": "^4.1.0",
|
||||
"vuetify": "3.7.3",
|
||||
"vuetify-use-dialog": "^0.6.11",
|
||||
"webfontloader": "^1.6.28"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -113,4 +112,4 @@
|
||||
"workbox-window": "^7.3.0"
|
||||
},
|
||||
"packageManager": "yarn@1.22.18"
|
||||
}
|
||||
}
|
||||
86
src/@core/components/ConfirmDialog.vue
Normal file
86
src/@core/components/ConfirmDialog.vue
Normal file
@@ -0,0 +1,86 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
type?: 'info' | 'warn' | 'error'
|
||||
title?: string
|
||||
content?: string
|
||||
confirmText?: string
|
||||
cancelText?: string
|
||||
width?: string | number
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
type: 'info',
|
||||
title: '',
|
||||
content: '',
|
||||
confirmText: '',
|
||||
cancelText: '',
|
||||
width: '28rem',
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'confirm'): void
|
||||
(e: 'cancel'): void
|
||||
}>()
|
||||
|
||||
// 对话框类型对应的图标和颜色
|
||||
const typeConfig = {
|
||||
info: {
|
||||
icon: 'mdi-information',
|
||||
color: 'info',
|
||||
},
|
||||
warn: {
|
||||
icon: 'mdi-alert',
|
||||
color: 'warning',
|
||||
},
|
||||
error: {
|
||||
icon: 'mdi-alert-circle',
|
||||
color: 'error',
|
||||
},
|
||||
}
|
||||
|
||||
// 获取当前类型的配置
|
||||
const currentType = computed(() => typeConfig[props.type])
|
||||
|
||||
// 确认按钮点击
|
||||
function handleConfirm() {
|
||||
emit('confirm')
|
||||
emit('update:modelValue', false)
|
||||
}
|
||||
|
||||
// 取消按钮点击
|
||||
function handleCancel() {
|
||||
emit('cancel')
|
||||
emit('update:modelValue', false)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VDialog :model-value="modelValue" @update:model-value="emit('update:modelValue', $event)" :max-width="width">
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<div class="d-flex align-center justify-start mt-3">
|
||||
<VAvatar :color="currentType.color" variant="text" size="x-large">
|
||||
<VIcon size="x-large" :icon="currentType.icon" />
|
||||
</VAvatar>
|
||||
<div class="mx-3">
|
||||
<p class="font-weight-bold text-xl text-high-emphasis">{{ title }}</p>
|
||||
<p>{{ content }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</VCardItem>
|
||||
<VCardActions class="mx-auto">
|
||||
<VBtn variant="tonal" color="secondary" class="px-5" @click="handleCancel">
|
||||
{{ cancelText }}
|
||||
</VBtn>
|
||||
<VBtn variant="elevated" :color="currentType.color" @click="handleConfirm" class="px-5">
|
||||
{{ confirmText }}
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
<VDialogCloseBtn @click="handleCancel" />
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
@@ -1,15 +1,88 @@
|
||||
<script lang="ts" setup>
|
||||
// 定义输入参数
|
||||
const props = defineProps({
|
||||
progress: Number,
|
||||
text: String,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full text-center text-gray-500 text-sm flex flex-col items-center">
|
||||
<VProgressCircular v-if="!props.text || !props.progress" class="mb-3" size="64" indeterminate color="primary" />
|
||||
<VProgressCircular v-if="props.progress" class="mb-3" color="primary" :model-value="props.progress" size="64" />
|
||||
<span>{{ props.text }}</span>
|
||||
<div class="w-full text-center text-gray-500 text-sm flex flex-col items-center my-5">
|
||||
<div class="initial-loading-container">
|
||||
<div class="initial-loading-content">
|
||||
<div class="wave-loader">
|
||||
<div class="wave-dot"></div>
|
||||
<div class="wave-dot"></div>
|
||||
<div class="wave-dot"></div>
|
||||
<div class="wave-dot"></div>
|
||||
</div>
|
||||
<div class="initial-loading-text" v-if="props.text">{{ props.text }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 初始的加载状态 */
|
||||
.initial-loading-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-block-size: 20vh;
|
||||
}
|
||||
|
||||
.initial-loading-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.wave-loader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
block-size: 40px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.wave-dot {
|
||||
border-radius: 50%;
|
||||
animation: wave 1.5s ease-in-out infinite;
|
||||
background-color: rgb(var(--v-theme-primary));
|
||||
block-size: 8px;
|
||||
inline-size: 8px;
|
||||
}
|
||||
|
||||
.wave-dot:nth-child(1) {
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.wave-dot:nth-child(2) {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
.wave-dot:nth-child(3) {
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
|
||||
.wave-dot:nth-child(4) {
|
||||
animation-delay: 0.6s;
|
||||
}
|
||||
|
||||
@keyframes wave {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateY(-15px);
|
||||
}
|
||||
}
|
||||
|
||||
.initial-loading-text {
|
||||
color: rgb(var(--v-theme-primary));
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -118,11 +118,6 @@
|
||||
opacity: var(--v-disabled-opacity);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Vertical nav link
|
||||
.nav-link {
|
||||
@extend %nav-link;
|
||||
|
||||
> .router-link-exact-active {
|
||||
@extend %nav-link-active;
|
||||
|
||||
@@ -22,7 +22,7 @@ $header: ".layout-navbar";
|
||||
// If navbar is contained => Add border radius to header
|
||||
@if variables.$layout-vertical-nav-navbar-is-contained {
|
||||
#{$header} {
|
||||
// border-radius: 0 0 variables.$default-layout-with-vertical-nav-navbar-footer-roundness variables.$default-layout-with-vertical-nav-navbar-footer-roundness;
|
||||
border-radius: 0 0 variables.$default-layout-with-vertical-nav-navbar-footer-roundness variables.$default-layout-with-vertical-nav-navbar-footer-roundness;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ $header: ".layout-navbar";
|
||||
/* ℹ️ This html selector with not selector is required when:
|
||||
dialog is opened and window don't have any scroll. This removes window-scrolled class from layout and out style broke
|
||||
*/
|
||||
html.v-overlay-scroll-blocked:not([style*="--v-body-scroll-y: 0px;"]) .layout-navbar-fixed,
|
||||
html.v-overlay-scroll-blocked .layout-navbar-fixed,
|
||||
&.window-scrolled.layout-navbar-fixed {
|
||||
|
||||
#{$header} {
|
||||
@@ -63,7 +63,7 @@ $header: ".layout-navbar";
|
||||
|
||||
#{$header} {
|
||||
@if variables.$layout-vertical-nav-navbar-is-contained {
|
||||
// border-radius: variables.$default-layout-with-vertical-nav-navbar-footer-roundness;
|
||||
border-radius: variables.$default-layout-with-vertical-nav-navbar-footer-roundness;
|
||||
}
|
||||
|
||||
background-color: rgb(var(--v-theme-surface));
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
position: relative;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
|
||||
|
||||
.v-theme--light & {
|
||||
backdrop-filter: blur(16px);
|
||||
background: rgba(var(--v-theme-surface), 0.9);
|
||||
box-shadow: 0 0 8px 0 rgba(var(--v-theme-on-surface), 0.1);
|
||||
}
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
@@ -11,29 +18,17 @@
|
||||
inset-block-start: 0;
|
||||
inset-inline: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.2s ease-in-out, background 0.2s ease-in-out;
|
||||
transition: all 0.2s ease-in-out;
|
||||
|
||||
// PC端样式 (默认)
|
||||
.v-theme--light & {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(var(--v-theme-surface), 0.9) 0%,
|
||||
rgba(var(--v-theme-surface), 0.7) 20%,
|
||||
rgba(var(--v-theme-surface), 0.5) 40%,
|
||||
rgba(var(--v-theme-surface), 0.3) 60%,
|
||||
rgba(var(--v-theme-surface), 0.1) 80%,
|
||||
rgba(var(--v-theme-surface), 0.0) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.v-theme--dark & {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(var(--v-theme-background), 0.8) 0%,
|
||||
rgba(var(--v-theme-background), 0.6) 20%,
|
||||
rgba(var(--v-theme-background), 0.4) 40%,
|
||||
rgba(var(--v-theme-background), 0.25) 60%,
|
||||
rgba(var(--v-theme-background), 0.1) 80%,
|
||||
rgba(var(--v-theme-background), 1) 0%,
|
||||
rgba(var(--v-theme-background), 0.8) 20%,
|
||||
rgba(var(--v-theme-background), 0.6) 40%,
|
||||
rgba(var(--v-theme-background), 0.4) 60%,
|
||||
rgba(var(--v-theme-background), 0.2) 80%,
|
||||
rgba(var(--v-theme-background), 0.0) 100%
|
||||
);
|
||||
}
|
||||
@@ -41,11 +36,11 @@
|
||||
.v-theme--purple & {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(var(--v-theme-background), 0.8) 0%,
|
||||
rgba(var(--v-theme-background), 0.6) 20%,
|
||||
rgba(var(--v-theme-background), 0.4) 40%,
|
||||
rgba(var(--v-theme-background), 0.25) 60%,
|
||||
rgba(var(--v-theme-background), 0.1) 80%,
|
||||
rgba(var(--v-theme-background), 1) 0%,
|
||||
rgba(var(--v-theme-background), 0.8) 20%,
|
||||
rgba(var(--v-theme-background), 0.6) 40%,
|
||||
rgba(var(--v-theme-background), 0.4) 60%,
|
||||
rgba(var(--v-theme-background), 0.2) 80%,
|
||||
rgba(var(--v-theme-background), 0.0) 100%
|
||||
);
|
||||
}
|
||||
@@ -53,68 +48,25 @@
|
||||
.v-theme--transparent & {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(11, 11, 11, 60%) 0%,
|
||||
rgba(11, 11, 11, 50%) 20%,
|
||||
rgba(11, 11, 11, 40%) 40%,
|
||||
rgba(11, 11, 11, 25%) 60%,
|
||||
rgba(11, 11, 11, 10%) 80%,
|
||||
rgba(11, 11, 11, 0%) 100%
|
||||
rgba(var(--v-theme-background), 0.5) 0%,
|
||||
rgba(var(--v-theme-background), 0.4) 20%,
|
||||
rgba(var(--v-theme-background), 0.3) 40%,
|
||||
rgba(var(--v-theme-background), 0.2) 60%,
|
||||
rgba(var(--v-theme-background), 0.1) 80%,
|
||||
rgba(var(--v-theme-background), 0.0) 100%
|
||||
);
|
||||
|
||||
@media (width <= 640px) {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(var(--v-theme-background), 0.9) 0%,
|
||||
rgba(var(--v-theme-background), 0.7) 20%,
|
||||
rgba(var(--v-theme-background), 0.5) 40%,
|
||||
rgba(var(--v-theme-background), 0.3) 60%,
|
||||
rgba(var(--v-theme-background), 0.1) 80%,
|
||||
rgba(var(--v-theme-background), 0.0) 100%
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 移动端样式
|
||||
@media (pointer: coarse) {
|
||||
%blurry-bg {
|
||||
&::before {
|
||||
.v-theme--light & {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(var(--v-theme-surface), 1) 0%,
|
||||
rgba(var(--v-theme-surface), 0.9) 20%,
|
||||
rgba(var(--v-theme-surface), 0.7) 40%,
|
||||
rgba(var(--v-theme-surface), 0.5) 60%,
|
||||
rgba(var(--v-theme-surface), 0.2) 80%,
|
||||
rgba(var(--v-theme-surface), 0.0) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.v-theme--dark & {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(var(--v-theme-background), 1) 0%,
|
||||
rgba(var(--v-theme-background), 0.85) 20%,
|
||||
rgba(var(--v-theme-background), 0.7) 40%,
|
||||
rgba(var(--v-theme-background), 0.5) 60%,
|
||||
rgba(var(--v-theme-background), 0.3) 80%,
|
||||
rgba(var(--v-theme-background), 0.0) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.v-theme--purple & {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(var(--v-theme-background), 1) 0%,
|
||||
rgba(var(--v-theme-background), 0.85) 20%,
|
||||
rgba(var(--v-theme-background), 0.7) 40%,
|
||||
rgba(var(--v-theme-background), 0.5) 60%,
|
||||
rgba(var(--v-theme-background), 0.3) 80%,
|
||||
rgba(var(--v-theme-background), 0.0) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.v-theme--transparent & {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(11, 11, 11, 90%) 0%,
|
||||
rgba(11, 11, 11, 80%) 20%,
|
||||
rgba(11, 11, 11, 60%) 40%,
|
||||
rgba(11, 11, 11, 40%) 60%,
|
||||
rgba(11, 11, 11, 15%) 80%,
|
||||
rgba(11, 11, 11, 0%) 100%
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,209 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { Transition } from 'vue'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import VerticalNav from '@layouts/components/VerticalNav.vue'
|
||||
|
||||
export default defineComponent({
|
||||
setup(props, { slots }) {
|
||||
const isOverlayNavActive = ref(false)
|
||||
const isLayoutOverlayVisible = ref(false)
|
||||
const toggleIsOverlayNavActive = useToggle(isOverlayNavActive)
|
||||
|
||||
const route = useRoute()
|
||||
const { mdAndDown } = useDisplay()
|
||||
|
||||
// ℹ️ This is alternative to below two commented watcher
|
||||
// We want to show overlay if overlay nav is visible and want to hide overlay if overlay is hidden and vice versa.
|
||||
syncRef(isOverlayNavActive, isLayoutOverlayVisible)
|
||||
|
||||
const scrollDistance = ref(window.scrollY)
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('scroll', () => {
|
||||
scrollDistance.value = window.scrollY
|
||||
})
|
||||
})
|
||||
|
||||
return () => {
|
||||
// 👉 Vertical nav
|
||||
const verticalNav = h(
|
||||
VerticalNav,
|
||||
{ isOverlayNavActive: isOverlayNavActive.value, toggleIsOverlayNavActive },
|
||||
{
|
||||
'nav-header': () => slots['vertical-nav-header']?.(),
|
||||
'before-nav-items': () => slots['before-vertical-nav-items']?.(),
|
||||
'default': () => slots['vertical-nav-content']?.(),
|
||||
'after-nav-items': () => slots['after-vertical-nav-items']?.(),
|
||||
},
|
||||
)
|
||||
|
||||
// 👉 Navbar
|
||||
const navbar = h('header', { class: ['layout-navbar navbar-blur'] }, [
|
||||
h(
|
||||
'div',
|
||||
{ class: 'navbar-content-container' },
|
||||
slots.navbar?.({
|
||||
toggleVerticalOverlayNavActive: toggleIsOverlayNavActive,
|
||||
}),
|
||||
),
|
||||
])
|
||||
|
||||
const main = h(
|
||||
'main',
|
||||
{ class: 'layout-page-content' },
|
||||
h(Transition, { name: 'fade-slide', mode: 'out-in', appear: true }, () =>
|
||||
h('section', { class: 'page-content-container' }, slots.default?.()),
|
||||
),
|
||||
)
|
||||
|
||||
// 👉 根据路由 meta 决定 footer 高度
|
||||
const shouldShowFooter = !route.meta.hideFooter
|
||||
|
||||
// 👉 Footer
|
||||
const footer = h('footer', { class: 'layout-footer' }, [
|
||||
h(
|
||||
'div',
|
||||
{
|
||||
class: ['footer-content-container', !shouldShowFooter && 'footer-content-container-noheight'],
|
||||
},
|
||||
slots.footer?.(),
|
||||
),
|
||||
])
|
||||
|
||||
// 👉 Overlay
|
||||
const layoutOverlay = h('div', {
|
||||
class: ['layout-overlay', 'touch-none', { visible: isLayoutOverlayVisible.value }],
|
||||
onClick: () => {
|
||||
isLayoutOverlayVisible.value = !isLayoutOverlayVisible.value
|
||||
},
|
||||
})
|
||||
|
||||
return h(
|
||||
'div',
|
||||
{
|
||||
class: [
|
||||
'layout-wrapper layout-nav-type-vertical layout-navbar-static layout-footer-static layout-content-width-fluid',
|
||||
'layout-navbar-fixed',
|
||||
mdAndDown.value && 'layout-overlay-nav',
|
||||
route.meta.layoutWrapperClasses,
|
||||
scrollDistance.value && 'window-scrolled',
|
||||
],
|
||||
},
|
||||
[verticalNav, h('div', { class: 'layout-content-wrapper' }, [navbar, main, footer]), layoutOverlay],
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@use '@configured-variables' as variables;
|
||||
@use '@layouts/styles/placeholders';
|
||||
@use '@layouts/styles/mixins';
|
||||
|
||||
.layout-wrapper.layout-nav-type-vertical {
|
||||
// TODO(v2): Check why we need height in vertical nav & min-height in horizontal nav
|
||||
block-size: 100%;
|
||||
|
||||
.layout-content-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
min-block-size: calc(var(--vh, 1vh) * 100);
|
||||
transition: padding-inline-start 0.2s ease-in-out;
|
||||
will-change: padding-inline-start;
|
||||
}
|
||||
|
||||
.layout-navbar {
|
||||
position: fixed;
|
||||
width: calc(100vw - variables.$layout-vertical-nav-width - 1rem);
|
||||
z-index: variables.$layout-vertical-nav-layout-navbar-z-index;
|
||||
inset-block-start: 0;
|
||||
|
||||
.navbar-content-container {
|
||||
block-size: calc(env(safe-area-inset-top) + variables.$layout-vertical-nav-navbar-height);
|
||||
}
|
||||
|
||||
@at-root {
|
||||
.layout-wrapper.layout-nav-type-vertical {
|
||||
.layout-navbar {
|
||||
@if variables.$layout-vertical-nav-navbar-is-contained {
|
||||
@include mixins.boxed-content;
|
||||
} @else {
|
||||
.navbar-content-container {
|
||||
// @include mixins.boxed-content;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.layout-navbar-fixed .layout-navbar {
|
||||
@extend %layout-navbar-fixed;
|
||||
}
|
||||
|
||||
&.layout-navbar-hidden .layout-navbar {
|
||||
@extend %layout-navbar-hidden;
|
||||
}
|
||||
|
||||
// 👉 Footer
|
||||
.layout-footer {
|
||||
@include mixins.boxed-content;
|
||||
}
|
||||
|
||||
// 👉 Layout overlay
|
||||
.layout-overlay {
|
||||
position: fixed;
|
||||
z-index: variables.$layout-overlay-z-index;
|
||||
background-color: rgb(0 0 0 / 60%);
|
||||
cursor: pointer;
|
||||
inset: 0;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.25s ease-in-out;
|
||||
will-change: transform;
|
||||
|
||||
&.visible {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.layout-overlay-nav) .layout-content-wrapper {
|
||||
padding-inline-start: variables.$layout-vertical-nav-width;
|
||||
}
|
||||
|
||||
// Adjust right column pl when vertical nav is collapsed
|
||||
&.layout-vertical-nav-collapsed .layout-content-wrapper {
|
||||
padding-inline-start: variables.$layout-vertical-nav-collapsed-width;
|
||||
}
|
||||
|
||||
// 👉 Content height fixed
|
||||
&.layout-content-height-fixed {
|
||||
.layout-content-wrapper {
|
||||
max-block-size: calc(var(--vh) * 100);
|
||||
}
|
||||
|
||||
.layout-page-content {
|
||||
// display: flex;
|
||||
overflow: hidden;
|
||||
|
||||
.page-content-container {
|
||||
inline-size: 100%;
|
||||
|
||||
> :first-child {
|
||||
max-block-size: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.layout-wrapper.layout-nav-type-vertical.layout-overlay-nav {
|
||||
.layout-navbar {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
109
src/App.vue
109
src/App.vue
@@ -4,13 +4,9 @@ import { checkPrefersColorSchemeIsDark } from '@/@core/utils'
|
||||
import { ensureRenderComplete, removeEl } from './@core/utils/dom'
|
||||
import api from '@/api'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { getBrowserLocale, setI18nLanguage } from './plugins/i18n'
|
||||
import { SupportedLocale } from '@/types/i18n'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
|
||||
// 生效主题
|
||||
const { global: globalTheme } = useTheme()
|
||||
let themeValue = localStorage.getItem('theme') || 'light'
|
||||
@@ -21,9 +17,6 @@ globalTheme.name.value = themeValue === 'auto' ? autoTheme : themeValue
|
||||
const localeValue = getBrowserLocale()
|
||||
setI18nLanguage(localeValue as SupportedLocale)
|
||||
|
||||
// 从 provide 中获取全局设置
|
||||
const globalSettings: any = inject('globalSettings')
|
||||
|
||||
// 显示状态
|
||||
const show = ref(false)
|
||||
|
||||
@@ -31,6 +24,9 @@ const show = ref(false)
|
||||
const authStore = useAuthStore()
|
||||
const isLogin = computed(() => authStore.token)
|
||||
|
||||
// 生成背景图片key
|
||||
const loginStateKey = computed(() => (isLogin.value ? 'logged-in' : 'logged-out'))
|
||||
|
||||
// 背景图片
|
||||
const backgroundImages = ref<string[]>([])
|
||||
const activeImageIndex = ref(0)
|
||||
@@ -78,6 +74,7 @@ function updateHtmlThemeAttribute(themeName: string) {
|
||||
async function fetchBackgroundImages() {
|
||||
try {
|
||||
backgroundImages.value = await api.get(`/login/wallpapers`)
|
||||
activeImageIndex.value = 0
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
@@ -85,6 +82,7 @@ async function fetchBackgroundImages() {
|
||||
|
||||
// 开始背景图片轮换
|
||||
function startBackgroundRotation() {
|
||||
// 清除轮换定时器
|
||||
if (backgroundRotationTimer) clearInterval(backgroundRotationTimer)
|
||||
|
||||
if (backgroundImages.value.length > 1) {
|
||||
@@ -106,7 +104,6 @@ function startBackgroundRotation() {
|
||||
function preloadImage(url: string): Promise<boolean> {
|
||||
return new Promise(resolve => {
|
||||
const img = new Image()
|
||||
const imageUrl = getImgUrl(url)
|
||||
|
||||
img.onload = () => resolve(true)
|
||||
img.onerror = () => resolve(false)
|
||||
@@ -117,7 +114,7 @@ function preloadImage(url: string): Promise<boolean> {
|
||||
resolve(false)
|
||||
}, 5000) // 5秒超时
|
||||
|
||||
img.src = imageUrl
|
||||
img.src = url
|
||||
|
||||
// 如果图片已经缓存,onload可能不会触发
|
||||
if (img.complete) {
|
||||
@@ -127,28 +124,6 @@ function preloadImage(url: string): Promise<boolean> {
|
||||
})
|
||||
}
|
||||
|
||||
// 计算图片地址
|
||||
function getImgUrl(url: string) {
|
||||
// 使用图片缓存
|
||||
if (globalSettings.GLOBAL_IMAGE_CACHE && isLogin.value)
|
||||
return `${import.meta.env.VITE_API_BASE_URL}system/cache/image?url=${encodeURIComponent(url)}`
|
||||
return url
|
||||
}
|
||||
|
||||
// 处理页面可见性变化
|
||||
function handleVisibilityChange() {
|
||||
if (document.visibilityState === 'visible') {
|
||||
// 如果已有背景图片数据,直接重启轮换
|
||||
if (backgroundImages.value.length > 0) {
|
||||
startBackgroundRotation()
|
||||
}
|
||||
// 如果没有背景图片数据,重新获取
|
||||
else {
|
||||
fetchBackgroundImages().then(() => startBackgroundRotation())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 添加logo动画效果并延迟移除加载界面
|
||||
function animateAndRemoveLoader() {
|
||||
const loadingBg = document.querySelector('#loading-bg') as HTMLElement
|
||||
@@ -167,29 +142,61 @@ function animateAndRemoveLoader() {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 加载背景图片
|
||||
async function loadBackgroundImages() {
|
||||
await fetchBackgroundImages()
|
||||
.then(() => {
|
||||
startBackgroundRotation()
|
||||
})
|
||||
.catch(() => {
|
||||
// 3秒后重试
|
||||
setTimeout(() => {
|
||||
loadBackgroundImages()
|
||||
}, 3000)
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// 初始化data-theme属性
|
||||
updateHtmlThemeAttribute(globalTheme.name.value)
|
||||
|
||||
// 加载背景图片并开始轮换
|
||||
fetchBackgroundImages().then(() => startBackgroundRotation())
|
||||
// 默认隐藏页面
|
||||
show.value = false
|
||||
|
||||
// 添加页面可见性变化监听
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange)
|
||||
// 加载背景图片
|
||||
await loadBackgroundImages()
|
||||
|
||||
// 移除加载动画
|
||||
ensureRenderComplete(() => {
|
||||
nextTick(() => {
|
||||
setTimeout(() => {
|
||||
// 移除加载动画
|
||||
// 移除加载动画,显示页面
|
||||
animateAndRemoveLoader()
|
||||
}, 1500)
|
||||
})
|
||||
})
|
||||
|
||||
// 添加页面可见性变化监听
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
loadBackgroundImages()
|
||||
}
|
||||
})
|
||||
|
||||
// 添加PWA的页面恢复事件监听
|
||||
window.addEventListener('pageshow', event => {
|
||||
// persisted属性为true表示页面是从bfcache中恢复的
|
||||
if (event.persisted) {
|
||||
loadBackgroundImages()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// 移除页面可见性监听
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange)
|
||||
document.removeEventListener('visibilitychange', () => {})
|
||||
// 移除PWA的页面恢复事件监听
|
||||
window.removeEventListener('pageshow', () => {})
|
||||
|
||||
// 清除轮换定时器
|
||||
if (backgroundRotationTimer) {
|
||||
@@ -202,20 +209,18 @@ onUnmounted(() => {
|
||||
<template>
|
||||
<div class="app-wrapper">
|
||||
<!-- 透明主题背景 -->
|
||||
<template v-if="backgroundImages.length > 0 && (isTransparentTheme || !isLogin)">
|
||||
<div class="background-container">
|
||||
<div
|
||||
v-for="(imageUrl, index) in backgroundImages"
|
||||
:key="index"
|
||||
class="background-image"
|
||||
:class="{ 'active': index === activeImageIndex }"
|
||||
:style="{ backgroundImage: `url(${getImgUrl(imageUrl)})` }"
|
||||
></div>
|
||||
<!-- 全局磨砂层 -->
|
||||
<div v-if="isLogin" class="global-blur-layer"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-if="backgroundImages.length > 0 && (isTransparentTheme || !isLogin)" class="background-container">
|
||||
<div
|
||||
v-for="(imageUrl, index) in backgroundImages"
|
||||
:key="`bg-${index}-${loginStateKey}`"
|
||||
class="background-image"
|
||||
:class="{ 'active': index === activeImageIndex }"
|
||||
:style="{ 'backgroundImage': `url(${imageUrl})` }"
|
||||
></div>
|
||||
<!-- 全局磨砂层 -->
|
||||
<div v-if="isLogin && isTransparentTheme" class="global-blur-layer"></div>
|
||||
</div>
|
||||
<!-- 页面内容 -->
|
||||
<VApp v-show="show" :class="{ 'transparent-app': isTransparentTheme }">
|
||||
<RouterView />
|
||||
</VApp>
|
||||
|
||||
@@ -335,6 +335,10 @@ export const actionStepOptions = [
|
||||
title: i18n.global.t('actionStep.transferFile'),
|
||||
value: '整理文件',
|
||||
},
|
||||
{
|
||||
title: i18n.global.t('actionStep.invokePlugin'),
|
||||
value: '调用插件',
|
||||
},
|
||||
]
|
||||
|
||||
// 操作步骤字典
|
||||
|
||||
@@ -565,9 +565,9 @@ export interface NotExistMediaInfo {
|
||||
|
||||
// 插件
|
||||
export interface Plugin {
|
||||
id?: string
|
||||
id: string
|
||||
// 插件名称
|
||||
plugin_name?: string
|
||||
plugin_name: string
|
||||
// 插件描述
|
||||
plugin_desc?: string
|
||||
// 插件图标
|
||||
@@ -632,7 +632,7 @@ export interface DashboardItem {
|
||||
// 页面元素
|
||||
elements: RenderProps[]
|
||||
// 渲染方式
|
||||
render_mode: string
|
||||
render_mode?: string
|
||||
}
|
||||
|
||||
// 种子信息
|
||||
@@ -1305,3 +1305,49 @@ export interface Workflow {
|
||||
// 最后执行时间
|
||||
last_time?: string
|
||||
}
|
||||
|
||||
// 种子缓存项
|
||||
export interface TorrentCacheItem {
|
||||
// 种子hash(用于操作标识)
|
||||
hash: string
|
||||
// 站点域名
|
||||
domain: string
|
||||
// 种子标题
|
||||
title: string
|
||||
// 种子描述
|
||||
description?: string
|
||||
// 种子大小
|
||||
size: number
|
||||
// 发布时间
|
||||
pubdate?: string
|
||||
// 站点名称
|
||||
site_name?: string
|
||||
// 识别的媒体名称
|
||||
media_name?: string
|
||||
// 识别的媒体年份
|
||||
media_year?: string
|
||||
// 识别的媒体类型
|
||||
media_type?: string
|
||||
// 季集信息
|
||||
season_episode?: string
|
||||
// 资源信息
|
||||
resource_term?: string
|
||||
// 种子链接
|
||||
enclosure?: string
|
||||
// 详情页面
|
||||
page_url?: string
|
||||
// 海报图片
|
||||
poster_path?: string
|
||||
// 背景图片
|
||||
backdrop_path?: string
|
||||
}
|
||||
|
||||
// 种子缓存数据
|
||||
export interface TorrentCacheData {
|
||||
// 缓存数量
|
||||
count: number
|
||||
// 站点数量
|
||||
sites: number
|
||||
// 缓存数据
|
||||
data: TorrentCacheItem[]
|
||||
}
|
||||
|
||||
BIN
src/assets/images/logos/notification.png
Normal file
BIN
src/assets/images/logos/notification.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
@@ -136,6 +136,12 @@ const sort = ref('name')
|
||||
// 是否显示目录树
|
||||
const showDirTree = ref(false)
|
||||
|
||||
// 拖动分隔条相关
|
||||
const navigatorWidth = ref(280) // 初始宽度
|
||||
const isDragging = ref(false)
|
||||
const dragStartX = ref(0)
|
||||
const dragStartWidth = ref(0)
|
||||
|
||||
// 计算属性
|
||||
const storagesArray = computed(() => {
|
||||
return props.storages?.map(item => ({
|
||||
@@ -181,6 +187,58 @@ function fileListUpdated(items: FileItem[]) {
|
||||
fileListItems.value = items
|
||||
}
|
||||
|
||||
// 阻止选择事件
|
||||
function preventSelect(event: Event) {
|
||||
event.preventDefault()
|
||||
return false
|
||||
}
|
||||
|
||||
// 拖动分隔条相关方法
|
||||
function startDrag(event: MouseEvent) {
|
||||
event.preventDefault() // 阻止默认行为
|
||||
event.stopPropagation() // 阻止事件冒泡
|
||||
|
||||
isDragging.value = true
|
||||
dragStartX.value = event.clientX
|
||||
dragStartWidth.value = navigatorWidth.value
|
||||
|
||||
document.addEventListener('mousemove', handleDrag, { passive: false })
|
||||
document.addEventListener('mouseup', stopDrag, { passive: false })
|
||||
document.addEventListener('selectstart', preventSelect) // 阻止选择开始
|
||||
|
||||
document.body.style.cursor = 'col-resize'
|
||||
document.body.style.userSelect = 'none'
|
||||
;(document.body.style as any).webkitUserSelect = 'none' // Safari兼容
|
||||
;(document.body.style as any).mozUserSelect = 'none' // Firefox兼容
|
||||
}
|
||||
|
||||
function handleDrag(event: MouseEvent) {
|
||||
if (!isDragging.value) return
|
||||
|
||||
event.preventDefault() // 阻止默认行为
|
||||
|
||||
const deltaX = event.clientX - dragStartX.value
|
||||
const newWidth = dragStartWidth.value + deltaX
|
||||
|
||||
// 设置最小和最大宽度限制
|
||||
const minWidth = 200
|
||||
const maxWidth = window.innerWidth * 0.6
|
||||
|
||||
navigatorWidth.value = Math.max(minWidth, Math.min(maxWidth, newWidth))
|
||||
}
|
||||
|
||||
function stopDrag() {
|
||||
isDragging.value = false
|
||||
document.removeEventListener('mousemove', handleDrag)
|
||||
document.removeEventListener('mouseup', stopDrag)
|
||||
document.removeEventListener('selectstart', preventSelect)
|
||||
|
||||
document.body.style.cursor = ''
|
||||
document.body.style.userSelect = ''
|
||||
;(document.body.style as any).webkitUserSelect = ''
|
||||
;(document.body.style as any).mozUserSelect = ''
|
||||
}
|
||||
|
||||
// 外层DIV大小控制
|
||||
const scrollStyle = computed(() => {
|
||||
return appMode
|
||||
@@ -219,8 +277,14 @@ const fileListStyle = computed(() => {
|
||||
:items="fileListItems"
|
||||
:endpoints="endpoints"
|
||||
:axios="axios"
|
||||
:style="{ width: `${navigatorWidth}px`, minWidth: `${navigatorWidth}px` }"
|
||||
@navigate="pathChanged"
|
||||
/>
|
||||
<!-- 拖动分隔条 -->
|
||||
<div v-if="showDirTree" class="divider" :class="{ 'divider-dragging': isDragging }" @mousedown="startDrag">
|
||||
<div class="divider-line"></div>
|
||||
<VIcon class="divider-icon" size="small">mdi-drag-vertical</VIcon>
|
||||
</div>
|
||||
<FileList
|
||||
:item="item"
|
||||
:storage="activeStorage"
|
||||
@@ -231,6 +295,7 @@ const fileListStyle = computed(() => {
|
||||
:sort="sort"
|
||||
:listStyle="fileListStyle"
|
||||
:showTree="showDirTree"
|
||||
:style="{ flex: 1 }"
|
||||
@pathchanged="pathChanged"
|
||||
@loading="loadingChanged"
|
||||
@refreshed="refreshPending = false"
|
||||
@@ -243,3 +308,64 @@ const fileListStyle = computed(() => {
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.divider {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: transparent;
|
||||
cursor: col-resize;
|
||||
inline-size: 4px;
|
||||
transition: background-color 0.2s ease;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.divider:hover {
|
||||
background-color: rgba(var(--v-theme-on-surface), 0.08);
|
||||
}
|
||||
|
||||
.divider-dragging {
|
||||
background-color: rgba(var(--v-theme-primary), 0.12) !important;
|
||||
}
|
||||
|
||||
.divider-line {
|
||||
background-color: rgba(var(--v-theme-outline), 0.3);
|
||||
block-size: 100%;
|
||||
inline-size: 1px;
|
||||
transition: background-color 0.2s ease;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.divider-dragging .divider-line {
|
||||
background-color: rgb(var(--v-theme-primary)) !important;
|
||||
}
|
||||
|
||||
.divider:hover .divider-line {
|
||||
background-color: rgba(var(--v-theme-primary), 0.8);
|
||||
}
|
||||
|
||||
.divider-icon {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
padding: 2px;
|
||||
border-radius: 2px;
|
||||
background-color: rgba(var(--v-theme-surface), 0.9);
|
||||
color: rgba(var(--v-theme-on-surface-variant), 0.6);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.divider-dragging .divider-icon {
|
||||
background-color: rgba(var(--v-theme-surface), 0.95);
|
||||
color: rgb(var(--v-theme-primary));
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.divider:hover .divider-icon {
|
||||
color: rgba(var(--v-theme-primary), 0.9);
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,6 +5,10 @@ import filter_svg from '@images/svg/filter.svg'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import { innerFilterRules } from '@/api/constants'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useDisplay } from 'vuetify'
|
||||
|
||||
// 显示器宽度
|
||||
const display = useDisplay()
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
@@ -106,8 +110,20 @@ function onClose() {
|
||||
<VImg :src="filter_svg" cover class="mt-7" max-width="3rem" />
|
||||
</VCardText>
|
||||
</VCard>
|
||||
<VDialog v-if="ruleInfoDialog" v-model="ruleInfoDialog" scrollable max-width="40rem">
|
||||
<VCard :title="t('customRule.title', { id: props.rule.id })">
|
||||
<VDialog
|
||||
v-if="ruleInfoDialog"
|
||||
v-model="ruleInfoDialog"
|
||||
scrollable
|
||||
max-width="40rem"
|
||||
:fullscreen="!display.mdAndUp.value"
|
||||
>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-filter-outline" class="me-2" />
|
||||
</template>
|
||||
<VCardTitle>{{ t('customRule.title', { id: props.rule.id }) }}</VCardTitle>
|
||||
</VCardItem>
|
||||
<VDialogCloseBtn v-model="ruleInfoDialog" />
|
||||
<VDivider />
|
||||
<VCardText>
|
||||
@@ -121,6 +137,7 @@ function onClose() {
|
||||
:hint="t('customRule.hint.ruleId')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-identifier"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -131,6 +148,7 @@ function onClose() {
|
||||
:hint="t('customRule.hint.ruleName')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-label"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
@@ -141,6 +159,7 @@ function onClose() {
|
||||
:hint="t('customRule.hint.include')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-plus-circle"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
@@ -151,6 +170,7 @@ function onClose() {
|
||||
:hint="t('customRule.hint.exclude')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-minus-circle"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6">
|
||||
@@ -161,6 +181,7 @@ function onClose() {
|
||||
:hint="t('customRule.hint.sizeRange')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-harddisk"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6">
|
||||
@@ -171,6 +192,7 @@ function onClose() {
|
||||
:hint="t('customRule.hint.seeders')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-account-group"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6">
|
||||
@@ -181,13 +203,14 @@ function onClose() {
|
||||
:hint="t('customRule.hint.publishTime')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-calendar-clock"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
<VCardActions class="pt-3">
|
||||
<VBtn @click="saveRuleInfo" variant="elevated" prepend-icon="mdi-content-save" class="px-5">{{
|
||||
<VBtn @click="saveRuleInfo" prepend-icon="mdi-content-save" class="px-5">{{
|
||||
t('customRule.action.confirm')
|
||||
}}</VBtn>
|
||||
</VCardActions>
|
||||
|
||||
@@ -214,7 +214,7 @@ watch(
|
||||
<VForm>
|
||||
<VRow>
|
||||
<VCol cols="6">
|
||||
<VSelect
|
||||
<VAutocomplete
|
||||
v-model="props.directory.media_type"
|
||||
variant="underlined"
|
||||
:items="typeItems"
|
||||
@@ -223,7 +223,7 @@ watch(
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6">
|
||||
<VSelect
|
||||
<VAutocomplete
|
||||
v-model="props.directory.media_category"
|
||||
variant="underlined"
|
||||
:items="getCategories"
|
||||
@@ -231,7 +231,7 @@ watch(
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="4">
|
||||
<VSelect
|
||||
<VAutocomplete
|
||||
v-model="props.directory.storage"
|
||||
variant="underlined"
|
||||
:items="resourceStorageOptions"
|
||||
@@ -277,7 +277,7 @@ watch(
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="4">
|
||||
<VSelect
|
||||
<VAutocomplete
|
||||
v-model="props.directory.library_storage"
|
||||
variant="underlined"
|
||||
:items="libraryStorageOptions"
|
||||
|
||||
@@ -10,6 +10,10 @@ import custom_image from '@images/logos/downloader.png'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { downloaderDict } from '@/api/constants'
|
||||
import { useDisplay } from 'vuetify'
|
||||
|
||||
// 显示器宽度
|
||||
const display = useDisplay()
|
||||
|
||||
// 获取i18n实例
|
||||
const { t } = useI18n()
|
||||
@@ -188,8 +192,22 @@ onUnmounted(() => {
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VHover>
|
||||
<VDialog v-if="downloaderInfoDialog" v-model="downloaderInfoDialog" scrollable max-width="40rem">
|
||||
<VCard :title="`${props.downloader.name} - ${t('downloader.title')}`">
|
||||
|
||||
<VDialog
|
||||
v-if="downloaderInfoDialog"
|
||||
v-model="downloaderInfoDialog"
|
||||
scrollable
|
||||
max-width="40rem"
|
||||
:fullscreen="!display.mdAndUp.value"
|
||||
>
|
||||
<VCard>
|
||||
<VCardItem class="py-2">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-download" class="me-2" />
|
||||
</template>
|
||||
<VCardTitle>{{ t('common.config') }}</VCardTitle>
|
||||
<VCardSubtitle>{{ props.downloader.name }}</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VDialogCloseBtn v-model="downloaderInfoDialog" />
|
||||
<VDivider />
|
||||
<VCardText>
|
||||
@@ -215,6 +233,7 @@ onUnmounted(() => {
|
||||
:hint="t('downloader.name')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-label"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -225,6 +244,7 @@ onUnmounted(() => {
|
||||
:hint="t('downloader.host')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-server"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -234,6 +254,7 @@ onUnmounted(() => {
|
||||
:hint="t('downloader.username')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-account"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -244,6 +265,7 @@ onUnmounted(() => {
|
||||
:hint="t('downloader.password')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-lock"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -292,6 +314,7 @@ onUnmounted(() => {
|
||||
:hint="t('downloader.name')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-label"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -302,6 +325,7 @@ onUnmounted(() => {
|
||||
:hint="t('downloader.host')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-server"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -311,6 +335,7 @@ onUnmounted(() => {
|
||||
:hint="t('downloader.username')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-account"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -321,6 +346,7 @@ onUnmounted(() => {
|
||||
:hint="t('downloader.password')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-lock"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -332,6 +358,7 @@ onUnmounted(() => {
|
||||
:hint="t('downloader.customTypeHint')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-cog"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -341,13 +368,14 @@ onUnmounted(() => {
|
||||
:hint="t('downloader.nameRequired')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-label"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
<VCardActions class="pt-3">
|
||||
<VBtn @click="saveDownloaderInfo" variant="elevated" prepend-icon="mdi-content-save" class="px-5">
|
||||
<VBtn @click="saveDownloaderInfo" prepend-icon="mdi-content-save" class="px-5">
|
||||
{{ t('common.save') }}
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
|
||||
@@ -56,7 +56,7 @@ onMounted(() => {
|
||||
<VCardTitle>{{ t('filterRule.priority') }} {{ props.pri }}</VCardTitle>
|
||||
<VRow>
|
||||
<VCol>
|
||||
<VSelect
|
||||
<VAutocomplete
|
||||
v-model="props.rules"
|
||||
variant="underlined"
|
||||
:items="selectFilterOptions"
|
||||
|
||||
@@ -8,6 +8,10 @@ import ImportCodeDialog from '@/components/dialog/ImportCodeDialog.vue'
|
||||
import filter_group_svg from '@images/svg/filter-group.svg'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useDisplay } from 'vuetify'
|
||||
|
||||
// 显示器宽度
|
||||
const display = useDisplay()
|
||||
|
||||
// 获取i18n实例
|
||||
const { t } = useI18n()
|
||||
@@ -219,7 +223,13 @@ function onClose() {
|
||||
<VImg :src="filter_group_svg" cover class="mt-10" max-width="3rem" />
|
||||
</VCardText>
|
||||
</VCard>
|
||||
<VDialog v-if="groupInfoDialog" v-model="groupInfoDialog" scrollable max-width="80rem">
|
||||
<VDialog
|
||||
v-if="groupInfoDialog"
|
||||
v-model="groupInfoDialog"
|
||||
scrollable
|
||||
max-width="80rem"
|
||||
:fullscreen="!display.mdAndUp.value"
|
||||
>
|
||||
<VCard :title="`${props.group.name} - ${t('filterRule.title')}`">
|
||||
<VDialogCloseBtn v-model="groupInfoDialog" />
|
||||
<VDivider />
|
||||
@@ -233,26 +243,29 @@ function onClose() {
|
||||
:hint="t('filterRule.groupName')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-label"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6" md="3">
|
||||
<VSelect
|
||||
<VAutocomplete
|
||||
v-model="groupInfo.media_type"
|
||||
:label="t('filterRule.mediaType')"
|
||||
:items="mediaTypeItems"
|
||||
:hint="t('filterRule.mediaType')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-movie-open"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6" md="3">
|
||||
<VSelect
|
||||
<VAutocomplete
|
||||
v-model="groupInfo.category"
|
||||
:items="getCategories"
|
||||
:label="t('filterRule.category')"
|
||||
:hint="t('filterRule.category')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-folder-open"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -280,17 +293,17 @@ function onClose() {
|
||||
<div class="text-center" v-if="filterRuleCards.length == 0">{{ t('filterRule.add') }}</div>
|
||||
</VCardText>
|
||||
<VCardActions class="pt-3">
|
||||
<VBtn color="primary" variant="tonal" @click="addFilterCard">
|
||||
<VBtn color="primary" @click="addFilterCard">
|
||||
<VIcon icon="mdi-plus" />
|
||||
</VBtn>
|
||||
<VBtn color="success" variant="tonal" @click="importRules('priority')">
|
||||
<VBtn color="success" @click="importRules('priority')">
|
||||
<VIcon icon="mdi-import" />
|
||||
</VBtn>
|
||||
<VBtn color="info" variant="tonal" @click="shareRules">
|
||||
<VBtn color="info" @click="shareRules">
|
||||
<VIcon icon="mdi-share" />
|
||||
</VBtn>
|
||||
<VSpacer />
|
||||
<VBtn @click="saveGroupInfo" variant="elevated" prepend-icon="mdi-content-save" class="px-5">
|
||||
<VBtn @click="saveGroupInfo" prepend-icon="mdi-content-save" class="px-5">
|
||||
{{ t('common.save') }}
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
|
||||
@@ -10,6 +10,10 @@ import api from '@/api'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { mediaServerDict } from '@/api/constants'
|
||||
import { useDisplay } from 'vuetify'
|
||||
|
||||
// 显示器宽度
|
||||
const display = useDisplay()
|
||||
|
||||
// 获取i18n实例
|
||||
const { t } = useI18n()
|
||||
@@ -199,8 +203,22 @@ onMounted(() => {
|
||||
<VImg :src="getIcon" cover class="mt-7 me-3" max-width="3rem" min-width="3rem" />
|
||||
</VCardText>
|
||||
</VCard>
|
||||
<VDialog v-if="mediaServerInfoDialog" v-model="mediaServerInfoDialog" scrollable max-width="40rem">
|
||||
<VCard :title="`${props.mediaserver.name} - ${t('common.config')}`">
|
||||
|
||||
<VDialog
|
||||
v-if="mediaServerInfoDialog"
|
||||
v-model="mediaServerInfoDialog"
|
||||
scrollable
|
||||
max-width="40rem"
|
||||
:fullscreen="!display.mdAndUp.value"
|
||||
>
|
||||
<VCard>
|
||||
<VCardItem class="py-2">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-cog" class="me-2" />
|
||||
</template>
|
||||
<VCardTitle>{{ t('common.config') }}</VCardTitle>
|
||||
<VCardSubtitle>{{ props.mediaserver.name }}</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VDialogCloseBtn v-model="mediaServerInfoDialog" />
|
||||
<VDivider />
|
||||
<VCardText>
|
||||
@@ -219,6 +237,7 @@ onMounted(() => {
|
||||
:hint="t('mediaserver.serverAlias')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-label"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -229,6 +248,7 @@ onMounted(() => {
|
||||
:hint="t('mediaserver.hostHint')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-server"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -239,6 +259,7 @@ onMounted(() => {
|
||||
:hint="t('mediaserver.playHostHint')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-play-network"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -248,10 +269,11 @@ onMounted(() => {
|
||||
:hint="t('mediaserver.embyApiKeyHint')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-key"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VSelect
|
||||
<VAutocomplete
|
||||
v-model="mediaServerInfo.sync_libraries"
|
||||
:label="t('mediaserver.syncLibraries')"
|
||||
:items="librariesOptions"
|
||||
@@ -262,6 +284,7 @@ onMounted(() => {
|
||||
persistent-hint
|
||||
active
|
||||
append-inner-icon="mdi-refresh"
|
||||
prepend-inner-icon="mdi-library"
|
||||
@click:append-inner="loadLibrary(mediaServerInfo.name)"
|
||||
/>
|
||||
</VCol>
|
||||
@@ -275,6 +298,7 @@ onMounted(() => {
|
||||
:hint="t('mediaserver.serverAlias')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-label"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -285,6 +309,7 @@ onMounted(() => {
|
||||
:hint="t('mediaserver.hostHint')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-server"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -295,6 +320,7 @@ onMounted(() => {
|
||||
:hint="t('mediaserver.playHostHint')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-play-network"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -304,10 +330,11 @@ onMounted(() => {
|
||||
:hint="t('mediaserver.jellyfinApiKeyHint')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-key"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VSelect
|
||||
<VAutocomplete
|
||||
v-model="mediaServerInfo.sync_libraries"
|
||||
:label="t('mediaserver.syncLibraries')"
|
||||
:items="librariesOptions"
|
||||
@@ -318,6 +345,7 @@ onMounted(() => {
|
||||
persistent-hint
|
||||
active
|
||||
append-inner-icon="mdi-refresh"
|
||||
prepend-inner-icon="mdi-library"
|
||||
@click:append-inner="loadLibrary(mediaServerInfo.name)"
|
||||
/>
|
||||
</VCol>
|
||||
@@ -331,6 +359,7 @@ onMounted(() => {
|
||||
:hint="t('mediaserver.serverAlias')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-label"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -341,6 +370,7 @@ onMounted(() => {
|
||||
:hint="t('mediaserver.hostHint')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-server"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
@@ -351,10 +381,16 @@ onMounted(() => {
|
||||
:hint="t('mediaserver.playHostHint')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-play-network"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField v-model="mediaServerInfo.config.username" :label="t('mediaserver.username')" active />
|
||||
<VTextField
|
||||
v-model="mediaServerInfo.config.username"
|
||||
:label="t('mediaserver.username')"
|
||||
active
|
||||
prepend-inner-icon="mdi-account"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
@@ -362,10 +398,11 @@ onMounted(() => {
|
||||
v-model="mediaServerInfo.config.password"
|
||||
:label="t('mediaserver.password')"
|
||||
active
|
||||
prepend-inner-icon="mdi-lock"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VSelect
|
||||
<VAutocomplete
|
||||
v-model="mediaServerInfo.sync_libraries"
|
||||
:label="t('mediaserver.syncLibraries')"
|
||||
:items="librariesOptions"
|
||||
@@ -376,6 +413,7 @@ onMounted(() => {
|
||||
persistent-hint
|
||||
active
|
||||
append-inner-icon="mdi-refresh"
|
||||
prepend-inner-icon="mdi-library"
|
||||
@click:append-inner="loadLibrary(mediaServerInfo.name)"
|
||||
/>
|
||||
</VCol>
|
||||
@@ -389,6 +427,7 @@ onMounted(() => {
|
||||
:hint="t('mediaserver.serverAlias')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-label"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -399,6 +438,7 @@ onMounted(() => {
|
||||
:hint="t('mediaserver.hostHint')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-server"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -409,6 +449,7 @@ onMounted(() => {
|
||||
:hint="t('mediaserver.playHostHint')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-play-network"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -418,25 +459,11 @@ onMounted(() => {
|
||||
:hint="t('mediaserver.plexTokenHint')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-key"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VSelect
|
||||
v-model="mediaServerInfo.sync_libraries"
|
||||
:label="t('mediaserver.syncLibraries')"
|
||||
:items="librariesOptions"
|
||||
chips
|
||||
multiple
|
||||
clearable
|
||||
:hint="t('mediaserver.syncLibrariesHint')"
|
||||
persistent-hint
|
||||
active
|
||||
append-inner-icon="mdi-refresh"
|
||||
@click:append-inner="loadLibrary(mediaServerInfo.name)"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VSelect
|
||||
<VAutocomplete
|
||||
v-model="mediaServerInfo.sync_libraries"
|
||||
:label="t('mediaserver.syncLibraries')"
|
||||
:items="librariesOptions"
|
||||
@@ -447,6 +474,7 @@ onMounted(() => {
|
||||
persistent-hint
|
||||
active
|
||||
append-inner-icon="mdi-refresh"
|
||||
prepend-inner-icon="mdi-library"
|
||||
@click:append-inner="loadLibrary(mediaServerInfo.name)"
|
||||
/>
|
||||
</VCol>
|
||||
@@ -458,16 +486,22 @@ onMounted(() => {
|
||||
:label="t('mediaserver.type')"
|
||||
:hint="t('mediaserver.customTypeHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-cog"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField :label="t('common.name')" :hint="t('mediaserver.nameRequired')" persistent-hint />
|
||||
<VTextField
|
||||
:label="t('common.name')"
|
||||
:hint="t('mediaserver.nameRequired')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-label"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
<VCardActions class="pt-3">
|
||||
<VBtn @click="saveMediaServerInfo" variant="elevated" prepend-icon="mdi-content-save" class="px-5">
|
||||
<VBtn @click="saveMediaServerInfo" prepend-icon="mdi-content-save" class="px-5">
|
||||
{{ t('common.confirm') }}
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
|
||||
@@ -6,9 +6,14 @@ import vocechat_image from '@images/logos/vocechat.png'
|
||||
import synologychat_image from '@images/logos/synologychat.png'
|
||||
import slack_image from '@images/logos/slack.webp'
|
||||
import chrome_image from '@images/logos/chrome.png'
|
||||
import custom_image from '@images/logos/notification.png'
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useDisplay } from 'vuetify'
|
||||
|
||||
// 显示器宽度
|
||||
const display = useDisplay()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -51,6 +56,7 @@ const notificationTypeNames: { [key: string]: string } = {
|
||||
synologychat: t('notification.synologychat.name'),
|
||||
slack: t('notification.slack.name'),
|
||||
webpush: t('notification.webpush.name'),
|
||||
custom: t('setting.notification.custom'),
|
||||
}
|
||||
|
||||
// 消息类型下拉字典
|
||||
@@ -105,7 +111,7 @@ const getIcon = computed(() => {
|
||||
case 'webpush':
|
||||
return chrome_image
|
||||
default:
|
||||
return wechat_image
|
||||
return custom_image
|
||||
}
|
||||
})
|
||||
|
||||
@@ -131,12 +137,26 @@ function onClose() {
|
||||
</div>
|
||||
<div class="text-body-1 mb-3">{{ notificationTypeNames[notification.type] }}</div>
|
||||
</div>
|
||||
<VImg :src="getIcon" cover class="mt-7 me-3" max-width="3rem" />
|
||||
<VImg :src="getIcon" cover class="mt-7 me-1" max-width="3rem" />
|
||||
</VCardText>
|
||||
</VCard>
|
||||
<VDialog v-if="notificationInfoDialog" v-model="notificationInfoDialog" scrollable max-width="40rem">
|
||||
<VCard :title="`${props.notification.name} - ${t('notification.config')}`">
|
||||
<VDialogCloseBtn v-model="notificationInfoDialog" />
|
||||
|
||||
<VDialog
|
||||
v-if="notificationInfoDialog"
|
||||
v-model="notificationInfoDialog"
|
||||
scrollable
|
||||
max-width="40rem"
|
||||
:fullscreen="!display.mdAndUp.value"
|
||||
>
|
||||
<VCard>
|
||||
<VCardItem class="py-2">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-cog" class="me-2" />
|
||||
</template>
|
||||
<VCardTitle>{{ t('common.config') }}</VCardTitle>
|
||||
<VCardSubtitle>{{ props.notification.name }}</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VDialogCloseBtn @click="notificationInfoDialog = false" />
|
||||
<VDivider />
|
||||
<VCardText>
|
||||
<VForm>
|
||||
@@ -145,7 +165,7 @@ function onClose() {
|
||||
<VSwitch v-model="notificationInfo.enabled" :label="t('notification.enabled')" />
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VSelect
|
||||
<VAutocomplete
|
||||
v-model="notificationInfo.switchs"
|
||||
:items="notificationTypes"
|
||||
:label="t('notification.type')"
|
||||
@@ -154,6 +174,7 @@ function onClose() {
|
||||
clearable
|
||||
chips
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-bell-outline"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -165,6 +186,7 @@ function onClose() {
|
||||
:placeholder="t('notification.name')"
|
||||
:hint="t('notification.nameHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-label"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -173,6 +195,7 @@ function onClose() {
|
||||
:label="t('notification.wechat.corpId')"
|
||||
:hint="t('notification.wechat.corpIdHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-domain"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -181,6 +204,7 @@ function onClose() {
|
||||
:label="t('notification.wechat.appId')"
|
||||
:hint="t('notification.wechat.appIdHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-application"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -189,6 +213,7 @@ function onClose() {
|
||||
:label="t('notification.wechat.appSecret')"
|
||||
:hint="t('notification.wechat.appSecretHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-key"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -197,6 +222,7 @@ function onClose() {
|
||||
:label="t('notification.wechat.proxy')"
|
||||
:hint="t('notification.wechat.proxyHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-server-network"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -205,6 +231,7 @@ function onClose() {
|
||||
:label="t('notification.wechat.token')"
|
||||
:hint="t('notification.wechat.tokenHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-key-variant"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -213,6 +240,7 @@ function onClose() {
|
||||
:label="t('notification.wechat.encodingAesKey')"
|
||||
:hint="t('notification.wechat.encodingAesKeyHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-lock"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -222,10 +250,11 @@ function onClose() {
|
||||
:placeholder="t('notification.wechat.adminsPlaceholder')"
|
||||
:hint="t('notification.wechat.adminsHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-account-supervisor"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow v-if="notificationInfo.type == 'telegram'">
|
||||
<VRow v-else-if="notificationInfo.type == 'telegram'">
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="notificationInfo.name"
|
||||
@@ -233,6 +262,7 @@ function onClose() {
|
||||
:placeholder="t('notification.name')"
|
||||
:hint="t('notification.nameHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-label"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -241,6 +271,7 @@ function onClose() {
|
||||
:label="t('notification.telegram.token')"
|
||||
:hint="t('notification.telegram.tokenHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-key"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -249,6 +280,7 @@ function onClose() {
|
||||
:label="t('notification.telegram.chatId')"
|
||||
:hint="t('notification.telegram.chatIdHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-chat"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -258,6 +290,7 @@ function onClose() {
|
||||
:placeholder="t('notification.telegram.usersPlaceholder')"
|
||||
:hint="t('notification.telegram.usersHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-account-group"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -267,10 +300,11 @@ function onClose() {
|
||||
:placeholder="t('notification.telegram.adminsPlaceholder')"
|
||||
:hint="t('notification.telegram.adminsHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-account-supervisor"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow v-if="notificationInfo.type == 'slack'">
|
||||
<VRow v-else-if="notificationInfo.type == 'slack'">
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="notificationInfo.name"
|
||||
@@ -278,6 +312,7 @@ function onClose() {
|
||||
:placeholder="t('notification.name')"
|
||||
:hint="t('notification.nameHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-label"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -287,6 +322,7 @@ function onClose() {
|
||||
:placeholder="t('notification.slack.oauthTokenPlaceholder')"
|
||||
:hint="t('notification.slack.oauthTokenHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-key"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -296,6 +332,7 @@ function onClose() {
|
||||
:placeholder="t('notification.slack.appTokenPlaceholder')"
|
||||
:hint="t('notification.slack.appTokenHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-application"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -305,10 +342,11 @@ function onClose() {
|
||||
:placeholder="t('notification.slack.channelPlaceholder')"
|
||||
:hint="t('notification.slack.channelHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-pound"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow v-if="notificationInfo.type == 'synologychat'">
|
||||
<VRow v-else-if="notificationInfo.type == 'synologychat'">
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="notificationInfo.name"
|
||||
@@ -316,6 +354,7 @@ function onClose() {
|
||||
:placeholder="t('notification.name')"
|
||||
:hint="t('notification.nameHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-label"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -324,6 +363,7 @@ function onClose() {
|
||||
:label="t('notification.synologychat.webhook')"
|
||||
:hint="t('notification.synologychat.webhookHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-webhook"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -332,10 +372,11 @@ function onClose() {
|
||||
:label="t('notification.synologychat.token')"
|
||||
:hint="t('notification.synologychat.tokenHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-key"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow v-if="notificationInfo.type == 'vocechat'">
|
||||
<VRow v-else-if="notificationInfo.type == 'vocechat'">
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="notificationInfo.name"
|
||||
@@ -343,6 +384,7 @@ function onClose() {
|
||||
:placeholder="t('notification.name')"
|
||||
:hint="t('notification.nameHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-label"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -351,6 +393,7 @@ function onClose() {
|
||||
:label="t('notification.vocechat.host')"
|
||||
:hint="t('notification.vocechat.hostHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-server"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -359,6 +402,7 @@ function onClose() {
|
||||
:label="t('notification.vocechat.apiKey')"
|
||||
:hint="t('notification.vocechat.apiKeyHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-key"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -368,10 +412,11 @@ function onClose() {
|
||||
:placeholder="t('notification.vocechat.channelIdPlaceholder')"
|
||||
:hint="t('notification.vocechat.channelIdHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-pound"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow v-if="notificationInfo.type == 'webpush'">
|
||||
<VRow v-else-if="notificationInfo.type == 'webpush'">
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="notificationInfo.name"
|
||||
@@ -379,6 +424,7 @@ function onClose() {
|
||||
:placeholder="t('notification.name')"
|
||||
:hint="t('notification.nameHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-label"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -387,13 +433,35 @@ function onClose() {
|
||||
:label="t('notification.webpush.username')"
|
||||
:hint="t('notification.webpush.usernameHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-account"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow v-else>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="notificationInfo.type"
|
||||
:label="t('notification.type')"
|
||||
:hint="t('notification.customTypeHint')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-cog"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="notificationInfo.name"
|
||||
:label="t('notification.name')"
|
||||
:hint="t('notification.nameRequired')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-label"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
<VCardActions class="pt-3">
|
||||
<VBtn @click="saveNotificationInfo" variant="elevated" prepend-icon="mdi-content-save" class="px-5">
|
||||
<VBtn @click="saveNotificationInfo" prepend-icon="mdi-content-save" class="px-5">
|
||||
{{ t('common.confirm') }}
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
|
||||
@@ -82,9 +82,7 @@ function goPersonDetail() {
|
||||
}"
|
||||
@click.stop="goPersonDetail"
|
||||
>
|
||||
<div
|
||||
class="person-card relative transform-gpu cursor-pointer rounded transition duration-150 ease-in-out scale-100 ring-gray-700"
|
||||
>
|
||||
<div class="person-card relative cursor-pointer ring-gray-700">
|
||||
<div style="padding-block-end: 150%">
|
||||
<div class="absolute inset-0 flex h-full w-full flex-col items-center p-2">
|
||||
<div class="relative mt-2 mb-4 flex h-1/2 w-full justify-center">
|
||||
@@ -100,10 +98,7 @@ function goPersonDetail() {
|
||||
<div class="w-full truncate text-center font-bold">
|
||||
{{ getPersonName() }}
|
||||
</div>
|
||||
<div
|
||||
class="overflow-hidden whitespace-normal text-center text-sm"
|
||||
style="display: -webkit-box; overflow: hidden; -webkit-box-orient: vertical; -webkit-line-clamp: 2"
|
||||
>
|
||||
<div class="overflow-hidden whitespace-normal text-center text-sm text-ellipsis line-clamp-2">
|
||||
{{ getPersonCharacter() }}
|
||||
</div>
|
||||
<div class="absolute bottom-0 left-0 right-0 h-12 rounded-b" />
|
||||
|
||||
@@ -36,7 +36,17 @@ const $toast = useToast()
|
||||
const progressDialog = ref(false)
|
||||
|
||||
// 进度框文本
|
||||
const progressText = ref('正在安装插件...')
|
||||
const progressText = ref('')
|
||||
|
||||
// 获取当前插件的标签
|
||||
const pluginLabels = computed(() => {
|
||||
if (!props.plugin?.plugin_label) return []
|
||||
|
||||
return props.plugin.plugin_label
|
||||
.split(',')
|
||||
.map(tag => tag.trim())
|
||||
.filter(tag => tag.length > 0)
|
||||
})
|
||||
|
||||
// 图片是否加载完成
|
||||
const isImageLoaded = ref(false)
|
||||
@@ -167,51 +177,77 @@ const dropdownItems = ref([
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="relative flex flex-row items-start pa-3 justify-between grow"
|
||||
:style="{ background: `${backgroundColor}` }"
|
||||
class="flex-grow"
|
||||
:style="`background: linear-gradient(rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0.5) 100%), linear-gradient(${backgroundColor} 0%, ${backgroundColor} 100%)`"
|
||||
>
|
||||
<div
|
||||
class="absolute inset-0 bg-cover bg-center"
|
||||
:style="{ background: `${backgroundColor}`, filter: 'brightness(0.5)' }"
|
||||
></div>
|
||||
<div class="relative flex-1 min-w-0">
|
||||
<VCardText class="px-2 pt-2 pb-0">
|
||||
<VCardTitle
|
||||
class="text-white text-lg px-2 text-shadow whitespace-nowrap overflow-hidden text-ellipsis ..."
|
||||
class="text-white px-2 pb-0 text-lg text-shadow whitespace-nowrap overflow-hidden text-ellipsis"
|
||||
>
|
||||
{{ props.plugin?.plugin_name }}
|
||||
<span class="text-sm text-gray-200">v{{ props.plugin?.plugin_version }}</span>
|
||||
<span class="text-sm mt-1 text-gray-200"> v{{ props.plugin?.plugin_version }} </span>
|
||||
</VCardTitle>
|
||||
<VCardText class="text-white text-sm px-2 py-0 text-shadow overflow-hidden line-clamp-3 ...">
|
||||
{{ props.plugin?.plugin_desc }}
|
||||
</VCardText>
|
||||
</div>
|
||||
<div class="relative flex-shrink-0 self-center">
|
||||
<VAvatar size="64">
|
||||
<VImg
|
||||
ref="imageRef"
|
||||
:src="iconPath"
|
||||
aspect-ratio="4/3"
|
||||
cover
|
||||
@load="imageLoaded"
|
||||
@error="imageLoadError = true"
|
||||
/>
|
||||
</VAvatar>
|
||||
</VCardText>
|
||||
<div class="relative flex flex-row items-start px-2 justify-between grow">
|
||||
<div class="relative flex-1 min-w-0">
|
||||
<div
|
||||
class="text-white text-sm px-2 py-1 text-shadow overflow-hidden ..."
|
||||
:class="{ 'line-clamp-3': !props.plugin?.plugin_label, 'line-clamp-2': props.plugin?.plugin_label }"
|
||||
>
|
||||
{{ props.plugin?.plugin_desc }}
|
||||
</div>
|
||||
<!-- 插件标签 -->
|
||||
<div v-if="pluginLabels.length > 0" class="plugin-app-card__tags-section px-2">
|
||||
<VChip
|
||||
v-for="tag in pluginLabels"
|
||||
:key="tag"
|
||||
size="x-small"
|
||||
variant="tonal"
|
||||
color="info"
|
||||
class="me-1 mb-1"
|
||||
tile
|
||||
>
|
||||
{{ tag }}
|
||||
</VChip>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative flex-shrink-0 self-center pb-3">
|
||||
<VAvatar size="48">
|
||||
<VImg
|
||||
ref="imageRef"
|
||||
:src="iconPath"
|
||||
aspect-ratio="4/3"
|
||||
cover
|
||||
@load="imageLoaded"
|
||||
@error="imageLoadError = true"
|
||||
/>
|
||||
</VAvatar>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<VCardText class="flex flex-none align-self-baseline py-3 w-full align-end">
|
||||
<span>
|
||||
<VIcon icon="mdi-github" class="me-1" />
|
||||
<a :href="props.plugin?.author_url" target="_blank" @click.stop>
|
||||
{{ props.plugin?.plugin_author }}
|
||||
</a>
|
||||
</span>
|
||||
<span v-if="props.count" class="ms-3">
|
||||
<VIcon icon="mdi-download" />
|
||||
<span class="text-sm ms-1 mt-1">{{ props.count?.toLocaleString() }}</span>
|
||||
</span>
|
||||
<div class="me-n3 absolute bottom-1 right-3">
|
||||
<VCardText
|
||||
class="flex flex-col align-self-baseline justify-between px-2 py-2 w-full overflow-hidden max-h-10 min-h-10"
|
||||
>
|
||||
<div class="flex flex-nowrap items-center w-full pe-10">
|
||||
<div class="flex flex-nowrap max-w-40 items-center align-middle">
|
||||
<VIcon icon="mdi-github" class="me-1" />
|
||||
<a
|
||||
class="overflow-hidden text-ellipsis whitespace-nowrap"
|
||||
:href="props.plugin?.author_url"
|
||||
target="_blank"
|
||||
@click.stop
|
||||
>
|
||||
{{ props.plugin?.plugin_author }}
|
||||
</a>
|
||||
</div>
|
||||
<div v-if="props.count" class="ms-2 flex-shrink-0 download-count align-middle items-center">
|
||||
<VIcon size="small" icon="mdi-download" />
|
||||
<span class="text-sm">{{ props.count?.toLocaleString() }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="absolute bottom-0 right-0">
|
||||
<IconBtn>
|
||||
<VIcon icon="mdi-dots-vertical" />
|
||||
<VIcon size="small" icon="mdi-dots-vertical" />
|
||||
<VMenu activator="parent" close-on-content-click>
|
||||
<VList>
|
||||
<VListItem v-for="(item, i) in dropdownItems" v-show="item.show" :key="i" @click="item.props.click">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import { useConfirm } from 'vuetify-use-dialog'
|
||||
import { useConfirm } from '@/composables/useConfirm'
|
||||
import api from '@/api'
|
||||
import type { Plugin } from '@/api/types'
|
||||
import { isNullOrEmptyObject } from '@core/utils'
|
||||
@@ -10,7 +10,12 @@ import VersionHistory from '@/components/misc/VersionHistory.vue'
|
||||
import ProgressDialog from '../dialog/ProgressDialog.vue'
|
||||
import PluginConfigDialog from '../dialog/PluginConfigDialog.vue'
|
||||
import PluginDataDialog from '../dialog/PluginDataDialog.vue'
|
||||
import LoggingView from '@/views/system/LoggingView.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useDisplay } from 'vuetify'
|
||||
|
||||
// 显示器宽度
|
||||
const display = useDisplay()
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
@@ -54,6 +59,9 @@ const progressDialog = ref(false)
|
||||
// 插件数据页面
|
||||
const pluginInfoDialog = ref(false)
|
||||
|
||||
// 实时日志弹窗
|
||||
const loggingDialog = ref(false)
|
||||
|
||||
// 进度框文本
|
||||
const progressText = ref('正在更新插件...')
|
||||
|
||||
@@ -69,6 +77,18 @@ const imageLoadError = ref(false)
|
||||
// 更新日志弹窗
|
||||
const releaseDialog = ref(false)
|
||||
|
||||
// 插件分身对话框
|
||||
const pluginCloneDialog = ref(false)
|
||||
|
||||
// 插件分身表单
|
||||
const cloneForm = ref({
|
||||
suffix: '',
|
||||
name: '',
|
||||
description: '',
|
||||
version: '',
|
||||
icon: '',
|
||||
})
|
||||
|
||||
// 监听动作标识,如为true则打开详情
|
||||
watch(
|
||||
() => props.action,
|
||||
@@ -120,7 +140,12 @@ async function uninstallPlugin() {
|
||||
// 通知父组件刷新
|
||||
emit('remove')
|
||||
} else {
|
||||
$toast.error(t('plugin.uninstallFailed', { name: props.plugin?.plugin_name, message: result.message }))
|
||||
$toast.error(
|
||||
t('plugin.uninstallFailed', {
|
||||
name: props.plugin?.plugin_name,
|
||||
message: result.message,
|
||||
}),
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
@@ -174,7 +199,12 @@ async function resetPlugin() {
|
||||
// 通知父组件刷新
|
||||
emit('save')
|
||||
} else {
|
||||
$toast.error(t('plugin.resetFailed', { name: props.plugin?.plugin_name, message: result.message }))
|
||||
$toast.error(
|
||||
t('plugin.resetFailed', {
|
||||
name: props.plugin?.plugin_name,
|
||||
message: result.message,
|
||||
}),
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
@@ -205,7 +235,12 @@ async function updatePlugin() {
|
||||
// 通知父组件刷新
|
||||
emit('save')
|
||||
} else {
|
||||
$toast.error(t('plugin.updateFailed', { name: props.plugin?.plugin_name, message: result.message }))
|
||||
$toast.error(
|
||||
t('plugin.updateFailed', {
|
||||
name: props.plugin?.plugin_name,
|
||||
message: result.message,
|
||||
}),
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
@@ -237,6 +272,54 @@ function configDone() {
|
||||
emit('save')
|
||||
}
|
||||
|
||||
// 显示插件分身对话框
|
||||
function showPluginClone() {
|
||||
cloneForm.value = {
|
||||
suffix: '',
|
||||
name: t('plugin.cloneDefaultName', { name: props.plugin?.plugin_name }),
|
||||
description: t('plugin.cloneDefaultDescription', { description: props.plugin?.plugin_desc }),
|
||||
version: props.plugin?.plugin_version || '1.0',
|
||||
icon: props.plugin?.plugin_icon || '',
|
||||
}
|
||||
pluginCloneDialog.value = true
|
||||
}
|
||||
|
||||
// 执行插件分身
|
||||
async function executePluginClone() {
|
||||
if (!cloneForm.value.suffix.trim()) {
|
||||
$toast.error(t('plugin.suffixRequired'))
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
progressDialog.value = true
|
||||
progressText.value = t('plugin.cloning', { name: props.plugin?.plugin_name })
|
||||
|
||||
const result: { [key: string]: any } = await api.post(`plugin/clone/${props.plugin?.id}`, {
|
||||
suffix: cloneForm.value.suffix.trim(),
|
||||
name: cloneForm.value.name.trim(),
|
||||
description: cloneForm.value.description.trim(),
|
||||
version: cloneForm.value.version.trim(),
|
||||
icon: cloneForm.value.icon.trim(),
|
||||
})
|
||||
|
||||
progressDialog.value = false
|
||||
|
||||
if (result.success) {
|
||||
$toast.success(t('plugin.cloneSuccess', { name: cloneForm.value.name }))
|
||||
pluginCloneDialog.value = false
|
||||
// 通知父组件刷新
|
||||
emit('remove')
|
||||
} else {
|
||||
$toast.error(t('plugin.cloneFailed', { message: result.message }))
|
||||
}
|
||||
} catch (error) {
|
||||
progressDialog.value = false
|
||||
$toast.error(t('plugin.cloneFailedGeneral'))
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 弹出菜单
|
||||
const dropdownItems = ref([
|
||||
{
|
||||
@@ -257,6 +340,16 @@ const dropdownItems = ref([
|
||||
click: showPluginConfig,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('plugin.clone'),
|
||||
value: 8,
|
||||
show: true,
|
||||
props: {
|
||||
prependIcon: 'mdi-content-copy',
|
||||
color: 'info',
|
||||
click: showPluginClone,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('plugin.update'),
|
||||
value: 3,
|
||||
@@ -294,7 +387,7 @@ const dropdownItems = ref([
|
||||
props: {
|
||||
prependIcon: 'mdi-file-document-outline',
|
||||
click: () => {
|
||||
openLoggerWindow()
|
||||
loggingDialog.value = true
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -328,7 +421,7 @@ watch(
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="h-full">
|
||||
<!-- 插件卡片 -->
|
||||
<VHover>
|
||||
<template #default="hover">
|
||||
@@ -344,50 +437,61 @@ watch(
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="relative flex flex-row items-start pa-3 justify-between grow"
|
||||
:style="{ background: `${backgroundColor}` }"
|
||||
class="flex-grow"
|
||||
:style="`background: linear-gradient(rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0.5) 100%), linear-gradient(${backgroundColor} 0%, ${backgroundColor} 100%)`"
|
||||
>
|
||||
<div
|
||||
class="absolute inset-0 bg-cover bg-center"
|
||||
:style="{ background: `${backgroundColor}`, filter: 'brightness(0.5)' }"
|
||||
/>
|
||||
<div class="relative flex-1 min-w-0">
|
||||
<VCardTitle class="text-white text-lg px-2 text-shadow whitespace-nowrap overflow-hidden text-ellipsis">
|
||||
<VBadge v-if="props.plugin?.state" dot inline color="success" />
|
||||
<VCardText class="px-2 pt-2 pb-0">
|
||||
<VCardTitle
|
||||
class="text-white px-2 pb-0 text-lg text-shadow whitespace-nowrap overflow-hidden text-ellipsis"
|
||||
>
|
||||
<VBadge dot inline :color="props.plugin?.state ? 'success' : 'secondary'" />
|
||||
{{ props.plugin?.plugin_name }}
|
||||
<span class="text-sm mt-1 text-gray-200"> v{{ props.plugin?.plugin_version }} </span>
|
||||
</VCardTitle>
|
||||
<VCardText class="px-2 py-0 text-white text-sm text-shadow overflow-hidden line-clamp-3 ...">
|
||||
{{ props.plugin?.plugin_desc }}
|
||||
</VCardText>
|
||||
</div>
|
||||
<div class="relative flex-shrink-0 self-center cursor-move">
|
||||
<VAvatar size="64">
|
||||
<VImg
|
||||
ref="imageRef"
|
||||
:src="iconPath"
|
||||
aspect-ratio="4/3"
|
||||
cover
|
||||
@load="imageLoaded"
|
||||
@error="imageLoadError = true"
|
||||
/>
|
||||
</VAvatar>
|
||||
</VCardText>
|
||||
<div class="relative flex flex-row items-start px-2 justify-between grow">
|
||||
<div class="relative flex-1 min-w-0">
|
||||
<div class="px-2 py-1 text-white text-sm text-shadow overflow-hidden line-clamp-3 ...">
|
||||
{{ props.plugin?.plugin_desc }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative flex-shrink-0 self-center pb-3" :class="{ 'cursor-move': display.mdAndUp.value }">
|
||||
<VAvatar size="48">
|
||||
<VImg
|
||||
ref="imageRef"
|
||||
:src="iconPath"
|
||||
aspect-ratio="4/3"
|
||||
cover
|
||||
@load="imageLoaded"
|
||||
@error="imageLoadError = true"
|
||||
/>
|
||||
</VAvatar>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<VCardText class="flex flex-none align-self-baseline py-3 w-full align-end">
|
||||
<span class="author-info">
|
||||
<VImg :src="authorPath" class="author-avatar" @load="isAvatarLoaded = true">
|
||||
<VIcon v-if="!isAvatarLoaded" icon="mdi-github" class="me-1" />
|
||||
</VImg>
|
||||
<a :href="props.plugin?.author_url" target="_blank" @click.stop>
|
||||
{{ props.plugin?.plugin_author }}
|
||||
</a>
|
||||
</span>
|
||||
<span v-if="props.count" class="ms-3">
|
||||
<VIcon icon="mdi-download" />
|
||||
<span class="text-sm ms-1 mt-1">{{ props.count?.toLocaleString() }}</span>
|
||||
</span>
|
||||
<div class="me-n3 absolute bottom-1 right-3">
|
||||
<VCardText
|
||||
class="flex flex-col align-self-baseline justify-between px-2 py-2 w-full overflow-hidden max-h-10 min-h-10"
|
||||
>
|
||||
<div class="flex flex-nowrap items-center w-full pe-10">
|
||||
<div class="flex flex-nowrap max-w-40 items-center align-middle">
|
||||
<VImg :src="authorPath" class="author-avatar" @load="isAvatarLoaded = true">
|
||||
<VIcon v-if="!isAvatarLoaded" size="small" icon="mdi-github" class="me-1" />
|
||||
</VImg>
|
||||
<a
|
||||
:href="props.plugin?.author_url"
|
||||
target="_blank"
|
||||
@click.stop
|
||||
class="overflow-hidden text-ellipsis whitespace-nowrap"
|
||||
>
|
||||
{{ props.plugin?.plugin_author }}
|
||||
</a>
|
||||
</div>
|
||||
<span v-if="props.count" class="ms-2 flex-shrink-0 download-count items-center align-middle">
|
||||
<VIcon size="small" icon="mdi-download" />
|
||||
<span class="text-sm">{{ props.count?.toLocaleString() }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="absolute bottom-0 right-0">
|
||||
<IconBtn>
|
||||
<VIcon icon="mdi-dots-vertical" />
|
||||
<VMenu v-model="menuVisible" activator="parent" close-on-content-click>
|
||||
@@ -439,7 +543,7 @@ watch(
|
||||
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" />
|
||||
|
||||
<!-- 更新日志 -->
|
||||
<VDialog v-if="releaseDialog" v-model="releaseDialog" width="600" max-height="85vh" scrollable>
|
||||
<VDialog v-if="releaseDialog" v-model="releaseDialog" width="600" scrollable :fullscreen="!display.mdAndUp.value">
|
||||
<VCard :title="t('plugin.updateHistoryTitle', { name: props.plugin?.plugin_name })">
|
||||
<VDialogCloseBtn @click="releaseDialog = false" />
|
||||
<VDivider />
|
||||
@@ -455,6 +559,144 @@ watch(
|
||||
</VCardItem>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
|
||||
<!-- 实时日志弹窗 -->
|
||||
<VDialog
|
||||
v-if="loggingDialog"
|
||||
v-model="loggingDialog"
|
||||
scrollable
|
||||
max-width="60rem"
|
||||
:fullscreen="!display.mdAndUp.value"
|
||||
>
|
||||
<VCard>
|
||||
<VDialogCloseBtn @click="loggingDialog = false" />
|
||||
<VCardItem>
|
||||
<VCardTitle class="d-inline-flex">
|
||||
<VIcon icon="mdi-file-document" class="me-2" />
|
||||
{{ t('plugin.logTitle') }}
|
||||
<a class="mx-2 d-inline-flex align-center cursor-pointer" @click="openLoggerWindow">
|
||||
<VChip color="grey-darken-1" size="small" class="ml-2">
|
||||
<VIcon icon="mdi-open-in-new" size="small" start />
|
||||
{{ t('common.openInNewWindow') }}
|
||||
</VChip>
|
||||
</a>
|
||||
</VCardTitle>
|
||||
</VCardItem>
|
||||
<VDivider />
|
||||
<VCardText>
|
||||
<LoggingView :logfile="`plugins/${props.plugin?.id?.toLowerCase()}.log`" />
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
|
||||
<!-- 插件分身对话框 -->
|
||||
<VDialog
|
||||
v-if="pluginCloneDialog"
|
||||
v-model="pluginCloneDialog"
|
||||
width="600"
|
||||
scrollable
|
||||
:fullscreen="!display.mdAndUp.value"
|
||||
>
|
||||
<VCard>
|
||||
<VCardItem class="py-2">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-content-copy" class="me-2" />
|
||||
</template>
|
||||
<VCardTitle>{{ t('plugin.cloneTitle') }}</VCardTitle>
|
||||
<VCardSubtitle>{{ t('plugin.cloneSubtitle', { name: props.plugin?.plugin_name }) }}</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VDialogCloseBtn @click="pluginCloneDialog = false" />
|
||||
<VDivider />
|
||||
<VCardText>
|
||||
<VForm>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="cloneForm.suffix"
|
||||
:label="t('plugin.suffix') + ' *'"
|
||||
:placeholder="t('plugin.suffixPlaceholder')"
|
||||
:hint="t('plugin.suffixHint')"
|
||||
persistent-hint
|
||||
:rules="[
|
||||
v => !!v || t('plugin.suffixRequired'),
|
||||
v => /^[a-zA-Z0-9]+$/.test(v) || t('plugin.suffixFormatError'),
|
||||
v => v.length <= 20 || t('plugin.suffixLengthError'),
|
||||
]"
|
||||
required
|
||||
prepend-inner-icon="mdi-tag"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="cloneForm.name"
|
||||
:label="t('plugin.cloneName')"
|
||||
:placeholder="t('plugin.cloneNamePlaceholder')"
|
||||
:hint="t('plugin.cloneNameHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-rename-box"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
v-model="cloneForm.description"
|
||||
:label="t('plugin.cloneDescriptionLabel')"
|
||||
:placeholder="t('plugin.cloneDescriptionPlaceholder')"
|
||||
:hint="t('plugin.cloneDescriptionHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-text"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="cloneForm.version"
|
||||
:label="t('plugin.cloneVersion')"
|
||||
:placeholder="t('plugin.cloneVersionPlaceholder')"
|
||||
:hint="t('plugin.cloneVersionHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-numeric"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="cloneForm.icon"
|
||||
:label="t('plugin.cloneIcon')"
|
||||
:placeholder="t('plugin.cloneIconPlaceholder')"
|
||||
:hint="t('plugin.cloneIconHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-image"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- 重要提醒 -->
|
||||
<VCol cols="12">
|
||||
<VAlert type="warning" variant="tonal" density="compact" class="mt-2" icon="mdi-alert-circle-outline">
|
||||
<div class="text-body-2">
|
||||
<strong>{{ t('common.notice') }}</strong
|
||||
>:{{ t('plugin.cloneNotice') }}
|
||||
</div>
|
||||
</VAlert>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
<VCardActions class="pt-3">
|
||||
<VSpacer />
|
||||
<VBtn
|
||||
color="primary"
|
||||
@click="executePluginClone"
|
||||
prepend-icon="mdi-content-copy"
|
||||
class="px-5"
|
||||
:disabled="!cloneForm.suffix.trim()"
|
||||
>
|
||||
{{ t('plugin.createClone') }}
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -469,11 +711,6 @@ watch(
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
.author-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.author-avatar {
|
||||
border-radius: 50%;
|
||||
block-size: 24px;
|
||||
|
||||
663
src/components/cards/PluginFolderCard.vue
Normal file
663
src/components/cards/PluginFolderCard.vue
Normal file
@@ -0,0 +1,663 @@
|
||||
<script lang="ts" setup>
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import { useConfirm } from '@/composables/useConfirm'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useDisplay } from 'vuetify'
|
||||
|
||||
// 文件夹配置接口
|
||||
interface FolderConfig {
|
||||
plugins?: string[]
|
||||
order?: number
|
||||
background?: string
|
||||
icon?: string
|
||||
color?: string
|
||||
gradient?: string
|
||||
showIcon?: boolean
|
||||
}
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
folderName: String,
|
||||
pluginCount: Number,
|
||||
folderConfig: {
|
||||
type: Object as PropType<FolderConfig>,
|
||||
default: () => ({}),
|
||||
},
|
||||
width: String,
|
||||
height: String,
|
||||
})
|
||||
|
||||
// 定义触发的自定义事件
|
||||
const emit = defineEmits(['open', 'delete', 'rename', 'update-config'])
|
||||
|
||||
// 多语言
|
||||
const { t } = useI18n()
|
||||
|
||||
// 响应式显示
|
||||
const display = useDisplay()
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
// 确认框
|
||||
const createConfirm = useConfirm()
|
||||
|
||||
// 菜单显示状态
|
||||
const menuVisible = ref(false)
|
||||
|
||||
// 重命名对话框
|
||||
const renameDialog = ref(false)
|
||||
|
||||
// 设置对话框
|
||||
const settingDialog = ref(false)
|
||||
|
||||
// 新名称
|
||||
const newFolderName = ref('')
|
||||
|
||||
// 默认颜色
|
||||
const defaultColor = '#2196F3'
|
||||
// 默认图标
|
||||
const defaultIcon = 'mdi-folder'
|
||||
// 默认渐变
|
||||
const defaultGradient =
|
||||
'linear-gradient(rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0.5) 100%), linear-gradient(135deg, rgba(33, 150, 243, 0.7) 0%, rgba(33, 150, 243, 0.8s) 100%)'
|
||||
|
||||
// 文件夹设置
|
||||
const folderSettings = ref<FolderConfig>({
|
||||
background: '',
|
||||
icon: defaultIcon,
|
||||
color: defaultColor,
|
||||
gradient: defaultGradient,
|
||||
showIcon: true,
|
||||
})
|
||||
|
||||
// 计算背景图片
|
||||
const backgroundImage = computed(() => {
|
||||
return props.folderConfig.background || folderSettings.value.background
|
||||
})
|
||||
|
||||
// 预设图标选项
|
||||
const iconOptions = [
|
||||
'mdi-folder',
|
||||
'mdi-folder-star',
|
||||
'mdi-folder-heart',
|
||||
'mdi-folder-cog',
|
||||
'mdi-folder-music',
|
||||
'mdi-folder-image',
|
||||
'mdi-folder-video',
|
||||
'mdi-folder-download',
|
||||
'mdi-folder-network',
|
||||
'mdi-folder-special',
|
||||
]
|
||||
|
||||
// 预设颜色选项
|
||||
const colorOptions = [
|
||||
'#2196F3', // 蓝色
|
||||
'#4CAF50', // 绿色
|
||||
'#FF9800', // 橙色
|
||||
'#9C27B0', // 紫色
|
||||
'#F44336', // 红色
|
||||
'#607D8B', // 蓝灰色
|
||||
'#795548', // 棕色
|
||||
'#E91E63', // 粉色
|
||||
]
|
||||
|
||||
// 预设渐变选项
|
||||
const gradientOptions = [
|
||||
'linear-gradient(rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0.4) 100%), linear-gradient(135deg, rgba(33, 150, 243, 0.7) 0%, rgba(33, 150, 243, 0.8) 100%)',
|
||||
'linear-gradient(rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0.4) 100%), linear-gradient(135deg, rgba(76, 175, 80, 0.7) 0%, rgba(76, 175, 80, 0.8) 100%)',
|
||||
'linear-gradient(rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0.4) 100%), linear-gradient(135deg, rgba(255, 152, 0, 0.7) 0%, rgba(255, 152, 0, 0.8) 100%)',
|
||||
'linear-gradient(rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0.4) 100%), linear-gradient(135deg, rgba(156, 39, 176, 0.7) 0%, rgba(156, 39, 176, 0.8) 100%)',
|
||||
'linear-gradient(rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0.4) 100%), linear-gradient(135deg, rgba(244, 67, 54, 0.7) 0%, rgba(244, 67, 54, 0.8) 100%)',
|
||||
'linear-gradient(rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0.4) 100%), linear-gradient(135deg, rgba(96, 125, 139, 0.7) 0%, rgba(96, 125, 139, 0.8) 100%)',
|
||||
'linear-gradient(rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0.4) 100%), linear-gradient(135deg, rgba(233, 30, 99, 0.7) 0%, rgba(233, 30, 99, 0.8) 100%)',
|
||||
'linear-gradient(rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0.4) 100%), linear-gradient(135deg, rgba(63, 81, 181, 0.7) 0%, rgba(156, 39, 176, 0.8) 100%)',
|
||||
]
|
||||
|
||||
// 计算背景渐变
|
||||
const backgroundGradient = computed(() => {
|
||||
const config = props.folderConfig || {}
|
||||
const settings = folderSettings.value
|
||||
|
||||
return config.gradient || settings.gradient || gradientOptions[0]
|
||||
})
|
||||
|
||||
// 计算图标
|
||||
const folderIcon = computed(() => {
|
||||
const config = props.folderConfig || {}
|
||||
const settings = folderSettings.value
|
||||
|
||||
return config.icon || settings.icon || defaultIcon
|
||||
})
|
||||
|
||||
// 计算图标颜色
|
||||
const iconColor = computed(() => {
|
||||
const config = props.folderConfig || {}
|
||||
const settings = folderSettings.value
|
||||
|
||||
return config.color || settings.color || defaultColor
|
||||
})
|
||||
|
||||
// 计算是否显示图标
|
||||
const shouldShowIcon = computed(() => {
|
||||
const config = props.folderConfig || {}
|
||||
const settings = folderSettings.value
|
||||
|
||||
return config.showIcon !== undefined ? config.showIcon : settings.showIcon !== undefined ? settings.showIcon : true
|
||||
})
|
||||
|
||||
// 监听props变化,更新本地设置
|
||||
watch(
|
||||
() => props.folderConfig,
|
||||
newConfig => {
|
||||
if (newConfig) {
|
||||
folderSettings.value = {
|
||||
...folderSettings.value,
|
||||
...newConfig,
|
||||
}
|
||||
}
|
||||
},
|
||||
{ deep: true, immediate: true },
|
||||
)
|
||||
|
||||
// 打开文件夹
|
||||
function openFolder() {
|
||||
emit('open', props.folderName)
|
||||
}
|
||||
|
||||
// 重命名文件夹
|
||||
function showRenameDialog() {
|
||||
newFolderName.value = props.folderName || ''
|
||||
renameDialog.value = true
|
||||
}
|
||||
|
||||
// 确认重命名
|
||||
async function confirmRename() {
|
||||
if (!newFolderName.value.trim()) {
|
||||
$toast.error(t('folder.folderNameCannotBeEmpty'))
|
||||
return
|
||||
}
|
||||
|
||||
if (newFolderName.value === props.folderName) {
|
||||
renameDialog.value = false
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
emit('rename', props.folderName, newFolderName.value)
|
||||
renameDialog.value = false
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 删除文件夹
|
||||
async function deleteFolder() {
|
||||
const isConfirmed = await createConfirm({
|
||||
title: t('common.confirm'),
|
||||
content: t('folder.confirmDeleteFolder', { folderName: props.folderName }),
|
||||
})
|
||||
|
||||
if (!isConfirmed) return
|
||||
|
||||
try {
|
||||
emit('delete', props.folderName)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 显示设置对话框
|
||||
function showSettingDialog() {
|
||||
folderSettings.value = {
|
||||
background: props.folderConfig?.background || '',
|
||||
icon: props.folderConfig?.icon || defaultIcon,
|
||||
color: props.folderConfig?.color || defaultColor,
|
||||
gradient: props.folderConfig?.gradient || gradientOptions[0],
|
||||
showIcon: props.folderConfig?.showIcon !== undefined ? props.folderConfig.showIcon : true,
|
||||
}
|
||||
settingDialog.value = true
|
||||
}
|
||||
|
||||
// 保存设置
|
||||
function saveSettings() {
|
||||
const config = {
|
||||
...props.folderConfig,
|
||||
...folderSettings.value,
|
||||
}
|
||||
|
||||
emit('update-config', props.folderName, config)
|
||||
settingDialog.value = false
|
||||
$toast.success(t('folder.folderSettingsSaved'))
|
||||
}
|
||||
|
||||
// 弹出菜单
|
||||
const dropdownItems = ref([
|
||||
{
|
||||
title: t('folder.settingAppearance'),
|
||||
value: 0,
|
||||
show: true,
|
||||
props: {
|
||||
prependIcon: 'mdi-palette',
|
||||
click: showSettingDialog,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('folder.rename'),
|
||||
value: 1,
|
||||
show: true,
|
||||
props: {
|
||||
prependIcon: 'mdi-pencil',
|
||||
click: showRenameDialog,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('folder.deleteFolder'),
|
||||
value: 2,
|
||||
show: true,
|
||||
props: {
|
||||
prependIcon: 'mdi-delete',
|
||||
color: 'error',
|
||||
click: deleteFolder,
|
||||
},
|
||||
},
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full">
|
||||
<!-- 文件夹卡片 -->
|
||||
<VHover>
|
||||
<template #default="hover">
|
||||
<VCard
|
||||
v-bind="hover.props"
|
||||
:ripple="false"
|
||||
:width="props.width"
|
||||
:height="props.height"
|
||||
min-height="8.5rem"
|
||||
@click="openFolder"
|
||||
class="plugin-folder-card h-full"
|
||||
:class="{
|
||||
'plugin-folder-card--mobile': display.mobile,
|
||||
'plugin-folder-card--hover': hover.isHovering,
|
||||
}"
|
||||
>
|
||||
<template v-if="backgroundImage" #image>
|
||||
<VImg :src="backgroundImage" cover position="top"> </VImg>
|
||||
</template>
|
||||
|
||||
<!-- 背景遮罩(当有背景图片时) -->
|
||||
<div v-if="backgroundImage" class="plugin-folder-card__overlay" />
|
||||
|
||||
<!-- 背景渐变层 -->
|
||||
<div v-else class="plugin-folder-card__bg" :style="{ background: backgroundGradient }" />
|
||||
|
||||
<!-- 卡片内容 -->
|
||||
<div class="plugin-folder-card__content">
|
||||
<!-- 主体内容 -->
|
||||
<div class="plugin-folder-card__body" :class="{ 'plugin-folder-card__body--no-icon': !shouldShowIcon }">
|
||||
<!-- 文件夹图标 -->
|
||||
<div v-if="shouldShowIcon" class="plugin-folder-card__icon-container">
|
||||
<VIcon
|
||||
:icon="folderIcon"
|
||||
:size="display.mobile ? 56 : 72"
|
||||
:color="iconColor"
|
||||
:class="{ 'cursor-move': display.mdAndUp.value }"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 文件夹信息 -->
|
||||
<div
|
||||
class="plugin-folder-card__info"
|
||||
:class="{ 'cursor-move': display.mdAndUp.value, 'plugin-folder-card__info--no-icon': !shouldShowIcon }"
|
||||
>
|
||||
<!-- 文件夹名称 -->
|
||||
<h3 class="plugin-folder-card__name">
|
||||
{{ props.folderName }}
|
||||
</h3>
|
||||
<!-- 插件数量 -->
|
||||
<p class="plugin-folder-card__count">{{ t('folder.pluginCount', { count: props.pluginCount }) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 更多菜单按钮 - 右下角 -->
|
||||
<div class="absolute top-0 right-0">
|
||||
<VMenu v-model="menuVisible" location="top end" :close-on-content-click="true">
|
||||
<template #activator="{ props: menuProps }">
|
||||
<IconBtn v-bind="menuProps" @click.stop>
|
||||
<VIcon size="small" icon="mdi-dots-vertical" class="text-white" />
|
||||
</IconBtn>
|
||||
</template>
|
||||
<VList>
|
||||
<VListItem
|
||||
v-for="(item, i) in dropdownItems"
|
||||
v-show="item.show"
|
||||
:key="i"
|
||||
:base-color="item.props.color"
|
||||
@click="item.props.click"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon :icon="item.props.prependIcon" size="16" />
|
||||
</template>
|
||||
<VListItemTitle class="text-body-2">{{ item.title }}</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</div>
|
||||
</div>
|
||||
</VCard>
|
||||
</template>
|
||||
</VHover>
|
||||
|
||||
<!-- 重命名对话框 -->
|
||||
<VDialog v-if="renameDialog" v-model="renameDialog" max-width="400">
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-pencil" class="me-2" />
|
||||
</template>
|
||||
<VCardTitle>{{ t('folder.renameFolder') }}</VCardTitle>
|
||||
</VCardItem>
|
||||
<VDialogCloseBtn @click="renameDialog = false" />
|
||||
<VDivider />
|
||||
<VCardText>
|
||||
<VTextField
|
||||
v-model="newFolderName"
|
||||
:label="t('folder.folderName')"
|
||||
variant="outlined"
|
||||
autofocus
|
||||
@keyup.enter="confirmRename"
|
||||
/>
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
<VBtn color="primary" prepend-icon="mdi-check" class="px-5" @click="confirmRename">确认</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
|
||||
<!-- 设置对话框 -->
|
||||
<VDialog
|
||||
v-if="settingDialog"
|
||||
v-model="settingDialog"
|
||||
max-width="600"
|
||||
scrollable
|
||||
:fullscreen="!display.mdAndUp.value"
|
||||
>
|
||||
<VCard>
|
||||
<VDialogCloseBtn @click="settingDialog = false" />
|
||||
<VCardItem>
|
||||
<VCardTitle>
|
||||
<VIcon icon="mdi-palette" class="mr-2" />
|
||||
{{ t('folder.folderAppearanceSettings') }}
|
||||
</VCardTitle>
|
||||
</VCardItem>
|
||||
<VDivider />
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<!-- 显示图标开关 -->
|
||||
<VCol cols="12">
|
||||
<VSwitch
|
||||
v-model="folderSettings.showIcon"
|
||||
:label="t('folder.showFolderIcon')"
|
||||
color="primary"
|
||||
hide-details
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- 图标选择 -->
|
||||
<VCol v-if="folderSettings.showIcon" cols="12" md="6">
|
||||
<VCardSubtitle class="pa-0 mb-2">{{ t('folder.icon') }}</VCardSubtitle>
|
||||
<div class="icon-grid">
|
||||
<VBtn
|
||||
v-for="icon in iconOptions"
|
||||
icon
|
||||
:key="icon"
|
||||
:variant="folderSettings.icon === icon ? 'tonal' : 'text'"
|
||||
:color="folderSettings.icon === icon ? 'primary' : 'default'"
|
||||
size="large"
|
||||
class="ma-1"
|
||||
@click="folderSettings.icon = icon"
|
||||
>
|
||||
<VIcon :icon="icon" size="24" />
|
||||
</VBtn>
|
||||
</div>
|
||||
</VCol>
|
||||
|
||||
<!-- 颜色选择 -->
|
||||
<VCol v-if="folderSettings.showIcon" cols="12" md="6">
|
||||
<VCardSubtitle class="pa-0 mb-2">{{ t('folder.iconColor') }}</VCardSubtitle>
|
||||
<div class="color-grid">
|
||||
<VBtn
|
||||
v-for="color in colorOptions"
|
||||
:key="color"
|
||||
:variant="folderSettings.color === color ? 'tonal' : 'text'"
|
||||
:color="color"
|
||||
size="large"
|
||||
class="ma-1 color-btn"
|
||||
:style="{ backgroundColor: color }"
|
||||
@click="folderSettings.color = color"
|
||||
>
|
||||
<VIcon v-if="folderSettings.color === color" icon="mdi-check" color="white" />
|
||||
</VBtn>
|
||||
</div>
|
||||
</VCol>
|
||||
|
||||
<!-- 渐变背景选择 -->
|
||||
<VCol cols="12">
|
||||
<VCardSubtitle class="pa-0 mb-2">{{ t('folder.backgroundGradient') }}</VCardSubtitle>
|
||||
<div class="gradient-grid">
|
||||
<VBtn
|
||||
v-for="(gradient, index) in gradientOptions"
|
||||
:key="index"
|
||||
:variant="folderSettings.gradient === gradient ? 'tonal' : 'text'"
|
||||
class="ma-1 gradient-btn"
|
||||
:style="{ background: gradient }"
|
||||
size="large"
|
||||
@click="folderSettings.gradient = gradient"
|
||||
>
|
||||
<VIcon v-if="folderSettings.gradient === gradient" icon="mdi-check" color="white" />
|
||||
</VBtn>
|
||||
</div>
|
||||
</VCol>
|
||||
|
||||
<!-- 自定义背景图片 -->
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
v-model="folderSettings.background"
|
||||
:label="t('folder.customBackgroundImageURL')"
|
||||
placeholder="https://example.com/image.jpg"
|
||||
variant="outlined"
|
||||
:hint="t('folder.customBackgroundImageHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-image"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
<VBtn color="primary" prepend-icon="mdi-content-save" class="px-5" @click="saveSettings">保存</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.plugin-folder-card {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
&--hover {
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
&__bg {
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
inset: 0;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&__overlay {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
background: rgba(0, 0, 0, 60%);
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
&__content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 16px;
|
||||
block-size: 100%;
|
||||
padding-block-end: 12px;
|
||||
|
||||
.plugin-folder-card--mobile & {
|
||||
padding: 12px;
|
||||
padding-block-end: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&__body {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 16px;
|
||||
padding-block: 0;
|
||||
padding-inline: 8px;
|
||||
|
||||
.plugin-folder-card--mobile & {
|
||||
gap: 12px;
|
||||
padding-block: 0;
|
||||
padding-inline: 4px;
|
||||
}
|
||||
|
||||
&--no-icon {
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
padding: 16px;
|
||||
gap: 0;
|
||||
|
||||
.plugin-folder-card--mobile & {
|
||||
padding: 12px;
|
||||
gap: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__icon-container {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&__info {
|
||||
flex: 1;
|
||||
min-block-size: 0;
|
||||
text-align: start;
|
||||
|
||||
&--no-icon {
|
||||
flex: none;
|
||||
text-align: start;
|
||||
}
|
||||
}
|
||||
|
||||
&__name {
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
-webkit-box-orient: vertical;
|
||||
color: white;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
-webkit-line-clamp: 1;
|
||||
line-clamp: 1;
|
||||
line-height: 1.3;
|
||||
max-inline-size: none;
|
||||
text-overflow: ellipsis;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 50%);
|
||||
|
||||
.plugin-folder-card--mobile & {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.plugin-folder-card__info--no-icon & {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 700;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
margin-block-end: 4px;
|
||||
|
||||
.plugin-folder-card--mobile & {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__count {
|
||||
color: white;
|
||||
font-size: 0.85rem;
|
||||
margin-block: 2px 0;
|
||||
margin-inline: 0;
|
||||
opacity: 0.9;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 50%);
|
||||
|
||||
.plugin-folder-card--mobile & {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.plugin-folder-card__info--no-icon & {
|
||||
font-size: 0.9rem;
|
||||
margin-block-start: 0;
|
||||
|
||||
.plugin-folder-card--mobile & {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 设置对话框样式
|
||||
.icon-grid {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(60px, 1fr));
|
||||
max-block-size: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.color-grid {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(60px, 1fr));
|
||||
}
|
||||
|
||||
.gradient-grid {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||
max-block-size: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.color-btn {
|
||||
border-radius: 8px !important;
|
||||
block-size: 60px !important;
|
||||
min-inline-size: 60px !important;
|
||||
}
|
||||
|
||||
.gradient-btn {
|
||||
border-radius: 8px !important;
|
||||
block-size: 60px !important;
|
||||
min-inline-size: 120px !important;
|
||||
}
|
||||
</style>
|
||||
183
src/components/cards/PluginMixedSortCard.vue
Normal file
183
src/components/cards/PluginMixedSortCard.vue
Normal file
@@ -0,0 +1,183 @@
|
||||
<script lang="ts" setup>
|
||||
import PluginCard from './PluginCard.vue'
|
||||
import PluginFolderCard from './PluginFolderCard.vue'
|
||||
|
||||
interface MixedSortItem {
|
||||
type: 'folder' | 'plugin'
|
||||
id: string
|
||||
data: any
|
||||
order: number
|
||||
}
|
||||
|
||||
interface Props {
|
||||
item: MixedSortItem
|
||||
pluginStatistics?: { [key: string]: number }
|
||||
pluginActions?: { [key: string]: boolean }
|
||||
showRemoveButton?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
pluginStatistics: () => ({}),
|
||||
pluginActions: () => ({}),
|
||||
showRemoveButton: false,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
openFolder: [folderName: string]
|
||||
deleteFolder: [folderName: string]
|
||||
renameFolder: [oldName: string, newName: string]
|
||||
updateFolderConfig: [folderName: string, config: any]
|
||||
refreshData: []
|
||||
actionDone: [pluginId: string]
|
||||
removeFromFolder: [pluginId: string]
|
||||
dropToFolder: [event: DragEvent, folderName: string]
|
||||
}>()
|
||||
|
||||
// 拖拽事件处理
|
||||
function handleDragOver(event: DragEvent) {
|
||||
// 只有当拖拽的是插件时才允许放入文件夹
|
||||
if (props.item.type === 'folder') {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
event.dataTransfer!.dropEffect = 'move'
|
||||
const target = event.currentTarget as HTMLElement
|
||||
target.classList.add('drag-over')
|
||||
}
|
||||
}
|
||||
|
||||
function handleDragEnter(event: DragEvent) {
|
||||
if (props.item.type === 'folder') {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}
|
||||
}
|
||||
|
||||
function handleDragLeave(event: DragEvent) {
|
||||
if (props.item.type === 'folder') {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
const target = event.currentTarget as HTMLElement
|
||||
target.classList.remove('drag-over')
|
||||
}
|
||||
}
|
||||
|
||||
function handleDropToFolder(event: DragEvent) {
|
||||
if (props.item.type === 'folder') {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
const target = event.currentTarget as HTMLElement
|
||||
target.classList.remove('drag-over')
|
||||
|
||||
emit('dropToFolder', event, props.item.id)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mixed-sort-card-wrapper h-full">
|
||||
<!-- 文件夹卡片 -->
|
||||
<div
|
||||
v-if="item.type === 'folder'"
|
||||
class="drop-zone h-full"
|
||||
:data-plugin-id="item.id"
|
||||
@dragover="handleDragOver"
|
||||
@dragenter="handleDragEnter"
|
||||
@dragleave="handleDragLeave"
|
||||
@drop="handleDropToFolder"
|
||||
>
|
||||
<PluginFolderCard
|
||||
:folder-name="item.data.name"
|
||||
:plugin-count="item.data.pluginCount"
|
||||
:folder-config="item.data.config"
|
||||
@open="$emit('openFolder', item.id)"
|
||||
@delete="$emit('deleteFolder', item.id)"
|
||||
@rename="(oldName, newName) => $emit('renameFolder', oldName, newName)"
|
||||
@update-config="(folderName, config) => $emit('updateFolderConfig', folderName, config)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 插件卡片 -->
|
||||
<div v-else-if="item.type === 'plugin'" class="plugin-item-wrapper h-full" :data-plugin-id="item.id">
|
||||
<PluginCard
|
||||
:count="pluginStatistics[item.id] || 0"
|
||||
:plugin="item.data"
|
||||
:action="pluginActions[item.id] || false"
|
||||
@remove="$emit('refreshData')"
|
||||
@save="$emit('refreshData')"
|
||||
@action-done="$emit('actionDone', item.id)"
|
||||
/>
|
||||
|
||||
<!-- 移出文件夹按钮(仅在文件夹内显示) -->
|
||||
<VBtn
|
||||
v-if="showRemoveButton"
|
||||
icon="mdi-folder-remove"
|
||||
variant="text"
|
||||
color="warning"
|
||||
size="small"
|
||||
class="remove-from-folder-btn"
|
||||
@click="$emit('removeFromFolder', item.id)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.mixed-sort-card-wrapper {
|
||||
block-size: 100%;
|
||||
inline-size: 100%;
|
||||
|
||||
// 确保拖拽时的边界清晰
|
||||
&.sortable-chosen {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&.sortable-ghost {
|
||||
border: 2px dashed #2196f3;
|
||||
border-radius: 16px;
|
||||
background: rgba(33, 150, 243, 10%);
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
// 拖拽相关样式
|
||||
.drop-zone {
|
||||
position: relative;
|
||||
isolation: isolate; // 创建新的层叠上下文
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&.drag-over {
|
||||
border: 2px dashed #2196f3;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 0 20px rgba(33, 150, 243, 50%);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
}
|
||||
|
||||
.plugin-item-wrapper {
|
||||
position: relative;
|
||||
isolation: isolate; // 创建新的层叠上下文
|
||||
|
||||
.remove-from-folder-btn {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
border-radius: 50%;
|
||||
backdrop-filter: blur(4px);
|
||||
background: rgba(255, 255, 255, 10%);
|
||||
inset-block-start: 4px;
|
||||
inset-inline-end: 4px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
&:hover .remove-from-folder-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 拖拽时的样式优化
|
||||
.mixed-sort-card-wrapper.sortable-drag {
|
||||
.remove-from-folder-btn {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -11,7 +11,11 @@ import api from '@/api'
|
||||
import type { Site, SiteStatistic, SiteUserData } from '@/api/types'
|
||||
import { isNullOrEmptyObject } from '@/@core/utils'
|
||||
import { formatFileSize } from '@/@core/utils/formatters'
|
||||
import { useConfirm } from 'vuetify-use-dialog'
|
||||
import { useConfirm } from '@/composables/useConfirm'
|
||||
import { useDisplay } from 'vuetify'
|
||||
|
||||
// 显示器宽度
|
||||
const display = useDisplay()
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
@@ -214,7 +218,7 @@ onMounted(() => {
|
||||
elevation="0"
|
||||
rounded="lg"
|
||||
hover
|
||||
@click="siteEditDialog = true"
|
||||
@click="handleResourceBrowse"
|
||||
>
|
||||
<!-- 装饰性状态指示器 -->
|
||||
<div v-if="cardProps.site?.is_active" class="site-status-indicator" :class="statColor"></div>
|
||||
@@ -224,7 +228,7 @@ onMounted(() => {
|
||||
<!-- 顶部:图标和站点名称 -->
|
||||
<div class="flex items-center mb-1">
|
||||
<!-- 站点图标 -->
|
||||
<VAvatar tile rounded="lg" size="32" class="me-2 cursor-move">
|
||||
<VAvatar tile rounded="lg" size="32" class="me-2" :class="{ 'cursor-move': display.mdAndUp.value }">
|
||||
<VImg :src="siteIcon" class="w-full h-full" :alt="cardProps.site?.name" cover>
|
||||
<template #placeholder>
|
||||
<div class="w-full h-full">
|
||||
@@ -335,11 +339,11 @@ onMounted(() => {
|
||||
<VIcon icon="mdi-dots-vertical" size="20" />
|
||||
<VMenu :activator="'parent'" :close-on-content-click="true" :location="'left'">
|
||||
<VList>
|
||||
<VListItem @click="handleResourceBrowse" base-color="info">
|
||||
<VListItem @click="siteEditDialog = true" base-color="info">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-web" size="20" />
|
||||
<VIcon icon="mdi-file-edit-outline" size="20" />
|
||||
</template>
|
||||
<VListItemTitle>{{ t('site.browseResources') }}</VListItemTitle>
|
||||
<VListItemTitle>{{ t('site.actions.edit') }}</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem @click="deleteSiteInfo">
|
||||
<template #prepend>
|
||||
|
||||
@@ -16,6 +16,10 @@ import { useToast } from 'vue-toast-notification'
|
||||
import { isNullOrEmptyObject } from '@/@core/utils'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { storageIconDict } from '@/api/constants'
|
||||
import { useDisplay } from 'vuetify'
|
||||
|
||||
// 显示器宽度
|
||||
const display = useDisplay()
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
@@ -200,9 +204,18 @@ function onClose() {
|
||||
@close="aListConfigDialog = false"
|
||||
@done="handleDone"
|
||||
/>
|
||||
<VDialog v-if="customConfigDialog" v-model="customConfigDialog" scrollable max-width="30rem">
|
||||
<VDialog
|
||||
v-if="customConfigDialog"
|
||||
v-model="customConfigDialog"
|
||||
scrollable
|
||||
max-width="30rem"
|
||||
:fullscreen="!display.mdAndUp.value"
|
||||
>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-cog" />
|
||||
</template>
|
||||
<VCardTitle>{{ t('storage.custom') }}</VCardTitle>
|
||||
<VDialogCloseBtn v-model="customConfigDialog" />
|
||||
</VCardItem>
|
||||
@@ -215,16 +228,21 @@ function onClose() {
|
||||
:label="t('storage.type')"
|
||||
:hint="t('storage.customTypeHint')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-database"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField v-model="customName" :label="t('storage.name')" persistent-hint active />
|
||||
<VTextField
|
||||
v-model="customName"
|
||||
:label="t('storage.name')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-label"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
<VCardActions class="pt-3">
|
||||
<VBtn @click="handleDone" variant="elevated" prepend-icon="mdi-content-save" class="px-5">
|
||||
<VBtn @click="handleDone" prepend-icon="mdi-content-save" class="px-5">
|
||||
{{ t('common.save') }}
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import { useConfirm } from 'vuetify-use-dialog'
|
||||
import { useConfirm } from '@/composables/useConfirm'
|
||||
import SubscribeEditDialog from '../dialog/SubscribeEditDialog.vue'
|
||||
import SubscribeFilesDialog from '../dialog/SubscribeFilesDialog.vue'
|
||||
import SubscribeShareDialog from '../dialog/SubscribeShareDialog.vue'
|
||||
@@ -9,6 +9,10 @@ import api from '@/api'
|
||||
import type { Subscribe } from '@/api/types'
|
||||
import router from '@/router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useDisplay } from 'vuetify'
|
||||
|
||||
// 显示器宽度
|
||||
const display = useDisplay()
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
@@ -296,99 +300,116 @@ function onSubscribeEditRemove() {
|
||||
<div>
|
||||
<VHover>
|
||||
<template #default="hover">
|
||||
<VCard
|
||||
v-bind="hover.props"
|
||||
:key="props.media?.id"
|
||||
class="flex flex-col h-full"
|
||||
<div
|
||||
class="w-full h-full rounded-lg overflow-hidden"
|
||||
:class="{
|
||||
'outline-dashed outline-1': props.media?.best_version && imageLoaded,
|
||||
'transition transform-cpu duration-300 -translate-y-1': hover.isHovering,
|
||||
'opacity-70': subscribeState === 'S',
|
||||
'outline-dashed outline-1': props.media?.best_version && imageLoaded,
|
||||
}"
|
||||
min-height="170"
|
||||
@click="editSubscribeDialog"
|
||||
:ripple="false"
|
||||
>
|
||||
<div class="me-n3 absolute top-1 right-2">
|
||||
<IconBtn>
|
||||
<VIcon icon="mdi-dots-vertical" color="white" />
|
||||
<VMenu activator="parent" close-on-content-click>
|
||||
<VList>
|
||||
<template v-for="(item, i) in dropdownItems" :key="i">
|
||||
<VListItem v-if="item.show !== false" :base-color="item.props.color" @click="item.props.click">
|
||||
<template #prepend>
|
||||
<VIcon :icon="item.props.prependIcon" />
|
||||
</template>
|
||||
<VListItemTitle v-text="item.title" />
|
||||
</VListItem>
|
||||
</template>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</div>
|
||||
<template #image>
|
||||
<VImg :src="backdropUrl || posterUrl" aspect-ratio="3/2" cover @load="imageLoadHandler" position="top">
|
||||
<template #placeholder>
|
||||
<div class="w-full h-full">
|
||||
<VSkeletonLoader class="object-cover aspect-w-3 aspect-h-2" />
|
||||
</div>
|
||||
</template>
|
||||
<div class="absolute inset-0 subscribe-card-background"></div>
|
||||
</VImg>
|
||||
<div v-if="subscribeState === 'P'" class="absolute inset-0 bg-yellow-900 opacity-80 pointer-events-none" />
|
||||
</template>
|
||||
<div>
|
||||
<VCardText class="flex items-center py-3">
|
||||
<div class="h-auto w-16 flex-shrink-0 overflow-hidden rounded-md cursor-move" v-if="imageLoaded">
|
||||
<VImg :src="posterUrl" aspect-ratio="2/3" cover>
|
||||
<template #placeholder>
|
||||
<div class="w-full h-full">
|
||||
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
|
||||
</div>
|
||||
</template>
|
||||
</VImg>
|
||||
</div>
|
||||
<div class="flex flex-col justify-center overflow-hidden pl-2 xl:pl-4">
|
||||
<div class="text-sm font-medium text-white sm:pt-1">{{ props.media?.year }}</div>
|
||||
<div class="mr-2 min-w-0 text-lg font-bold text-white">
|
||||
{{ props.media?.name }}
|
||||
{{ formatSeason(props.media?.season ? props.media?.season.toString() : '') }}
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
<VCardText class="flex justify-space-between align-center flex-wrap py-3">
|
||||
<div class="flex align-center">
|
||||
<IconBtn
|
||||
v-if="props.media?.total_episode"
|
||||
v-bind="props"
|
||||
icon="mdi-progress-download"
|
||||
color="white"
|
||||
class="me-1"
|
||||
/>
|
||||
<div v-if="props.media?.season" class="text-subtitle-2 me-4 text-white">
|
||||
{{ (props.media?.total_episode || 0) - (props.media?.lack_episode || 0) }} /
|
||||
{{ props.media?.total_episode }}
|
||||
</div>
|
||||
<IconBtn v-if="props.media?.username" icon="mdi-account" color="white" class="me-1" />
|
||||
<span v-if="props.media?.username" class="text-subtitle-2 me-4 text-white">
|
||||
{{ 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>
|
||||
<div class="w-full absolute bottom-0">
|
||||
<VProgressLinear
|
||||
v-if="getPercentage() > 0"
|
||||
:model-value="getPercentage()"
|
||||
bg-color="success"
|
||||
color="success"
|
||||
/>
|
||||
<VCard
|
||||
v-bind="hover.props"
|
||||
:key="props.media?.id"
|
||||
class="flex flex-col h-full"
|
||||
:class="{
|
||||
'opacity-70': subscribeState === 'S',
|
||||
}"
|
||||
rounded="0"
|
||||
min-height="150"
|
||||
@click="editSubscribeDialog"
|
||||
:ripple="false"
|
||||
>
|
||||
<div class="me-n3 absolute top-1 right-4">
|
||||
<IconBtn>
|
||||
<VIcon icon="mdi-dots-vertical" color="white" />
|
||||
<VMenu activator="parent" close-on-content-click>
|
||||
<VList>
|
||||
<template v-for="(item, i) in dropdownItems" :key="i">
|
||||
<VListItem v-if="item.show !== false" :base-color="item.props.color" @click="item.props.click">
|
||||
<template #prepend>
|
||||
<VIcon :icon="item.props.prependIcon" />
|
||||
</template>
|
||||
<VListItemTitle v-text="item.title" />
|
||||
</VListItem>
|
||||
</template>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</div>
|
||||
</div>
|
||||
</VCard>
|
||||
<template #image>
|
||||
<VImg :src="backdropUrl || posterUrl" aspect-ratio="3/2" cover @load="imageLoadHandler" position="top">
|
||||
<template #placeholder>
|
||||
<div class="w-full h-full">
|
||||
<VSkeletonLoader class="object-cover aspect-w-3 aspect-h-2" />
|
||||
</div>
|
||||
</template>
|
||||
<div class="absolute inset-0 outline-none subscribe-card-background"></div>
|
||||
</VImg>
|
||||
<div
|
||||
v-if="subscribeState === 'P'"
|
||||
class="absolute inset-0 bg-yellow-900 opacity-80 pointer-events-none"
|
||||
/>
|
||||
</template>
|
||||
<div>
|
||||
<VCardText class="flex items-center pt-3 pb-2">
|
||||
<div
|
||||
class="h-auto w-12 flex-shrink-0 overflow-hidden rounded-md"
|
||||
v-if="imageLoaded"
|
||||
:class="{ 'cursor-move': display.mdAndUp.value }"
|
||||
>
|
||||
<VImg :src="posterUrl" aspect-ratio="2/3" cover>
|
||||
<template #placeholder>
|
||||
<div class="w-full h-full">
|
||||
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
|
||||
</div>
|
||||
</template>
|
||||
</VImg>
|
||||
</div>
|
||||
<div class="flex flex-col justify-center overflow-hidden pl-2 xl:pl-4">
|
||||
<div class="text-sm font-medium text-white sm:pt-1">{{ props.media?.year }}</div>
|
||||
<div class="mr-2 min-w-0 text-lg font-bold text-white text-ellipsis overflow-hidden line-clamp-2 ...">
|
||||
{{ props.media?.name }}
|
||||
{{ formatSeason(props.media?.season ? props.media?.season.toString() : '') }}
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
<VCardText class="flex justify-space-between align-center flex-wrap px-3">
|
||||
<div class="flex align-center">
|
||||
<IconBtn
|
||||
v-if="props.media?.total_episode"
|
||||
size="small"
|
||||
v-bind="props"
|
||||
icon="mdi-progress-download"
|
||||
color="white"
|
||||
/>
|
||||
<div v-if="props.media?.season" class="text-subtitle-2 me-2 text-white">
|
||||
{{ (props.media?.total_episode || 0) - (props.media?.lack_episode || 0) }} /
|
||||
{{ props.media?.total_episode }}
|
||||
</div>
|
||||
<IconBtn v-if="props.media?.username" icon="mdi-account" size="small" color="white" />
|
||||
<span v-if="props.media?.username" class="text-subtitle-2 text-white">
|
||||
{{ 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 text-xs"
|
||||
>
|
||||
<VIcon icon="mdi-download" class="me-1" />
|
||||
{{ lastUpdateText }}
|
||||
</VCardText>
|
||||
<div class="w-full absolute bottom-0">
|
||||
<VProgressLinear
|
||||
v-if="getPercentage() > 0"
|
||||
:model-value="getPercentage()"
|
||||
bg-color="success"
|
||||
color="success"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</VCard>
|
||||
</div>
|
||||
</template>
|
||||
</VHover>
|
||||
<!-- 订阅编辑弹窗 -->
|
||||
|
||||
@@ -97,64 +97,69 @@ function doDelete() {
|
||||
<div class="h-full">
|
||||
<VHover>
|
||||
<template #default="hover">
|
||||
<VCard
|
||||
v-bind="hover.props"
|
||||
:key="props.media?.id"
|
||||
class="flex flex-col h-full"
|
||||
<div
|
||||
class="w-full h-full rounded-lg overflow-hidden"
|
||||
:class="{
|
||||
'transition transform-cpu duration-300 -translate-y-1': hover.isHovering,
|
||||
}"
|
||||
min-height="170"
|
||||
@click="showForkSubscribe"
|
||||
>
|
||||
<template #image>
|
||||
<VImg :src="backdropUrl || posterUrl" aspect-ratio="3/2" cover @load="imageLoadHandler" position="top">
|
||||
<template #placeholder>
|
||||
<div class="w-full h-full">
|
||||
<VSkeletonLoader class="object-cover aspect-w-3 aspect-h-2" />
|
||||
<VCard
|
||||
v-bind="hover.props"
|
||||
:key="props.media?.id"
|
||||
class="flex flex-col h-full"
|
||||
rounded="0"
|
||||
min-height="150"
|
||||
@click="showForkSubscribe"
|
||||
>
|
||||
<template #image>
|
||||
<VImg :src="backdropUrl || posterUrl" aspect-ratio="3/2" cover @load="imageLoadHandler" position="top">
|
||||
<template #placeholder>
|
||||
<div class="w-full h-full">
|
||||
<VSkeletonLoader class="object-cover aspect-w-3 aspect-h-2" />
|
||||
</div>
|
||||
</template>
|
||||
<div class="absolute inset-0 subscribe-card-background"></div>
|
||||
</VImg>
|
||||
</template>
|
||||
<div class="h-full flex flex-col">
|
||||
<VCardText class="flex items-center pa-3 pb-1 grow">
|
||||
<div class="h-auto w-16 flex-shrink-0 overflow-hidden rounded-md" v-if="imageLoaded">
|
||||
<VImg :src="posterUrl" aspect-ratio="2/3" cover @click.stop="viewMediaDetail">
|
||||
<template #placeholder>
|
||||
<div class="w-full h-full">
|
||||
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
|
||||
</div>
|
||||
</template>
|
||||
</VImg>
|
||||
</div>
|
||||
</template>
|
||||
<div class="absolute inset-0 subscribe-card-background"></div>
|
||||
</VImg>
|
||||
</template>
|
||||
<div class="h-full flex flex-col">
|
||||
<VCardText class="flex items-center pb-1 grow">
|
||||
<div class="h-auto w-12 flex-shrink-0 overflow-hidden rounded-md" v-if="imageLoaded">
|
||||
<VImg :src="posterUrl" aspect-ratio="2/3" cover @click.stop="viewMediaDetail">
|
||||
<template #placeholder>
|
||||
<div class="w-full h-full">
|
||||
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
|
||||
</div>
|
||||
</template>
|
||||
</VImg>
|
||||
</div>
|
||||
<div class="flex flex-col justify-center pl-2 xl:pl-4">
|
||||
<div class="mr-2 min-w-0 text-lg font-bold text-white line-clamp-2 overflow-hidden text-ellipsis ...">
|
||||
{{ props.media?.share_title }}
|
||||
<div class="flex flex-col justify-center pl-2 xl:pl-4">
|
||||
<div class="mr-2 min-w-0 text-lg font-bold text-white line-clamp-2 overflow-hidden text-ellipsis ...">
|
||||
{{ props.media?.share_title }}
|
||||
</div>
|
||||
<div class="text-sm font-medium text-gray-200 sm:pt-1 line-clamp-3 overflow-hidden text-ellipsis ...">
|
||||
{{ props.media?.share_comment }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-sm font-medium text-gray-200 sm:pt-1 line-clamp-3 overflow-hidden text-ellipsis ...">
|
||||
{{ props.media?.share_comment }}
|
||||
</VCardText>
|
||||
<VCardText class="flex justify-space-between align-center flex-wrap py-2">
|
||||
<div class="flex align-center">
|
||||
<IconBtn v-bind="props" icon="mdi-account" color="white" class="me-1" />
|
||||
<div class="text-subtitle-2 me-4 text-white">
|
||||
{{ props.media?.share_user }}
|
||||
</div>
|
||||
<IconBtn v-if="props.media?.count" icon="mdi-fire" color="white" class="me-1" />
|
||||
<span v-if="props.media?.count" class="text-subtitle-2 me-4 text-white">
|
||||
{{ props.media?.count.toLocaleString() }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
<VCardText class="flex justify-space-between align-center flex-wrap">
|
||||
<div class="flex align-center">
|
||||
<IconBtn v-bind="props" icon="mdi-account" color="white" class="me-1" />
|
||||
<div class="text-subtitle-2 me-4 text-white">
|
||||
{{ props.media?.share_user }}
|
||||
</div>
|
||||
<IconBtn v-if="props.media?.count" icon="mdi-fire" color="white" class="me-1" />
|
||||
<span v-if="props.media?.count" class="text-subtitle-2 me-4 text-white">
|
||||
{{ props.media?.count.toLocaleString() }}
|
||||
</span>
|
||||
</div>
|
||||
</VCardText>
|
||||
<VCardText class="absolute right-0 bottom-0 d-flex align-center p-2 text-gray-300">
|
||||
<VIcon icon="mdi-calcdar" class="me-1" />
|
||||
{{ dateText }}
|
||||
</VCardText>
|
||||
</div>
|
||||
</VCard>
|
||||
</VCardText>
|
||||
<VCardText class="absolute right-0 bottom-0 d-flex align-center p-2 text-gray-300">
|
||||
<VIcon icon="mdi-calcdar" class="me-1" />
|
||||
{{ dateText }}
|
||||
</VCardText>
|
||||
</div>
|
||||
</VCard>
|
||||
</div>
|
||||
</template>
|
||||
</VHover>
|
||||
<!-- 订阅编辑弹窗 -->
|
||||
|
||||
@@ -283,7 +283,7 @@ onMounted(() => {
|
||||
v-for="(item, index) in props.more"
|
||||
:key="index"
|
||||
@click.stop="handleAddDownload(item)"
|
||||
class="border-b border-opacity-5 hover:bg-primary-lighten-5"
|
||||
class="hover:bg-primary-lighten-5"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<div class="d-flex align-center gap-1">
|
||||
|
||||
@@ -133,7 +133,12 @@ onMounted(() => {
|
||||
<VListItemTitle>
|
||||
<div class="d-flex flex-row flex-wrap align-center mb-2">
|
||||
<span class="text-h6 font-weight-bold me-2">{{ media?.title ?? meta?.name }}</span>
|
||||
<VChip v-if="meta?.season_episode" class="chip-season rounded-sm font-weight-bold" variant="elevated">
|
||||
<VChip
|
||||
v-if="meta?.season_episode"
|
||||
class="chip-season rounded-sm font-weight-bold"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
>
|
||||
{{ meta?.season_episode }}
|
||||
</VChip>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Subscribe, User } from '@/api/types'
|
||||
import { useUserStore } from '@/stores'
|
||||
import avatar1 from '@images/avatars/avatar-1.png'
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import { useConfirm } from 'vuetify-use-dialog'
|
||||
import { useConfirm } from '@/composables/useConfirm'
|
||||
import UserAddEditDialog from '@/components/dialog/UserAddEditDialog.vue'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { Workflow } from '@/api/types'
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import { useConfirm } from 'vuetify-use-dialog'
|
||||
import { useConfirm } from '@/composables/useConfirm'
|
||||
import WorkflowAddEditDialog from '@/components/dialog/WorkflowAddEditDialog.vue'
|
||||
import WorkflowActionsDialog from '@/components/dialog/WorkflowActionsDialog.vue'
|
||||
import api from '@/api'
|
||||
@@ -179,7 +179,13 @@ const resolveProgress = (item: Workflow) => {
|
||||
:loading="loading"
|
||||
:class="{ 'transition transform-cpu duration-300 -translate-y-1': hover.isHovering }"
|
||||
>
|
||||
<VCardItem class="py-3" :class="`bg-${resolveStatusVariant(workflow?.state).color}`">
|
||||
<VCardItem
|
||||
:class="{
|
||||
'py-1': workflow?.description,
|
||||
'py-3': !workflow?.description,
|
||||
[`bg-${resolveStatusVariant(workflow?.state).color}`]: true,
|
||||
}"
|
||||
>
|
||||
<template #prepend>
|
||||
<VAvatar variant="text" class="me-2">
|
||||
<VIcon
|
||||
|
||||
@@ -134,69 +134,75 @@ onMounted(() => {
|
||||
<template>
|
||||
<VDialog max-width="35rem" scrollable>
|
||||
<VCard>
|
||||
<VCardTitle class="py-4 me-12">
|
||||
<VIcon icon="mdi-download" class="me-2" />
|
||||
<span v-if="title">{{ torrent?.site_name }} - {{ title }}</span>
|
||||
<span v-else>{{ t('dialog.addDownload.confirmDownload') }}</span>
|
||||
</VCardTitle>
|
||||
<VCardItem class="py-2">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-monitor-arrow-down-variant" class="me-2" />
|
||||
</template>
|
||||
<VCardTitle>{{ t('dialog.addDownload.confirmDownload') }}</VCardTitle>
|
||||
<VCardSubtitle>{{ torrent?.site_name }} - {{ title }}</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VDialogCloseBtn @click="emit('close')" />
|
||||
<VDivider />
|
||||
<VList lines="one">
|
||||
<VListItem>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-web"></VIcon>
|
||||
</template>
|
||||
<VListItemTitle>
|
||||
<span class="whitespace-break-spaces me-2">{{ torrent?.title }}</span>
|
||||
<span class="text-green-700 ms-2 text-sm">↑{{ torrent?.seeders }}</span>
|
||||
<span class="text-orange-700 ms-2 text-sm">↓{{ torrent?.peers }}</span>
|
||||
</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem v-if="torrent?.description">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-subtitles-outline"></VIcon>
|
||||
</template>
|
||||
<VListItemTitle>
|
||||
<span class="text-body-2 whitespace-break-spaces">{{ torrent?.description }}</span>
|
||||
</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem v-if="torrent?.size">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-database"></VIcon>
|
||||
</template>
|
||||
<VListItemTitle>
|
||||
<span class="text-body-2">
|
||||
<VChip variant="tonal" label>
|
||||
{{ formatFileSize(torrent?.size || 0) }}
|
||||
</VChip>
|
||||
</span>
|
||||
</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
<VRow class="px-7">
|
||||
<VCol cols="12" md="4">
|
||||
<VSelect
|
||||
v-model="selectedDownloader"
|
||||
:items="downloaderOptions"
|
||||
size="small"
|
||||
:label="t('dialog.addDownload.downloader')"
|
||||
variant="underlined"
|
||||
:placeholder="t('dialog.addDownload.defaultPlaceholder')"
|
||||
density="compact"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="8">
|
||||
<VCombobox
|
||||
v-model="selectedDirectory"
|
||||
:items="targetDirectories"
|
||||
:label="t('dialog.addDownload.saveDirectory')"
|
||||
size="small"
|
||||
:placeholder="t('dialog.addDownload.autoPlaceholder')"
|
||||
variant="underlined"
|
||||
density="compact"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VCardText>
|
||||
<VList lines="one">
|
||||
<VListItem>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-web"></VIcon>
|
||||
</template>
|
||||
<VListItemTitle>
|
||||
<span class="whitespace-break-spaces me-2">{{ torrent?.title }}</span>
|
||||
<span class="text-green-700 ms-2 text-sm">↑{{ torrent?.seeders }}</span>
|
||||
<span class="text-orange-700 ms-2 text-sm">↓{{ torrent?.peers }}</span>
|
||||
</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem v-if="torrent?.description">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-subtitles-outline"></VIcon>
|
||||
</template>
|
||||
<VListItemTitle>
|
||||
<span class="text-body-2 whitespace-break-spaces">{{ torrent?.description }}</span>
|
||||
</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem v-if="torrent?.size">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-database"></VIcon>
|
||||
</template>
|
||||
<VListItemTitle>
|
||||
<span class="text-body-2">
|
||||
<VChip variant="tonal" label>
|
||||
{{ formatFileSize(torrent?.size || 0) }}
|
||||
</VChip>
|
||||
</span>
|
||||
</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
<VRow class="px-5">
|
||||
<VCol cols="12" md="6">
|
||||
<VSelect
|
||||
v-model="selectedDownloader"
|
||||
:items="downloaderOptions"
|
||||
size="small"
|
||||
:label="t('dialog.addDownload.downloader')"
|
||||
variant="underlined"
|
||||
:placeholder="t('dialog.addDownload.defaultPlaceholder')"
|
||||
density="comfortable"
|
||||
prepend-inner-icon="mdi-download"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VCombobox
|
||||
v-model="selectedDirectory"
|
||||
:items="targetDirectories"
|
||||
:label="t('dialog.addDownload.saveDirectory')"
|
||||
size="small"
|
||||
:placeholder="t('dialog.addDownload.autoPlaceholder')"
|
||||
variant="underlined"
|
||||
density="comfortable"
|
||||
prepend-inner-icon="mdi-folder"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
<VCardText class="text-center">
|
||||
<VBtn variant="elevated" :disabled="loading" @click="addDownload" :prepend-icon="icon" class="px-5">
|
||||
{{ buttonText }}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
<script lang="ts" setup>
|
||||
import api from '@/api'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useDisplay } from 'vuetify'
|
||||
|
||||
// 显示器宽度
|
||||
const display = useDisplay()
|
||||
|
||||
// 多语言支持
|
||||
const { t } = useI18n()
|
||||
@@ -28,17 +32,33 @@ async function handleReset() {
|
||||
const result: { [key: string]: any } = await api.get('/storage/reset/alist')
|
||||
if (result.success) {
|
||||
// 重置成功
|
||||
alertType.value = 'success'
|
||||
handleDone()
|
||||
} else {
|
||||
alertType.value = 'error'
|
||||
text.value = result.message
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
// 登录类型
|
||||
let loginType = ref('username')
|
||||
if (props.conf.token) {
|
||||
loginType = ref('token')
|
||||
} else if (props.conf.username) {
|
||||
loginType = ref('username')
|
||||
} else {
|
||||
loginType = ref('guest')
|
||||
}
|
||||
|
||||
// 数据源
|
||||
const sourceItems = [
|
||||
{
|
||||
'title': t('dialog.alistConfig.loginTypeOptions.username'),
|
||||
'value': 'username',
|
||||
},
|
||||
{ 'title': t('dialog.alistConfig.loginTypeOptions.token'), 'value': 'token' },
|
||||
{ 'title': t('dialog.alistConfig.loginTypeOptions.guest'), 'value': 'guest' },
|
||||
]
|
||||
|
||||
// 保存alist设置
|
||||
async function savaAlistConfig() {
|
||||
try {
|
||||
@@ -50,9 +70,18 @@ async function savaAlistConfig() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VDialog width="50rem" scrollable max-height="85vh">
|
||||
<VCard :title="t('dialog.alistConfig.title')">
|
||||
<VDialog width="50rem" scrollable :fullscreen="!display.mdAndUp.value">
|
||||
<VCard>
|
||||
<VDialogCloseBtn @click="emit('close')" />
|
||||
<VCardItem>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-cog-outline" class="me-2" />
|
||||
</template>
|
||||
<VCardTitle>
|
||||
{{ t('dialog.alistConfig.title') }}
|
||||
</VCardTitle>
|
||||
</VCardItem>
|
||||
<VDivider />
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
@@ -61,33 +90,55 @@ async function savaAlistConfig() {
|
||||
:hint="t('dialog.alistConfig.serverUrl')"
|
||||
:label="t('dialog.alistConfig.serverUrl')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-server"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VCol cols="12" md="4">
|
||||
<VSelect
|
||||
v-model="loginType"
|
||||
:items="sourceItems"
|
||||
:label="t('dialog.alistConfig.loginType')"
|
||||
:hint="t('dialog.alistConfig.loginType')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-login"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4" v-if="loginType == 'username'">
|
||||
<VTextField
|
||||
v-model="props.conf.username"
|
||||
:hint="t('dialog.alistConfig.username')"
|
||||
:label="t('dialog.alistConfig.username')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-account"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VCol cols="12" md="4" v-if="loginType == 'username'">
|
||||
<VTextField
|
||||
type="password"
|
||||
v-model="props.conf.password"
|
||||
:hint="t('dialog.alistConfig.password')"
|
||||
:label="t('dialog.alistConfig.password')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-lock"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="8" v-if="loginType == 'token'">
|
||||
<VTextField
|
||||
v-model="props.conf.token"
|
||||
:hint="t('dialog.alistConfig.loginTypeOptions.token')"
|
||||
:label="t('dialog.alistConfig.loginTypeOptions.token')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-key"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
<VBtn variant="tonal" color="error" @click="handleReset" prepend-icon="mdi-restore" class="px-5 me-3">
|
||||
<VBtn color="error" @click="handleReset" prepend-icon="mdi-restore" class="px-5 me-3">
|
||||
{{ t('dialog.alistConfig.reset') }}
|
||||
</VBtn>
|
||||
<VBtn variant="elevated" @click="handleDone" prepend-icon="mdi-check" class="px-5 me-3">
|
||||
<VSpacer />
|
||||
<VBtn @click="handleDone" prepend-icon="mdi-check" class="px-5 me-3">
|
||||
{{ t('dialog.alistConfig.complete') }}
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
<script lang="ts" setup>
|
||||
import api from '@/api'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useDisplay } from 'vuetify'
|
||||
|
||||
// 显示器宽度
|
||||
const display = useDisplay()
|
||||
|
||||
// 多语言支持
|
||||
const { t } = useI18n()
|
||||
@@ -106,11 +110,20 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VDialog width="40rem" scrollable max-height="85vh">
|
||||
<VCard :title="t('dialog.aliyunAuth.loginTitle')">
|
||||
<VDialog width="40rem" scrollable :fullscreen="!display.mdAndUp.value">
|
||||
<VCard>
|
||||
<VDialogCloseBtn @click="emit('close')" />
|
||||
<VCardText class="pt-2 flex flex-col items-center">
|
||||
<div class="my-6 rounded text-center p-3 border">
|
||||
<VCardItem>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-qrcode" class="me-2" />
|
||||
</template>
|
||||
<VCardTitle>
|
||||
{{ t('dialog.aliyunAuth.loginTitle') }}
|
||||
</VCardTitle>
|
||||
</VCardItem>
|
||||
<VDivider />
|
||||
<VCardText class="pt-2 flex flex-col items-center justify-center">
|
||||
<div class="mt-6 rounded text-center p-3 border">
|
||||
<VImg class="mx-auto" :src="qrCodeUrl" width="200" height="200">
|
||||
<template #placeholder>
|
||||
<div class="w-full h-full">
|
||||
@@ -119,16 +132,18 @@ onUnmounted(() => {
|
||||
</template>
|
||||
</VImg>
|
||||
</div>
|
||||
<VAlert variant="tonal" :type="alertType" class="my-4 text-center" :text="text">
|
||||
<template #prepend />
|
||||
</VAlert>
|
||||
<div>
|
||||
<VAlert variant="tonal" :type="alertType" class="my-4 text-center" :text="text">
|
||||
<template #prepend />
|
||||
</VAlert>
|
||||
</div>
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
<VBtn variant="tonal" color="error" @click="handleReset" prepend-icon="mdi-restore" class="px-5 me-3">
|
||||
<VBtn color="error" @click="handleReset" prepend-icon="mdi-restore" class="px-5 me-3">
|
||||
{{ t('dialog.aliyunAuth.reset') }}
|
||||
</VBtn>
|
||||
<VBtn variant="elevated" @click="handleDone" prepend-icon="mdi-check" class="px-5 me-3">
|
||||
<VSpacer />
|
||||
<VBtn @click="handleDone" prepend-icon="mdi-check" class="px-5 me-3">
|
||||
{{ t('dialog.aliyunAuth.complete') }}
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
|
||||
@@ -25,14 +25,20 @@ function handleImport() {
|
||||
|
||||
<template>
|
||||
<VDialog width="40rem" scrollable max-height="85vh">
|
||||
<VCard :title="props.title">
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-code-json" class="me-2" />
|
||||
</template>
|
||||
<VCardTitle>{{ props.title }}</VCardTitle>
|
||||
</VCardItem>
|
||||
<VDialogCloseBtn @click="emit('close')" />
|
||||
<VCardText class="pt-2">
|
||||
<VTextarea v-model="codeString" />
|
||||
<VTextarea v-model="codeString" prepend-inner-icon="mdi-code-json" />
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
<VBtn variant="elevated" @click="handleImport" prepend-icon="mdi-import" class="px-5 me-3">
|
||||
<VBtn @click="handleImport" prepend-icon="mdi-import" class="px-5 me-3">
|
||||
{{ t('dialog.importCode.import') }}
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
|
||||
@@ -161,18 +161,12 @@ onBeforeMount(async () => {
|
||||
</div>
|
||||
</VCardText>
|
||||
<VCardActions class="pt-3">
|
||||
<VBtn v-if="props.plugin?.has_page" @click="emit('switch')" variant="outlined" color="info">
|
||||
<VBtn v-if="props.plugin?.has_page" @click="emit('switch')" color="info">
|
||||
{{ t('dialog.pluginConfig.viewData') }}
|
||||
</VBtn>
|
||||
<VSpacer />
|
||||
<!-- 只有Vuetify模式显示默认保存按钮,Vue模式由组件内部控制 -->
|
||||
<VBtn
|
||||
v-if="renderMode === 'vuetify'"
|
||||
@click="savePluginConf"
|
||||
variant="elevated"
|
||||
prepend-icon="mdi-content-save"
|
||||
class="px-5"
|
||||
>
|
||||
<VBtn v-if="renderMode === 'vuetify'" @click="savePluginConf" prepend-icon="mdi-content-save" class="px-5">
|
||||
保存
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
import api from '@/api'
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { computed } from 'vue'
|
||||
import { useDisplay } from 'vuetify'
|
||||
|
||||
// 显示器宽度
|
||||
const display = useDisplay()
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
@@ -9,6 +14,16 @@ const $toast = useToast()
|
||||
|
||||
// 插件仓库设置字符串
|
||||
const repoString = ref('')
|
||||
// 用于显示的仓库地址数组
|
||||
const repoArray = ref<string[]>([])
|
||||
|
||||
// 计算属性:在数组和换行符分隔的字符串之间转换
|
||||
const displayRepos = computed({
|
||||
get: () => repoArray.value.join('\n'),
|
||||
set: (value: string) => {
|
||||
repoArray.value = value.split('\n').filter((repo: string) => repo.trim() !== '')
|
||||
},
|
||||
})
|
||||
|
||||
// 定义事件
|
||||
const emit = defineEmits(['save', 'close'])
|
||||
@@ -17,7 +32,10 @@ const emit = defineEmits(['save', 'close'])
|
||||
async function queryMarketRepoSetting() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/setting/PLUGIN_MARKET')
|
||||
if (result && result.data && result.data.value) repoString.value = result.data.value
|
||||
if (result && result.data && result.data.value) {
|
||||
repoString.value = result.data.value
|
||||
repoArray.value = result.data.value.split(',').filter((repo: string) => repo.trim() !== '')
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
@@ -26,8 +44,9 @@ async function queryMarketRepoSetting() {
|
||||
// 保存设置
|
||||
async function saveHandle() {
|
||||
try {
|
||||
// 用户名密码
|
||||
const result: { [key: string]: any } = await api.post('system/setting/PLUGIN_MARKET', repoString.value)
|
||||
// 将数组转换为逗号分隔的字符串
|
||||
const repoStringToSave = repoArray.value.join(',')
|
||||
const result: { [key: string]: any } = await api.post('system/setting/PLUGIN_MARKET', repoStringToSave)
|
||||
|
||||
if (result.success) {
|
||||
$toast.success(t('dialog.pluginMarketSetting.saveSuccess'))
|
||||
@@ -44,7 +63,7 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VDialog width="50rem" scrollable max-height="85vh">
|
||||
<VDialog width="50rem" scrollable :fullscreen="!display.mdAndUp.value">
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>
|
||||
@@ -53,17 +72,19 @@ onMounted(() => {
|
||||
</VCardTitle>
|
||||
<VDialogCloseBtn @click="emit('close')" />
|
||||
</VCardItem>
|
||||
<VDivider />
|
||||
<VCardText class="pt-2">
|
||||
<VTextarea
|
||||
v-model="repoString"
|
||||
v-model="displayRepos"
|
||||
:placeholder="t('dialog.pluginMarketSetting.repoPlaceholder')"
|
||||
:hint="t('dialog.pluginMarketSetting.repoHint')"
|
||||
persistent-hint
|
||||
auto-grow
|
||||
/>
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
<VBtn variant="elevated" @click="saveHandle" prepend-icon="mdi-content-save-check" class="px-5 me-3">
|
||||
<VBtn @click="saveHandle" prepend-icon="mdi-content-save-check" class="px-5 me-3">
|
||||
{{ t('dialog.pluginMarketSetting.save') }}
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
<script lang="ts" setup>
|
||||
import api from '@/api'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useDisplay } from 'vuetify'
|
||||
|
||||
// 显示器宽度
|
||||
const display = useDisplay()
|
||||
|
||||
// 多语言支持
|
||||
const { t } = useI18n()
|
||||
@@ -53,32 +57,44 @@ async function handleReset() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VDialog width="50rem" scrollable max-height="85vh">
|
||||
<VCard :title="t('dialog.rcloneConfig.title')">
|
||||
<VDialog width="50rem" scrollable :fullscreen="!display.mdAndUp.value">
|
||||
<VCard>
|
||||
<VDialogCloseBtn @click="emit('close')" />
|
||||
<VCardItem>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-cog-outline" class="me-2" />
|
||||
</template>
|
||||
<VCardTitle>
|
||||
{{ t('dialog.rcloneConfig.title') }}
|
||||
</VCardTitle>
|
||||
</VCardItem>
|
||||
<VDivider />
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VTextField v-model="props.conf.filepath" :label="t('dialog.rcloneConfig.filePath')" />
|
||||
<VTextField
|
||||
v-model="props.conf.filepath"
|
||||
:label="t('dialog.rcloneConfig.filePath')"
|
||||
prepend-inner-icon="mdi-file-document"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VAceEditor
|
||||
v-model:value="props.conf.content"
|
||||
lang="ini"
|
||||
theme="monokai"
|
||||
style="block-size: 30rem"
|
||||
class="rounded"
|
||||
class="rounded h-full min-h-[30rem]"
|
||||
>
|
||||
</VAceEditor>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
<VBtn variant="tonal" color="error" @click="handleReset" prepend-icon="mdi-restore" class="px-5 me-3">
|
||||
<VBtn color="error" @click="handleReset" prepend-icon="mdi-restore" class="px-5 me-3">
|
||||
{{ t('dialog.rcloneConfig.reset') }}
|
||||
</VBtn>
|
||||
<VBtn variant="elevated" @click="handleDone" prepend-icon="mdi-check" class="px-5 me-3">
|
||||
<VSpacer />
|
||||
<VBtn @click="handleDone" prepend-icon="mdi-check" class="px-5 me-3">
|
||||
{{ t('dialog.rcloneConfig.complete') }}
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
|
||||
@@ -82,15 +82,18 @@ const storageOptions = computed(() => {
|
||||
|
||||
// 标题
|
||||
const dialogTitle = computed(() => {
|
||||
return t('dialog.reorganize.manualTitle')
|
||||
})
|
||||
|
||||
// 副标题
|
||||
const dialogSubtitle = computed(() => {
|
||||
if (props.items) {
|
||||
if (props.items.length > 1) return t('dialog.reorganize.multipleItemsTitle', { count: props.items.length })
|
||||
return t('dialog.reorganize.singleItemTitle', { path: props.items[0].path })
|
||||
} else if (props.logids) {
|
||||
return t('dialog.reorganize.multipleItemsTitle', { count: props.logids.length })
|
||||
}
|
||||
return t('dialog.reorganize.manualTitle')
|
||||
})
|
||||
|
||||
// 禁用指定集数
|
||||
const disableEpisodeDetail = computed(() => {
|
||||
if (props.items) {
|
||||
@@ -250,7 +253,12 @@ onUnmounted(() => {
|
||||
|
||||
<template>
|
||||
<VDialog scrollable max-width="45rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VCard :title="dialogTitle">
|
||||
<VCard>
|
||||
<VCardItem class="py-2">
|
||||
<template #prepend> <VIcon icon="mdi-folder-move" class="me-2" /> </template>
|
||||
<VCardTitle>{{ dialogTitle }}</VCardTitle>
|
||||
<VCardSubtitle>{{ dialogSubtitle }}</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VDialogCloseBtn @click="emit('close')" />
|
||||
<VDivider />
|
||||
<VCardText>
|
||||
@@ -264,6 +272,7 @@ onUnmounted(() => {
|
||||
:placeholder="t('dialog.reorganize.targetPathPlaceholder')"
|
||||
:hint="t('dialog.reorganize.targetStorageHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-harddisk"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -273,6 +282,7 @@ onUnmounted(() => {
|
||||
:items="transferTypeOptions"
|
||||
:hint="t('dialog.reorganize.transferTypeHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-swap-horizontal"
|
||||
>
|
||||
<template v-slot:selection="{ item }">
|
||||
{{ transferForm.transfer_type === '' ? t('dialog.reorganize.auto') : item.title }}
|
||||
@@ -287,6 +297,7 @@ onUnmounted(() => {
|
||||
:placeholder="t('dialog.reorganize.targetPathPlaceholder')"
|
||||
:hint="t('dialog.reorganize.targetPathHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-folder-outline"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -302,6 +313,7 @@ onUnmounted(() => {
|
||||
]"
|
||||
:hint="t('dialog.reorganize.mediaTypeHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-movie-open"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -315,6 +327,7 @@ onUnmounted(() => {
|
||||
append-inner-icon="mdi-magnify"
|
||||
:hint="t('dialog.reorganize.mediaIdHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-identifier"
|
||||
@click:append-inner="mediaSelectorDialog = true"
|
||||
/>
|
||||
<VTextField
|
||||
@@ -327,6 +340,7 @@ onUnmounted(() => {
|
||||
append-inner-icon="mdi-magnify"
|
||||
:hint="t('dialog.reorganize.mediaIdHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-identifier"
|
||||
@click:append-inner="mediaSelectorDialog = true"
|
||||
/>
|
||||
</VCol>
|
||||
@@ -339,6 +353,7 @@ onUnmounted(() => {
|
||||
:placeholder="t('dialog.reorganize.episodeGroupPlaceholder')"
|
||||
:hint="t('dialog.reorganize.episodeGroupHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-view-list"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="3">
|
||||
@@ -348,6 +363,7 @@ onUnmounted(() => {
|
||||
:items="seasonItems"
|
||||
:hint="t('dialog.reorganize.seasonHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-calendar"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="3">
|
||||
@@ -358,6 +374,7 @@ onUnmounted(() => {
|
||||
:placeholder="t('dialog.reorganize.episodeDetailPlaceholder')"
|
||||
:hint="t('dialog.reorganize.episodeDetailHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-playlist-play"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -367,6 +384,7 @@ onUnmounted(() => {
|
||||
:placeholder="t('dialog.reorganize.episodeFormatPlaceholder')"
|
||||
:hint="t('dialog.reorganize.episodeFormatHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-format-text"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -376,6 +394,7 @@ onUnmounted(() => {
|
||||
:placeholder="t('dialog.reorganize.episodeOffsetPlaceholder')"
|
||||
:hint="t('dialog.reorganize.episodeOffsetHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-numeric"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -387,6 +406,7 @@ onUnmounted(() => {
|
||||
:placeholder="t('dialog.reorganize.episodePartPlaceholder')"
|
||||
:hint="t('dialog.reorganize.episodePartHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-file-multiple"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -397,6 +417,7 @@ onUnmounted(() => {
|
||||
placeholder="0"
|
||||
:hint="t('dialog.reorganize.minFileSizeHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-file-document-outline"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -438,10 +459,10 @@ onUnmounted(() => {
|
||||
</VCardText>
|
||||
<VCardActions class="pt-3">
|
||||
<VSpacer />
|
||||
<VBtn variant="elevated" color="success" @click="transfer(true)" prepend-icon="mdi-plus" class="px-5">
|
||||
<VBtn color="success" @click="transfer(true)" prepend-icon="mdi-plus" class="px-5">
|
||||
{{ t('dialog.reorganize.addToQueue') }}
|
||||
</VBtn>
|
||||
<VBtn variant="elevated" @click="transfer(false)" prepend-icon="mdi-arrow-right-bold" class="px-5">
|
||||
<VBtn @click="transfer(false)" prepend-icon="mdi-arrow-right-bold" class="px-5">
|
||||
{{ t('dialog.reorganize.reorganizeNow') }}
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
|
||||
@@ -6,6 +6,10 @@ import { NavMenu } from '@/@layouts/types'
|
||||
import { useUserStore } from '@/stores'
|
||||
import SearchSiteDialog from '@/components/dialog/SearchSiteDialog.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useDisplay } from 'vuetify'
|
||||
|
||||
// 显示器宽度
|
||||
const display = useDisplay()
|
||||
|
||||
// 多语言支持
|
||||
const { t } = useI18n()
|
||||
@@ -302,29 +306,24 @@ onMounted(() => {
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<VDialog v-model="dialog" max-width="42rem" scrollable maxHeight="85vh">
|
||||
<VDialog v-model="dialog" max-width="42rem" scrollable :fullscreen="!display.mdAndUp.value">
|
||||
<VCard class="search-dialog">
|
||||
<!-- 搜索输入框 -->
|
||||
<VCardItem class="pa-4 pa-sm-5 search-box-container">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-magnify" color="primary" size="x-large" />
|
||||
</template>
|
||||
<VCombobox
|
||||
ref="searchWordInput"
|
||||
v-model="searchWord"
|
||||
density="comfortable"
|
||||
variant="outlined"
|
||||
class="search-input"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
append-inner-icon="mdi-close"
|
||||
@click:append-inner="emit('close')"
|
||||
:placeholder="t('dialog.searchBar.searchPlaceholder')"
|
||||
@keydown.enter="searchMedia('media')"
|
||||
hide-details
|
||||
clearable
|
||||
/>
|
||||
<template #append>
|
||||
<IconBtn>
|
||||
<VIcon icon="mdi-close" color="primary" @click="emit('close')" size="x-large" />
|
||||
</IconBtn>
|
||||
</template>
|
||||
</VCardItem>
|
||||
|
||||
<VDivider />
|
||||
|
||||
@@ -58,23 +58,16 @@ const filteredSites = computed(() => {
|
||||
<!-- Site Selection Dialog -->
|
||||
<VDialog max-width="40rem" fullscreen-mobile>
|
||||
<VCard class="site-dialog">
|
||||
<VCardTitle class="d-flex align-center pa-4">
|
||||
<span class="text-h6 font-weight-medium">{{ t('dialog.searchSite.selectSites') }}</span>
|
||||
<VSpacer />
|
||||
<VTextField
|
||||
v-model="siteFilter"
|
||||
:placeholder="t('dialog.searchSite.siteSearch')"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
class="ml-4"
|
||||
style="max-inline-size: 200px"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
clearable
|
||||
/>
|
||||
</VCardTitle>
|
||||
<VDivider class="search-divider" />
|
||||
|
||||
<VCardItem>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-web-check" />
|
||||
</template>
|
||||
<VCardTitle>
|
||||
{{ t('dialog.searchSite.selectSites') }}
|
||||
</VCardTitle>
|
||||
</VCardItem>
|
||||
<VDialogCloseBtn @click="emit('close')" />
|
||||
<VDivider />
|
||||
<VCardText style="max-block-size: 420px" class="overflow-y-auto px-4 py-4">
|
||||
<!-- 站点列表 -->
|
||||
<div v-if="filteredSites.length > 0">
|
||||
@@ -163,27 +156,16 @@ const filteredSites = computed(() => {
|
||||
</div>
|
||||
</VCardText>
|
||||
|
||||
<VDivider class="search-divider" />
|
||||
|
||||
<VCardActions class="pa-4">
|
||||
<VCardActions class="pt-3">
|
||||
<VSpacer />
|
||||
<VBtn
|
||||
color="grey-darken-1"
|
||||
variant="text"
|
||||
@click="emit('close')"
|
||||
class="mr-2 d-flex align-center justify-center"
|
||||
>
|
||||
{{ t('dialog.searchSite.cancel') }}
|
||||
</VBtn>
|
||||
<VBtn
|
||||
color="primary"
|
||||
variant="flat"
|
||||
:disabled="selectedSites.length === 0"
|
||||
@click="emit('search', selectedSites)"
|
||||
prepend-icon="mdi-magnify"
|
||||
class="d-flex align-center justify-center px-5"
|
||||
>
|
||||
{{ t('dialog.searchSite.confirm') }}
|
||||
{{ t('common.search') }}
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
|
||||
@@ -148,11 +148,14 @@ onMounted(async () => {
|
||||
|
||||
<template>
|
||||
<VDialog scrollable :close-on-back="false" eager max-width="45rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VCard
|
||||
:title="`${props.oper === 'add' ? t('site.actions.add') : t('site.actions.edit')}${t('site.title')}${
|
||||
props.oper !== 'add' ? ` - ${siteForm.name}` : ''
|
||||
}`"
|
||||
>
|
||||
<VCard>
|
||||
<VCardItem :class="props.oper === 'add' ? 'py-3' : 'py-2'">
|
||||
<template #prepend>
|
||||
<VIcon :icon="oper == 'add' ? 'mdi-web-plus' : 'mdi-web'" class="me-2" />
|
||||
</template>
|
||||
<VCardTitle>{{ `${props.oper === 'add' ? t('site.actions.add') : t('site.actions.edit')}` }}</VCardTitle>
|
||||
<VCardSubtitle>{{ siteForm.name }}</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VDialogCloseBtn @click="emit('close')" />
|
||||
<VDivider />
|
||||
<VCardText>
|
||||
@@ -165,16 +168,18 @@ onMounted(async () => {
|
||||
:rules="[requiredValidator]"
|
||||
:hint="t('site.hints.url')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-web"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6" md="3">
|
||||
<VSelect
|
||||
<VAutocomplete
|
||||
v-model="siteForm.pri"
|
||||
:label="t('site.fields.priority')"
|
||||
:items="priorityItems"
|
||||
:rules="[requiredValidator]"
|
||||
:hint="t('site.hints.priority')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-priority-high"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6" md="3">
|
||||
@@ -184,6 +189,7 @@ onMounted(async () => {
|
||||
:label="t('site.fields.status')"
|
||||
:hint="t('site.hints.status')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-toggle-switch"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -194,6 +200,7 @@ onMounted(async () => {
|
||||
:label="t('site.fields.rss')"
|
||||
:hint="t('site.hints.rss')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-rss"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="3">
|
||||
@@ -202,15 +209,17 @@ onMounted(async () => {
|
||||
:label="t('site.fields.timeout')"
|
||||
:hint="t('site.hints.timeout')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-timer"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6" md="3">
|
||||
<VSelect
|
||||
<VAutocomplete
|
||||
v-model="siteForm.downloader"
|
||||
:label="t('site.fields.downloader')"
|
||||
:items="downloaderOptions"
|
||||
:hint="t('site.hints.downloader')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-download"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -237,6 +246,7 @@ onMounted(async () => {
|
||||
:label="t('site.fields.cookie')"
|
||||
:hint="t('site.hints.cookie')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-cookie"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
@@ -245,6 +255,7 @@ onMounted(async () => {
|
||||
:label="t('site.fields.userAgent')"
|
||||
:hint="t('site.hints.userAgent')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-web-box"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -257,6 +268,7 @@ onMounted(async () => {
|
||||
:label="t('site.fields.authorization')"
|
||||
:hint="t('site.hints.authorization')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-key"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -265,6 +277,7 @@ onMounted(async () => {
|
||||
:label="t('site.fields.apiKey')"
|
||||
:hint="t('site.hints.apiKey')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-api"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -283,6 +296,7 @@ onMounted(async () => {
|
||||
:rules="[numberValidator]"
|
||||
:hint="t('site.hints.limitInterval')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-clock-outline"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
@@ -292,6 +306,7 @@ onMounted(async () => {
|
||||
:rules="[numberValidator]"
|
||||
:hint="t('site.hints.limitCount')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-counter"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
@@ -301,6 +316,7 @@ onMounted(async () => {
|
||||
:rules="[numberValidator]"
|
||||
:hint="t('site.hints.limitSeconds')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-timer-sand"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -326,24 +342,10 @@ onMounted(async () => {
|
||||
</VCardText>
|
||||
<VCardActions class="pt-3">
|
||||
<VSpacer />
|
||||
<VBtn
|
||||
v-if="props.oper === 'add'"
|
||||
color="primary"
|
||||
variant="elevated"
|
||||
@click="addSite"
|
||||
prepend-icon="mdi-plus"
|
||||
class="px-5"
|
||||
>
|
||||
<VBtn v-if="props.oper === 'add'" color="primary" @click="addSite" prepend-icon="mdi-plus" class="px-5">
|
||||
{{ t('site.actions.add') }}
|
||||
</VBtn>
|
||||
<VBtn
|
||||
v-else
|
||||
color="primary"
|
||||
variant="elevated"
|
||||
@click="updateSiteInfo"
|
||||
prepend-icon="mdi-content-save"
|
||||
class="px-5"
|
||||
>
|
||||
<VBtn v-else color="primary" @click="updateSiteInfo" prepend-icon="mdi-content-save" class="px-5">
|
||||
{{ t('common.save') }}
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
|
||||
@@ -71,7 +71,7 @@ async function updateSiteCookie() {
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<VDialog max-width="30rem">
|
||||
<VDialog max-width="30rem" scrollable>
|
||||
<!-- Dialog Content -->
|
||||
<VCard :title="t('dialog.siteCookieUpdate.title')">
|
||||
<VDialogCloseBtn @click="emit('close')" />
|
||||
@@ -102,7 +102,6 @@ async function updateSiteCookie() {
|
||||
<VCardActions class="mx-auto">
|
||||
<VBtn
|
||||
size="large"
|
||||
variant="elevated"
|
||||
@click="updateSiteCookie"
|
||||
:disabled="updateButtonDisable"
|
||||
:loading="updateButtonDisable"
|
||||
|
||||
@@ -134,7 +134,7 @@ onMounted(() => {
|
||||
<VCard>
|
||||
<!-- Toolbar -->
|
||||
<div>
|
||||
<VToolbar color="primary">
|
||||
<VToolbar color="primary" density="comfortable">
|
||||
<VToolbarTitle>{{ t('dialog.siteResource.browseTitle', { name: props.site?.name }) }}</VToolbarTitle>
|
||||
<VSpacer />
|
||||
<VToolbarItems>
|
||||
@@ -153,6 +153,7 @@ onMounted(() => {
|
||||
density="compact"
|
||||
:label="t('dialog.siteResource.searchKeyword')"
|
||||
clearable
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6" md="5">
|
||||
@@ -165,10 +166,13 @@ onMounted(() => {
|
||||
:label="t('dialog.siteResource.resourceCategory')"
|
||||
multiple
|
||||
clearable
|
||||
prepend-inner-icon="mdi-folder"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="2" class="text-center">
|
||||
<VBtn block prepend-icon="mdi-magnify" @click="getResourceList">{{ t('dialog.siteResource.search') }}</VBtn>
|
||||
<VBtn variant="tonal" block prepend-icon="mdi-magnify" @click="getResourceList">
|
||||
{{ t('dialog.siteResource.search') }}
|
||||
</VBtn>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</div>
|
||||
@@ -186,7 +190,6 @@ onMounted(() => {
|
||||
fixed-header
|
||||
hover
|
||||
:items-per-page-text="t('dialog.siteResource.itemsPerPage')"
|
||||
:page-text="t('dialog.siteResource.pageText')"
|
||||
:loading-text="t('dialog.siteResource.loading')"
|
||||
class="h-full"
|
||||
>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { numberValidator } from '@/@validators'
|
||||
import api from '@/api'
|
||||
import type { DownloaderConf, FilterRuleGroup, Site, Subscribe, TransferDirectoryConf } from '@/api/types'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import { useConfirm } from 'vuetify-use-dialog'
|
||||
import { useConfirm } from '@/composables/useConfirm'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { qualityOptions, resolutionOptions, effectOptions } from '@/api/constants'
|
||||
// i18n
|
||||
@@ -281,18 +281,24 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<VDialog scrollable max-width="45rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VCard
|
||||
:title="
|
||||
props.default
|
||||
? t('dialog.subscribeEdit.titleDefault')
|
||||
: t('dialog.subscribeEdit.titleEditFormat', {
|
||||
name: subscribeForm.name,
|
||||
season: subscribeForm.season
|
||||
? t('dialog.subscribeEdit.seasonFormat', { number: subscribeForm.season })
|
||||
: '',
|
||||
})
|
||||
"
|
||||
>
|
||||
<VCard>
|
||||
<VCardItem class="py-2">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-clipboard-list-outline" class="me-2" />
|
||||
</template>
|
||||
<VCardTitle>
|
||||
{{ props.default ? t('dialog.subscribeEdit.titleDefault') : t('dialog.subscribeEdit.titleEdit') }}
|
||||
</VCardTitle>
|
||||
<VCardSubtitle v-if="!props.default">
|
||||
{{ subscribeForm.name }}
|
||||
<span v-if="subscribeForm.season">
|
||||
{{ t('dialog.subscribeEdit.seasonFormat', { number: subscribeForm.season }) }}
|
||||
</span>
|
||||
</VCardSubtitle>
|
||||
<VCardSubtitle v-else>
|
||||
{{ props.type }}
|
||||
</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VDialogCloseBtn @click="emit('close')" />
|
||||
<VForm @submit.prevent="() => {}">
|
||||
@@ -314,6 +320,7 @@ onMounted(() => {
|
||||
:label="t('dialog.subscribeEdit.searchKeyword')"
|
||||
:hint="t('dialog.subscribeEdit.searchKeywordHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol v-if="subscribeForm.type === '电视剧'" cols="12" md="4">
|
||||
@@ -323,6 +330,7 @@ onMounted(() => {
|
||||
:rules="[numberValidator]"
|
||||
:hint="t('dialog.subscribeEdit.totalEpisodeHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-playlist-play"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol v-if="subscribeForm.type === '电视剧'" cols="12" md="4">
|
||||
@@ -332,41 +340,45 @@ onMounted(() => {
|
||||
:rules="[numberValidator]"
|
||||
:hint="t('dialog.subscribeEdit.startEpisodeHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-play-circle-outline"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12" md="4">
|
||||
<VSelect
|
||||
<VAutocomplete
|
||||
v-model="subscribeForm.quality"
|
||||
:label="t('dialog.subscribeEdit.quality')"
|
||||
:items="qualityOptions"
|
||||
:hint="t('dialog.subscribeEdit.qualityHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-quality-high"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VSelect
|
||||
<VAutocomplete
|
||||
v-model="subscribeForm.resolution"
|
||||
:label="t('dialog.subscribeEdit.resolution')"
|
||||
:items="resolutionOptions"
|
||||
:hint="t('dialog.subscribeEdit.resolutionHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-monitor"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VSelect
|
||||
<VAutocomplete
|
||||
v-model="subscribeForm.effect"
|
||||
:label="t('dialog.subscribeEdit.effect')"
|
||||
:items="effectOptions"
|
||||
:hint="t('dialog.subscribeEdit.effectHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-auto-fix"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VSelect
|
||||
<VAutocomplete
|
||||
v-model="subscribeForm.sites"
|
||||
:items="selectSitesOptions"
|
||||
chips
|
||||
@@ -375,17 +387,19 @@ onMounted(() => {
|
||||
clearable
|
||||
:hint="t('dialog.subscribeEdit.subscribeSitesHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-web"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VSelect
|
||||
<VAutocomplete
|
||||
v-model="subscribeForm.downloader"
|
||||
:items="downloaderOptions"
|
||||
:label="t('dialog.subscribeEdit.downloader')"
|
||||
:hint="t('dialog.subscribeEdit.downloaderHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-download"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -395,6 +409,7 @@ onMounted(() => {
|
||||
:label="t('dialog.subscribeEdit.savePath')"
|
||||
:hint="t('dialog.subscribeEdit.savePathHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-folder"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -435,6 +450,7 @@ onMounted(() => {
|
||||
:label="t('dialog.subscribeEdit.include')"
|
||||
:hint="t('dialog.subscribeEdit.includeHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-plus-circle-outline"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -443,12 +459,13 @@ onMounted(() => {
|
||||
:label="t('dialog.subscribeEdit.exclude')"
|
||||
:hint="t('dialog.subscribeEdit.excludeHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-minus-circle-outline"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VSelect
|
||||
<VAutocomplete
|
||||
v-model="subscribeForm.filter_groups"
|
||||
:items="filterRuleGroupOptions"
|
||||
chips
|
||||
@@ -457,25 +474,28 @@ onMounted(() => {
|
||||
:label="t('dialog.subscribeEdit.filterGroups')"
|
||||
:hint="t('dialog.subscribeEdit.filterGroupsHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-filter"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol v-if="!props.default && subscribeForm.type === '电视剧'" cols="12" md="6">
|
||||
<VSelect
|
||||
<VAutocomplete
|
||||
v-model="subscribeForm.episode_group"
|
||||
:items="episodeGroupOptions"
|
||||
:item-props="episodeGroupItemProps"
|
||||
:label="t('dialog.subscribeEdit.episodeGroup')"
|
||||
:hint="t('dialog.subscribeEdit.episodeGroupHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-view-list"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol v-if="!props.default && subscribeForm.type === '电视剧'" cols="12" md="6">
|
||||
<VSelect
|
||||
<VAutocomplete
|
||||
v-model="subscribeForm.season"
|
||||
:items="seasonItems"
|
||||
:label="t('dialog.subscribeEdit.season')"
|
||||
:hint="t('dialog.subscribeEdit.seasonHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-calendar"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" v-if="!props.default">
|
||||
@@ -484,6 +504,7 @@ onMounted(() => {
|
||||
:label="t('dialog.subscribeEdit.mediaCategory')"
|
||||
:hint="t('dialog.subscribeEdit.mediaCategoryHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-tag"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -495,6 +516,7 @@ onMounted(() => {
|
||||
:hint="t('dialog.subscribeEdit.customWordsHint')"
|
||||
persistent-hint
|
||||
:placeholder="t('dialog.subscribeEdit.customWordsPlaceholder')"
|
||||
prepend-inner-icon="mdi-text"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -504,12 +526,11 @@ onMounted(() => {
|
||||
</VForm>
|
||||
</VCardText>
|
||||
<VCardActions class="pt-3">
|
||||
<VBtn v-if="!props.default" color="error" @click="removeSubscribe" variant="outlined" class="me-3">
|
||||
<VBtn v-if="!props.default" color="error" @click="removeSubscribe" class="me-3">
|
||||
{{ t('dialog.subscribeEdit.cancelSubscribe') }}
|
||||
</VBtn>
|
||||
<VSpacer />
|
||||
<VBtn
|
||||
variant="elevated"
|
||||
@click=";`${props.default ? saveDefaultSubscribeConfig() : updateSubscribeInfo()}`"
|
||||
prepend-icon="mdi-content-save"
|
||||
class="px-5"
|
||||
|
||||
@@ -23,6 +23,9 @@ const emit = defineEmits(['close'])
|
||||
// 订阅文件信息
|
||||
const subScribeInfo = ref<SubscrbieInfo>()
|
||||
|
||||
// 是否加载中
|
||||
const loading = ref(false)
|
||||
|
||||
// 下载文件表头
|
||||
const downloadHeaders = [
|
||||
{ title: t('dialog.subscribeFiles.episodeColumn'), key: 'episode_number', sortable: true },
|
||||
@@ -39,9 +42,12 @@ const libraryHeaders = [
|
||||
// 调用API查询订阅文件信息
|
||||
async function loadSubscribeFilesInfo() {
|
||||
try {
|
||||
loading.value = true
|
||||
subScribeInfo.value = await api.get(`subscribe/files/${props.subid}`)
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +90,8 @@ onBeforeMount(() => {
|
||||
<VCardItem class="my-2">
|
||||
<VDialogCloseBtn @click="emit('close')" />
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<LoadingBanner v-if="loading" />
|
||||
<VCardText v-else>
|
||||
<div class="media-page">
|
||||
<div class="media-header">
|
||||
<div class="media-poster">
|
||||
|
||||
@@ -56,11 +56,18 @@ const $toast = useToast()
|
||||
|
||||
<template>
|
||||
<VDialog scrollable max-width="30rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VCard
|
||||
:title="`${t('dialog.subscribeShare.shareSubscription')} - ${props.sub?.name} ${
|
||||
props.sub?.season ? t('dialog.subscribeShare.season', { number: props.sub?.season }) : ''
|
||||
}`"
|
||||
>
|
||||
<VCard>
|
||||
<VCardItem class="py-2">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-share-outline" class="me-2" />
|
||||
</template>
|
||||
<VCardTitle>{{ t('dialog.subscribeShare.shareSubscription') }}</VCardTitle>
|
||||
<VCardSubtitle>
|
||||
{{ props.sub?.name }}
|
||||
{{ props.sub?.season ? t('dialog.subscribeShare.season', { number: props.sub?.season }) : '' }}
|
||||
</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VDivider />
|
||||
<VCardText>
|
||||
<VDialogCloseBtn @click="emit('close')" />
|
||||
<VForm @submit.prevent="() => {}" class="pt-2">
|
||||
@@ -72,6 +79,7 @@ const $toast = useToast()
|
||||
:label="t('dialog.subscribeShare.title')"
|
||||
:rules="[requiredValidator]"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-format-title"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
@@ -81,6 +89,7 @@ const $toast = useToast()
|
||||
:rules="[requiredValidator]"
|
||||
:hint="t('dialog.subscribeShare.descriptionHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-comment-text-outline"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
@@ -90,6 +99,7 @@ const $toast = useToast()
|
||||
:rules="[requiredValidator]"
|
||||
:hint="t('dialog.subscribeShare.shareUserHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-account-outline"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -97,14 +107,7 @@ const $toast = useToast()
|
||||
</VCardText>
|
||||
<VCardActions class="pt-3">
|
||||
<VSpacer />
|
||||
<VBtn
|
||||
variant="elevated"
|
||||
:disabled="shareDoing"
|
||||
@click="doShare"
|
||||
prepend-icon="mdi-share"
|
||||
class="px-5"
|
||||
:loading="shareDoing"
|
||||
>
|
||||
<VBtn :disabled="shareDoing" @click="doShare" prepend-icon="mdi-share" class="px-5" :loading="shareDoing">
|
||||
{{ t('dialog.subscribeShare.confirmShare') }}
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
import api from '@/api'
|
||||
import QrcodeVue from 'qrcode.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useDisplay } from 'vuetify'
|
||||
|
||||
// 显示器宽度
|
||||
const display = useDisplay()
|
||||
|
||||
// 多语言支持
|
||||
const { t } = useI18n()
|
||||
@@ -111,23 +115,34 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VDialog width="40rem" scrollable max-height="85vh">
|
||||
<VCard :title="t('dialog.u115Auth.loginTitle')">
|
||||
<VDialog width="40rem" scrollable :fullscreen="!display.mdAndUp.value">
|
||||
<VCard>
|
||||
<VDialogCloseBtn @click="emit('close')" />
|
||||
<VCardText class="pt-2 flex flex-col items-center">
|
||||
<div class="my-6 rounded text-center p-3 border">
|
||||
<VCardItem>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-qrcode" class="me-2" />
|
||||
</template>
|
||||
<VCardTitle>
|
||||
{{ t('dialog.u115Auth.loginTitle') }}
|
||||
</VCardTitle>
|
||||
</VCardItem>
|
||||
<VDivider />
|
||||
<VCardText class="pt-2 flex flex-col items-center justify-center">
|
||||
<div class="mt-6 rounded text-center p-3 border">
|
||||
<QrcodeVue class="mx-auto" :value="qrCodeContent" :size="200" />
|
||||
</div>
|
||||
<VAlert variant="tonal" :type="alertType" class="my-4 text-center" :text="text">
|
||||
<template #prepend />
|
||||
</VAlert>
|
||||
<div>
|
||||
<VAlert variant="tonal" :type="alertType" class="my-4 text-center" :text="text">
|
||||
<template #prepend />
|
||||
</VAlert>
|
||||
</div>
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
<VBtn variant="tonal" color="error" @click="handleReset" prepend-icon="mdi-restore" class="px-5 me-3">
|
||||
<VBtn color="error" @click="handleReset" prepend-icon="mdi-restore" class="px-5 me-3">
|
||||
{{ t('dialog.u115Auth.reset') }}
|
||||
</VBtn>
|
||||
<VBtn variant="elevated" @click="handleDone" prepend-icon="mdi-check" class="px-5 me-3">
|
||||
<VSpacer />
|
||||
<VBtn @click="handleDone" prepend-icon="mdi-check" class="px-5 me-3">
|
||||
{{ t('dialog.u115Auth.complete') }}
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
|
||||
@@ -290,12 +290,15 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VDialog scrollable :close-on-back="false" eager max-width="40rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VCard
|
||||
:title="`${props.oper === 'add' ? t('dialog.userAddEdit.add') : t('dialog.userAddEdit.edit')}${
|
||||
props.oper !== 'add' ? ` - ${userName}` : ''
|
||||
}`"
|
||||
>
|
||||
<VDialog scrollable max-width="40rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VCard>
|
||||
<VCardItem :class="props.oper === 'add' ? 'py-3' : 'py-2'">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-account" class="me-2" />
|
||||
</template>
|
||||
<VCardTitle>{{ props.oper === 'add' ? t('dialog.userAddEdit.add') : t('dialog.userAddEdit.edit') }}</VCardTitle>
|
||||
<VCardSubtitle>{{ userName }}</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VDialogCloseBtn @click="emit('close')" />
|
||||
<VDivider />
|
||||
<VCardItem>
|
||||
@@ -350,6 +353,7 @@ onMounted(() => {
|
||||
density="comfortable"
|
||||
:readonly="props.oper !== 'add'"
|
||||
:label="t('dialog.userAddEdit.username')"
|
||||
prepend-inner-icon="mdi-account"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -359,6 +363,7 @@ onMounted(() => {
|
||||
clearable
|
||||
:label="t('dialog.userAddEdit.email')"
|
||||
type="email"
|
||||
prepend-inner-icon="mdi-email"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -370,6 +375,7 @@ onMounted(() => {
|
||||
clearable
|
||||
:label="t('dialog.userAddEdit.password')"
|
||||
autocomplete=""
|
||||
prepend-inner-icon="mdi-lock"
|
||||
@click:append-inner="isNewPasswordVisible = !isNewPasswordVisible"
|
||||
/>
|
||||
</VCol>
|
||||
@@ -382,6 +388,7 @@ onMounted(() => {
|
||||
:append-inner-icon="isConfirmPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
|
||||
clearable
|
||||
:label="t('dialog.userAddEdit.confirmPassword')"
|
||||
prepend-inner-icon="mdi-lock-check"
|
||||
@click:append-inner="isConfirmPasswordVisible = !isConfirmPasswordVisible"
|
||||
/>
|
||||
</VCol>
|
||||
@@ -392,6 +399,7 @@ onMounted(() => {
|
||||
clearable
|
||||
:label="t('dialog.userAddEdit.nickname')"
|
||||
placeholder="显示昵称,优先于用户名显示"
|
||||
prepend-inner-icon="mdi-card-account-details"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6" v-if="canControl">
|
||||
@@ -402,6 +410,7 @@ onMounted(() => {
|
||||
item-value="value"
|
||||
:label="t('dialog.userAddEdit.status')"
|
||||
dense
|
||||
prepend-inner-icon="mdi-toggle-switch"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -415,6 +424,7 @@ onMounted(() => {
|
||||
density="comfortable"
|
||||
clearable
|
||||
:label="t('dialog.userAddEdit.wechat')"
|
||||
prepend-inner-icon="mdi-wechat"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -423,6 +433,7 @@ onMounted(() => {
|
||||
density="comfortable"
|
||||
clearable
|
||||
:label="t('dialog.userAddEdit.telegram')"
|
||||
prepend-inner-icon="mdi-send"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -431,6 +442,7 @@ onMounted(() => {
|
||||
density="comfortable"
|
||||
clearable
|
||||
:label="t('dialog.userAddEdit.slack')"
|
||||
prepend-inner-icon="mdi-slack"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -439,6 +451,7 @@ onMounted(() => {
|
||||
density="comfortable"
|
||||
clearable
|
||||
:label="t('dialog.userAddEdit.vocechat')"
|
||||
prepend-inner-icon="mdi-chat"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -447,10 +460,17 @@ onMounted(() => {
|
||||
density="comfortable"
|
||||
clearable
|
||||
:label="t('dialog.userAddEdit.synologyChat')"
|
||||
prepend-inner-icon="mdi-message"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField v-model="userForm.settings.douban_userid" density="comfortable" clearable label="豆瓣用户" />
|
||||
<VTextField
|
||||
v-model="userForm.settings.douban_userid"
|
||||
density="comfortable"
|
||||
clearable
|
||||
label="豆瓣用户"
|
||||
prepend-inner-icon="mdi-movie"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
@@ -461,7 +481,6 @@ onMounted(() => {
|
||||
v-if="props.oper === 'add'"
|
||||
:disabled="isAdding"
|
||||
color="primary"
|
||||
variant="elevated"
|
||||
@click="addUser"
|
||||
prepend-icon="mdi-plus"
|
||||
class="px-5"
|
||||
@@ -473,7 +492,6 @@ onMounted(() => {
|
||||
v-else
|
||||
:disabled="isUpdating"
|
||||
color="primary"
|
||||
variant="elevated"
|
||||
@click="updateUser"
|
||||
prepend-icon="mdi-content-save"
|
||||
class="px-5"
|
||||
|
||||
@@ -4,6 +4,7 @@ import api from '@/api'
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
|
||||
// 多语言支持
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -133,9 +134,16 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VDialog width="40rem" max-height="85vh">
|
||||
<VCard :title="t('dialog.userAuth.title')">
|
||||
<VDialogCloseBtn @click="emit('close')" />
|
||||
<VDialog width="40rem" scrollable>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>
|
||||
<VIcon icon="mdi-user-check" class="me-2" />
|
||||
{{ t('dialog.userAuth.title') }}
|
||||
</VCardTitle>
|
||||
<VDialogCloseBtn @click="emit('close')" />
|
||||
</VCardItem>
|
||||
<VDivider />
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
@@ -146,6 +154,7 @@ onMounted(async () => {
|
||||
item-title="name"
|
||||
:label="t('dialog.userAuth.selectSite')"
|
||||
item-props
|
||||
prepend-inner-icon="mdi-web"
|
||||
>
|
||||
</VSelect>
|
||||
</VCol>
|
||||
@@ -165,14 +174,7 @@ onMounted(async () => {
|
||||
</VRow>
|
||||
</VCardText>
|
||||
<VCardText class="text-center">
|
||||
<VBtn
|
||||
variant="elevated"
|
||||
@click="handleDone"
|
||||
prepend-icon="mdi-check"
|
||||
class="px-5"
|
||||
size="large"
|
||||
:disabled="loading"
|
||||
>
|
||||
<VBtn @click="handleDone" prepend-icon="mdi-check" class="px-5" size="large" :disabled="loading">
|
||||
{{ t('dialog.userAuth.authBtn') }}
|
||||
</VBtn>
|
||||
</VCardText>
|
||||
|
||||
@@ -200,7 +200,7 @@ const isMacOS = computed(() => {
|
||||
<VDialog scrollable fullscreen :scrim="false" transition="dialog-bottom-transition">
|
||||
<VCard class="workflow-dialog">
|
||||
<!-- Toolbar -->
|
||||
<VToolbar color="primary">
|
||||
<VToolbar color="primary" density="comfortable">
|
||||
<VToolbarItems>
|
||||
<VBtn icon @click="emit('close')" class="ms-3">
|
||||
<VIcon size="large" color="white" icon="mdi-close" />
|
||||
|
||||
@@ -86,7 +86,13 @@ async function editWorkflow() {
|
||||
|
||||
<template>
|
||||
<VDialog scrollable :close-on-back="false" eager max-width="30rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VCard :title="title">
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-clock-outline" class="me-2" />
|
||||
</template>
|
||||
<VCardTitle>{{ title }}</VCardTitle>
|
||||
</VCardItem>
|
||||
<VDialogCloseBtn @click="emit('close')" />
|
||||
<VDivider />
|
||||
<VCardText>
|
||||
@@ -99,6 +105,7 @@ async function editWorkflow() {
|
||||
:rules="[requiredValidator]"
|
||||
persistent-hint
|
||||
:hint="t('dialog.workflowAddEdit.namePlaceholder')"
|
||||
prepend-inner-icon="mdi-workflow"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
@@ -109,6 +116,7 @@ async function editWorkflow() {
|
||||
placeholder="5位cron表达式"
|
||||
persistent-hint
|
||||
:hint="t('dialog.workflowAddEdit.cronExprDesc')"
|
||||
prepend-inner-icon="mdi-clock-outline"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
@@ -116,6 +124,7 @@ async function editWorkflow() {
|
||||
v-model="workflowForm.description"
|
||||
:label="t('dialog.workflowAddEdit.desc')"
|
||||
:placeholder="t('dialog.workflowAddEdit.descPlaceholder')"
|
||||
prepend-inner-icon="mdi-text-box-outline"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -123,18 +132,10 @@ async function editWorkflow() {
|
||||
</VCardText>
|
||||
<VCardActions class="pt-3">
|
||||
<VSpacer />
|
||||
<VBtn
|
||||
v-if="workflow"
|
||||
block
|
||||
color="primary"
|
||||
variant="elevated"
|
||||
@click="editWorkflow"
|
||||
prepend-icon="mdi-content-save"
|
||||
class="px-5"
|
||||
>
|
||||
<VBtn v-if="workflow" color="primary" @click="editWorkflow" prepend-icon="mdi-content-save" class="px-5">
|
||||
{{ t('dialog.workflowAddEdit.confirm') }}
|
||||
</VBtn>
|
||||
<VBtn v-else block color="primary" variant="elevated" @click="addWorkflow" prepend-icon="mdi-plus" class="px-5">
|
||||
<VBtn v-else color="primary" @click="addWorkflow" prepend-icon="mdi-plus" class="px-5">
|
||||
{{ t('dialog.workflowAddEdit.confirm') }}
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import type { AxiosRequestConfig } from 'axios'
|
||||
import type { PropType } from 'vue'
|
||||
import { useConfirm } from 'vuetify-use-dialog'
|
||||
import { useConfirm } from '@/composables/useConfirm'
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import ReorganizeDialog from '../dialog/ReorganizeDialog.vue'
|
||||
import { formatBytes } from '@core/utils/formatters'
|
||||
@@ -599,9 +599,7 @@ onMounted(() => {
|
||||
</IconBtn>
|
||||
</span>
|
||||
</div>
|
||||
<VCardText v-if="loading" class="text-center flex flex-col items-center">
|
||||
<VProgressCircular size="48" indeterminate color="primary" />
|
||||
</VCardText>
|
||||
<LoadingBanner v-if="loading" />
|
||||
<!-- 文件详情 -->
|
||||
<VCardText v-else-if="isFile && !isImage && items.length > 0" class="text-center break-all">
|
||||
<div v-if="items[0]?.thumbnail" class="flex justify-center">
|
||||
@@ -696,13 +694,24 @@ onMounted(() => {
|
||||
</VCard>
|
||||
<!-- 重命名弹窗 -->
|
||||
<VDialog v-if="renamePopper" v-model="renamePopper" max-width="35rem">
|
||||
<VCard :title="t('file.rename')">
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-pencil" class="me-2" />
|
||||
</template>
|
||||
<VCardTitle>{{ t('file.rename') }}</VCardTitle>
|
||||
</VCardItem>
|
||||
<VDialogCloseBtn @click="renamePopper = false" />
|
||||
<VDivider />
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VTextField v-model="newName" :label="t('file.newName')" :loading="renameLoading" />
|
||||
<VTextField
|
||||
v-model="newName"
|
||||
:label="t('file.newName')"
|
||||
:loading="renameLoading"
|
||||
prepend-inner-icon="mdi-format-text"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" v-if="currentItem && currentItem.type == 'dir'">
|
||||
<VSwitch v-model="renameAll" :label="t('file.includeSubfolders')" />
|
||||
@@ -710,10 +719,10 @@ onMounted(() => {
|
||||
</VRow>
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VBtn color="success" variant="elevated" @click="get_recommend_name" prepend-icon="mdi-magic" class="px-5 me-3">
|
||||
<VBtn color="success" @click="get_recommend_name" prepend-icon="mdi-magic" class="px-5 me-3">
|
||||
{{ t('file.autoRecognizeName') }}
|
||||
</VBtn>
|
||||
<VBtn :disabled="!newName" variant="elevated" @click="rename" prepend-icon="mdi-check" class="px-5 me-3">
|
||||
<VBtn :disabled="!newName" @click="rename" prepend-icon="mdi-check" class="px-5 me-3">
|
||||
{{ t('common.confirm') }}
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
|
||||
@@ -165,21 +165,28 @@ const sortIcon = computed(() => {
|
||||
<IconBtn v-if="pathSegments.length > 0" @click="goUp">
|
||||
<VIcon icon="mdi-arrow-up-bold-outline" />
|
||||
</IconBtn>
|
||||
<!-- 新建文件夹 -->
|
||||
<VDialog v-model="newFolderPopper" max-width="35rem">
|
||||
<template #activator="{ props }">
|
||||
<IconBtn>
|
||||
<VIcon v-bind="props" icon="mdi-folder-plus-outline" />
|
||||
</IconBtn>
|
||||
</template>
|
||||
<VCard :title="t('file.newFolder')">
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-folder-plus-outline" class="me-2" />
|
||||
</template>
|
||||
<VCardTitle>{{ t('file.newFolder') }}</VCardTitle>
|
||||
</VCardItem>
|
||||
<VDialogCloseBtn @click="newFolderPopper = false" />
|
||||
<VDivider />
|
||||
<VCardText>
|
||||
<VTextField v-model="newFolderName" :label="t('common.name')" />
|
||||
<VTextField v-model="newFolderName" :label="t('common.name')" prepend-inner-icon="mdi-format-text" />
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<div class="flex-grow-1" />
|
||||
<VBtn :disabled="!newFolderName" variant="elevated" @click="mkdir" prepend-icon="mdi-check" class="px-5 me-3">
|
||||
<VBtn :disabled="!newFolderName" @click="mkdir" prepend-icon="mdi-folder-plus" class="px-5 me-3">
|
||||
{{ t('common.create') }}
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
|
||||
@@ -32,7 +32,7 @@ async function loadDownloaderSetting() {
|
||||
})),
|
||||
]
|
||||
} catch (error) {
|
||||
console.error('加载下载器设置失败:', error)
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ onMounted(() => {
|
||||
<VTextField
|
||||
v-model="data.labels"
|
||||
:label="t('workflow.addDownload.category')"
|
||||
placeholder="多个使用,分隔"
|
||||
:placeholder="t('workflow.addDownload.categoryPlaceholder')"
|
||||
outlined
|
||||
dense
|
||||
/>
|
||||
@@ -80,7 +80,7 @@ onMounted(() => {
|
||||
storage="local"
|
||||
:label="t('workflow.addDownload.savePath')"
|
||||
clearable
|
||||
placeholder="留空自动"
|
||||
:placeholder="t('workflow.addDownload.savePathPlaceholder')"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
|
||||
@@ -100,7 +100,7 @@ const sourceTypeOptions = [
|
||||
]
|
||||
|
||||
// 计算下拉框
|
||||
const sourceOptions = computed(() => innerList.map(item => item.name))
|
||||
const sourceOptions = computed(() => innerList.map(item => ({ value: item.api_path, title: item.name })))
|
||||
|
||||
onMounted(() => {
|
||||
loadExtraRecommendSources()
|
||||
|
||||
129
src/components/workflow/InvokePluginAction.vue
Normal file
129
src/components/workflow/InvokePluginAction.vue
Normal file
@@ -0,0 +1,129 @@
|
||||
<script setup lang="ts">
|
||||
import api from '@/api'
|
||||
import { Handle, Position } from '@vue-flow/core'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
data: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
interface ActionItem {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
interface PluginAction {
|
||||
plugin_id: string
|
||||
plugin_name: string
|
||||
actions: ActionItem[]
|
||||
}
|
||||
|
||||
// 插件所有动作
|
||||
const pluginActions = ref<PluginAction[]>([])
|
||||
|
||||
// 插件选项
|
||||
const pluginOptions = computed(() => {
|
||||
return pluginActions.value.map((item: PluginAction) => ({
|
||||
title: item.plugin_name,
|
||||
value: item.plugin_id,
|
||||
}))
|
||||
})
|
||||
|
||||
// 动作选项
|
||||
const actionOptions = computed(() => {
|
||||
return pluginActions.value
|
||||
.find((item: PluginAction) => item.plugin_id === props.data.plugin_id)
|
||||
?.actions.map((item: ActionItem) => ({
|
||||
title: item.name,
|
||||
value: item.id,
|
||||
}))
|
||||
})
|
||||
|
||||
// 用于在文本框显示和保存时转换action_params
|
||||
const actionParamsText = computed({
|
||||
get: () => {
|
||||
try {
|
||||
return typeof props.data.action_params === 'object'
|
||||
? JSON.stringify(props.data.action_params, null, 2)
|
||||
: props.data.action_params || ''
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
return ''
|
||||
}
|
||||
},
|
||||
set: (value: string) => {
|
||||
try {
|
||||
props.data.action_params = value ? JSON.parse(value) : {}
|
||||
} catch (error) {
|
||||
// 如果JSON解析失败,保留原始文本
|
||||
props.data.action_params = value
|
||||
console.error(error)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// 加载动作选项
|
||||
async function loadPluginActions() {
|
||||
try {
|
||||
pluginActions.value = await api.get('workflow/plugin/actions')
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadPluginActions()
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<VCard max-width="20rem">
|
||||
<Handle id="edge_in" type="target" :position="Position.Left" />
|
||||
<VCardItem>
|
||||
<template v-slot:prepend>
|
||||
<VAvatar>
|
||||
<VIcon icon="mdi-run" size="x-large"></VIcon>
|
||||
</VAvatar>
|
||||
</template>
|
||||
<VCardTitle>{{ t('workflow.invokePlugin.title') }}</VCardTitle>
|
||||
<VCardSubtitle>{{ t('workflow.invokePlugin.subtitle') }}</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VDivider />
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VSelect
|
||||
v-model="data.plugin_id"
|
||||
:items="pluginOptions"
|
||||
:label="t('workflow.invokePlugin.plugin')"
|
||||
outlined
|
||||
dense
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VSelect
|
||||
v-model="data.action_id"
|
||||
:items="actionOptions"
|
||||
:label="t('workflow.invokePlugin.actionid')"
|
||||
outlined
|
||||
dense
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VTextarea v-model="actionParamsText" :label="t('workflow.invokePlugin.actionParams')" outlined dense />
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
<Handle id="edge_out" type="source" :position="Position.Right" />
|
||||
</VCard>
|
||||
</div>
|
||||
</template>
|
||||
88
src/composables/useConfirm.ts
Normal file
88
src/composables/useConfirm.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { ref } from 'vue'
|
||||
import { createApp } from 'vue'
|
||||
import i18n from '@/plugins/i18n'
|
||||
import vuetify from '@/plugins/vuetify'
|
||||
import ConfirmDialog from '@/@core/components/ConfirmDialog.vue'
|
||||
import DialogCloseBtn from '@/@core/components/DialogCloseBtn.vue'
|
||||
|
||||
interface ConfirmOptions {
|
||||
type?: 'info' | 'warn' | 'error'
|
||||
title?: string
|
||||
content?: string
|
||||
confirmText?: string
|
||||
cancelText?: string
|
||||
width?: string | number
|
||||
}
|
||||
|
||||
let resolvePromise: ((value: boolean) => void) | null = null
|
||||
|
||||
// 创建确认对话框实例
|
||||
async function createConfirmDialog(options: ConfirmOptions = {}) {
|
||||
return new Promise<boolean>(resolve => {
|
||||
resolvePromise = resolve
|
||||
|
||||
// 创建容器
|
||||
const container = document.createElement('div')
|
||||
document.body.appendChild(container)
|
||||
|
||||
// 处理国际化
|
||||
const i18nOptions = {
|
||||
...options,
|
||||
title: options.title || i18n.global.t('common.confirm'),
|
||||
confirmText: options.confirmText || i18n.global.t('common.confirm'),
|
||||
cancelText: options.cancelText || i18n.global.t('common.cancel'),
|
||||
}
|
||||
|
||||
// 创建应用实例
|
||||
const app = createApp(ConfirmDialog, {
|
||||
modelValue: true,
|
||||
...i18nOptions,
|
||||
'onUpdate:modelValue': (val: boolean) => {
|
||||
if (!val) {
|
||||
cleanup()
|
||||
}
|
||||
},
|
||||
onConfirm: () => {
|
||||
resolvePromise?.(true)
|
||||
cleanup()
|
||||
},
|
||||
onCancel: () => {
|
||||
resolvePromise?.(false)
|
||||
cleanup()
|
||||
},
|
||||
})
|
||||
|
||||
// 注册必要的组件
|
||||
app.component('VDialogCloseBtn', DialogCloseBtn)
|
||||
|
||||
// 使用插件
|
||||
app.use(vuetify)
|
||||
app.use(i18n)
|
||||
|
||||
// 挂载应用
|
||||
app.mount(container)
|
||||
|
||||
// 清理函数
|
||||
const cleanup = () => {
|
||||
app.unmount()
|
||||
document.body.removeChild(container)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 创建一个函数对象,同时支持直接调用和解构
|
||||
const confirmFunction = Object.assign(createConfirmDialog, {
|
||||
createConfirm: createConfirmDialog,
|
||||
})
|
||||
|
||||
// 导出 useConfirm 函数
|
||||
export function useConfirm() {
|
||||
return confirmFunction
|
||||
}
|
||||
|
||||
// 插件
|
||||
export default {
|
||||
install: (app: any) => {
|
||||
app.provide('confirm', { createConfirm: createConfirmDialog })
|
||||
},
|
||||
}
|
||||
@@ -183,8 +183,8 @@ const showDynamicButton = computed(() => {
|
||||
.footer-nav-card {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
backdrop-filter: blur(12px);
|
||||
background-color: rgba(var(--v-theme-surface), 0.8);
|
||||
backdrop-filter: blur(16px);
|
||||
background-color: rgba(var(--v-theme-surface), 0.6);
|
||||
pointer-events: auto;
|
||||
transition: all 0.5s cubic-bezier(0.25, 1, 0.5, 1);
|
||||
|
||||
|
||||
@@ -203,7 +203,13 @@ onMounted(() => {
|
||||
</VCard>
|
||||
</VMenu>
|
||||
<!-- 名称测试弹窗 -->
|
||||
<VDialog v-if="nameTestDialog" v-model="nameTestDialog" max-width="35rem" scrollable>
|
||||
<VDialog
|
||||
v-if="nameTestDialog"
|
||||
v-model="nameTestDialog"
|
||||
max-width="45rem"
|
||||
scrollable
|
||||
:fullscreen="!display.mdAndUp.value"
|
||||
>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>
|
||||
@@ -219,7 +225,13 @@ onMounted(() => {
|
||||
</VCard>
|
||||
</VDialog>
|
||||
<!-- 网络测试弹窗 -->
|
||||
<VDialog v-if="netTestDialog" v-model="netTestDialog" max-width="35rem" max-height="85vh" scrollable>
|
||||
<VDialog
|
||||
v-if="netTestDialog"
|
||||
v-model="netTestDialog"
|
||||
max-width="35rem"
|
||||
scrollable
|
||||
:fullscreen="!display.mdAndUp.value"
|
||||
>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>
|
||||
@@ -258,12 +270,18 @@ onMounted(() => {
|
||||
</VCardItem>
|
||||
<VDivider />
|
||||
<VCardText>
|
||||
<LoggingView />
|
||||
<LoggingView logfile="moviepilot.log" />
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
<!-- 过滤规则弹窗 -->
|
||||
<VDialog v-if="ruleTestDialog" v-model="ruleTestDialog" max-width="35rem" scrollable>
|
||||
<VDialog
|
||||
v-if="ruleTestDialog"
|
||||
v-model="ruleTestDialog"
|
||||
max-width="35rem"
|
||||
scrollable
|
||||
:fullscreen="!display.mdAndUp.value"
|
||||
>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>
|
||||
@@ -279,7 +297,13 @@ onMounted(() => {
|
||||
</VCard>
|
||||
</VDialog>
|
||||
<!-- 系统健康检查弹窗 -->
|
||||
<VDialog v-if="systemTestDialog" v-model="systemTestDialog" max-width="35rem" scrollable>
|
||||
<VDialog
|
||||
v-if="systemTestDialog"
|
||||
v-model="systemTestDialog"
|
||||
max-width="35rem"
|
||||
scrollable
|
||||
:fullscreen="!display.mdAndUp.value"
|
||||
>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>
|
||||
@@ -298,7 +322,7 @@ onMounted(() => {
|
||||
<VDialog
|
||||
v-if="messageDialog"
|
||||
v-model="messageDialog"
|
||||
max-width="40rem"
|
||||
max-width="50rem"
|
||||
scrollable
|
||||
:fullscreen="!display.mdAndUp.value"
|
||||
ref="messageDialogRef"
|
||||
|
||||
@@ -13,6 +13,7 @@ import { checkPrefersColorSchemeIsDark } from '@/@core/utils'
|
||||
import { getCurrentLocale, setI18nLanguage } from '@/plugins/i18n'
|
||||
import { saveLocalTheme } from '@/@core/utils/theme'
|
||||
import type { ThemeSwitcherTheme } from '@layouts/types'
|
||||
import { useConfirm } from '@/composables/useConfirm'
|
||||
|
||||
// 认证 Store
|
||||
const authStore = useAuthStore()
|
||||
@@ -32,9 +33,6 @@ const progressDialog = ref(false)
|
||||
// 站点认证对话框
|
||||
const siteAuthDialog = ref(false)
|
||||
|
||||
// 重启确认对话框
|
||||
const restartDialog = ref(false)
|
||||
|
||||
// 自定义CSS弹窗
|
||||
const cssDialog = ref(false)
|
||||
|
||||
@@ -47,6 +45,9 @@ const showLanguageMenu = ref(false)
|
||||
// 自定义CSS
|
||||
const customCSS = ref('')
|
||||
|
||||
// 确认框
|
||||
const { createConfirm } = useConfirm()
|
||||
|
||||
// 执行注销操作
|
||||
function logout() {
|
||||
// 清除登录状态信息
|
||||
@@ -57,7 +58,6 @@ function logout() {
|
||||
|
||||
// 执行重启操作
|
||||
async function restart() {
|
||||
restartDialog.value = false
|
||||
// 调用API重启
|
||||
try {
|
||||
// 显示等待框
|
||||
@@ -79,7 +79,15 @@ async function restart() {
|
||||
|
||||
// 显示重启确认对话框
|
||||
async function showRestartDialog() {
|
||||
restartDialog.value = true
|
||||
const isConfirmed = await createConfirm({
|
||||
type: 'warn',
|
||||
title: t('app.confirmRestart'),
|
||||
content: t('app.restartTip'),
|
||||
})
|
||||
|
||||
if (!isConfirmed) return
|
||||
|
||||
await restart()
|
||||
}
|
||||
|
||||
// 显示站点认证对话框
|
||||
@@ -417,32 +425,6 @@ onMounted(() => {
|
||||
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="t('app.restarting')" />
|
||||
<!-- 用户认证对话框 -->
|
||||
<UserAuthDialog v-if="siteAuthDialog" v-model="siteAuthDialog" @done="siteAuthDone" @close="siteAuthDialog = false" />
|
||||
<!-- 重启确认对话框 -->
|
||||
<VDialog v-if="restartDialog" v-model="restartDialog" max-width="25rem">
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<div class="d-flex align-center justify-center mt-3">
|
||||
<VAvatar color="warning" variant="text" size="x-large">
|
||||
<VIcon size="x-large" icon="mdi-alert" />
|
||||
</VAvatar>
|
||||
<div class="ms-3">
|
||||
<p class="font-weight-bold text-xl text-high-emphasis">{{ t('app.confirmRestart') }}</p>
|
||||
<p>{{ t('app.restartTip') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</VCardItem>
|
||||
<VCardActions class="mx-auto">
|
||||
<VBtn variant="tonal" color="secondary" class="px-5" @click="restartDialog = false">{{
|
||||
t('common.cancel')
|
||||
}}</VBtn>
|
||||
<VBtn variant="elevated" color="error" @click="restart" prepend-icon="mdi-restart" class="px-5">{{
|
||||
t('common.confirm')
|
||||
}}</VBtn>
|
||||
</VCardActions>
|
||||
<VDialogCloseBtn @click="restartDialog = false" />
|
||||
</VCard>
|
||||
</VDialog>
|
||||
|
||||
<!-- 自定义 CSS -->
|
||||
<VDialog v-if="cssDialog" v-model="cssDialog" max-width="50rem" scrollable :fullscreen="!display.mdAndUp.value">
|
||||
<VCard>
|
||||
|
||||
@@ -71,6 +71,7 @@ function getActionIcon(type: string): string {
|
||||
'SendEventAction': 'mdi-send-check',
|
||||
'SendMessageAction': 'mdi-message-arrow-right',
|
||||
'TransferFileAction': 'mdi-file-move',
|
||||
'InvokePluginAction': 'mdi-run',
|
||||
}
|
||||
|
||||
return iconMap[type] || 'mdi-puzzle-outline'
|
||||
|
||||
@@ -39,6 +39,11 @@ export default {
|
||||
unsubscribe: 'Unsubscribe',
|
||||
media: 'Media',
|
||||
unknown: 'Unknown',
|
||||
notice: 'Notice',
|
||||
itemsPerPage: 'Items per page',
|
||||
pageText: '{0}-{1} of {2}',
|
||||
noDataText: 'No data',
|
||||
loadingText: 'Loading...',
|
||||
},
|
||||
mediaType: {
|
||||
movie: 'Movie',
|
||||
@@ -71,6 +76,7 @@ export default {
|
||||
sendEvent: 'Send Event',
|
||||
sendMessage: 'Send Message',
|
||||
transferFile: 'Transfer File',
|
||||
invokePlugin: 'Invoke Plugin',
|
||||
},
|
||||
qualityOptions: {
|
||||
all: 'All',
|
||||
@@ -200,6 +206,10 @@ export default {
|
||||
title: 'Services',
|
||||
description: 'Scheduled jobs',
|
||||
},
|
||||
cache: {
|
||||
title: 'Cache',
|
||||
description: 'Torrent cache, media recognition data cache, image file cache management',
|
||||
},
|
||||
notification: {
|
||||
title: 'Notifications',
|
||||
description: 'Notification channels (WeChat, Telegram, Slack, SynologyChat, VoceChat, WebPush), message scope',
|
||||
@@ -278,6 +288,9 @@ export default {
|
||||
nameHint: 'Name of notification channel',
|
||||
type: 'Type',
|
||||
typeHint: 'Type of notification channel',
|
||||
customTypeHint: 'Custom notification type, used for plugin implementation scenarios',
|
||||
customTypePlaceholder: 'custom',
|
||||
nameRequired: 'Please enter name',
|
||||
enabled: 'Enabled',
|
||||
config: 'Configuration',
|
||||
wechat: {
|
||||
@@ -427,6 +440,8 @@ export default {
|
||||
forceResume: 'Force Resume',
|
||||
firstLastPiece: 'First Last Piece',
|
||||
onlyLack: 'Only Download Lack Resource',
|
||||
categoryPlaceholder: 'Use comma to separate multiple',
|
||||
savePathPlaceholder: 'Leave empty for auto',
|
||||
},
|
||||
addSubscribe: {
|
||||
title: 'Add Subscribe',
|
||||
@@ -534,6 +549,14 @@ export default {
|
||||
exclude: 'Exclude (Keywords, Regex)',
|
||||
ruleGroups: 'Filter Rule Groups',
|
||||
},
|
||||
invokePlugin: {
|
||||
title: 'Invoke Plugin',
|
||||
subtitle: 'Call plugin to perform specific actions',
|
||||
plugin: 'Plugin',
|
||||
actionid: 'Action ID',
|
||||
actionParams: 'Action Parameters',
|
||||
loadPluginSettingFailed: 'Failed to load plugin settings',
|
||||
},
|
||||
title: 'Workflow',
|
||||
noWorkflow: 'No Workflow',
|
||||
noWorkflowDescription: 'Click the add button to create a workflow task.',
|
||||
@@ -838,8 +861,8 @@ export default {
|
||||
browserSimulation: 'Use browser simulation for authentic site access',
|
||||
},
|
||||
actions: {
|
||||
add: 'Add',
|
||||
edit: 'Edit',
|
||||
add: 'Add Site',
|
||||
edit: 'Edit Site',
|
||||
},
|
||||
messages: {
|
||||
addSuccess: 'Site added successfully',
|
||||
@@ -1011,6 +1034,8 @@ export default {
|
||||
scrapFollowTmdb: 'Follow TMDB Recognition',
|
||||
scrapFollowTmdbHint:
|
||||
'When turned off, organization history will be used (if available) to avoid TMDB data changes during subscription',
|
||||
scrapOriginalImage: 'Scrap TheMovieDb Original Language Image',
|
||||
scrapOriginalImageHint: 'Scrap original language image from themoviedb, otherwise scrap metadata language image',
|
||||
fanartEnable: 'Fanart Image Data Source',
|
||||
fanartEnableHint: 'Use image data from fanart.tv',
|
||||
githubProxy: 'Github Acceleration Proxy',
|
||||
@@ -1067,13 +1092,28 @@ export default {
|
||||
bing: 'Bing Daily Wallpaper',
|
||||
mediaserver: 'Media Server',
|
||||
none: 'No Wallpaper',
|
||||
customize: 'Customize',
|
||||
},
|
||||
mb: 'MB',
|
||||
hour: 'hour',
|
||||
customizeWallpaperApi: 'Customize Wallpaper Api',
|
||||
customizeWallpaperApiHint:
|
||||
'It will get the image file extension format images that are allowed in settings in the content returned by the API.',
|
||||
customizeWallpaperApiRequired: 'Required field; please enter Wallpaper API',
|
||||
securityImageDomains: 'Security Image Domains',
|
||||
securityImageDomainsHint: 'Allowed image domains whitelist for caching, used to control trusted image sources',
|
||||
noSecurityImageDomains: 'No security domains',
|
||||
securityImageDomainAdd: 'Add domain, e.g.: image.tmdb.org',
|
||||
proxyHost: 'Proxy Server',
|
||||
proxyHostHint: 'Set proxy server address, support: http(s), socks5, socks5h, etc.',
|
||||
moviePilotAutoUpdate: 'Auto Update MoviePilot',
|
||||
moviePilotAutoUpdateHint: 'Automatically update MoviePilot to the latest release version when restarting',
|
||||
autoUpdateResource: 'Auto Update Resource',
|
||||
autoUpdateResourceHint: 'Automatically detect and update site resource package when restarting',
|
||||
},
|
||||
site: {
|
||||
siteSync: 'Site Synchronization',
|
||||
siteSyncDesc: 'Quickly sync site data from CookieCloud.',
|
||||
siteSyncDesc: 'Quickly sync site data from CookieCloud',
|
||||
enableLocalCookieCloud: 'Enable Local CookieCloud Server',
|
||||
enableLocalCookieCloudHint:
|
||||
'Use built-in CookieCloud service to sync site data, service address: http://localhost:3000/cookiecloud',
|
||||
@@ -1119,7 +1159,7 @@ export default {
|
||||
},
|
||||
notification: {
|
||||
channels: 'Notification Channels',
|
||||
channelsDesc: 'Set message sending channel parameters.',
|
||||
channelsDesc: 'Set message sending channel parameters',
|
||||
organizeSuccess: 'Media Import',
|
||||
downloadAdded: 'Download Added',
|
||||
subscribeAdded: 'Subscribe Added',
|
||||
@@ -1162,10 +1202,11 @@ export default {
|
||||
synologyChat: 'SynologyChat',
|
||||
voceChat: 'VoceChat',
|
||||
webPush: 'WebPush',
|
||||
custom: 'Custom Notification',
|
||||
},
|
||||
words: {
|
||||
customIdentifiers: 'Custom Identifiers',
|
||||
identifiersDesc: 'Add rules to preprocess torrent names or file names to correct identification.',
|
||||
identifiersDesc: 'Add rules to preprocess torrent names or file names to correct identification',
|
||||
identifiersPlaceholder: 'Support regular expressions, special characters need \\ escape, one line for each rule',
|
||||
identifiersHint: 'Support regular expressions, special characters need \\ escape, one line for each rule',
|
||||
formatTitle: 'Supported configuration formats (mind the spaces):',
|
||||
@@ -1174,7 +1215,7 @@ export default {
|
||||
'Word to replace => Replacement\n' +
|
||||
'Front word <> Back word >> Episode offset (EP)\n' +
|
||||
'Word to replace => Replacement && Front word <> Back word >> Episode offset (EP)\n' +
|
||||
'Replacement format supports: {tmdbid/doubanid=xxx;type=movie/tv;s=xxx;e=xxx} to directly specify TMDBID/Douban ID, where s and e are season and episode numbers (optional)',
|
||||
'Replacement format supports: {[tmdbid/doubanid=xxx;type=movie/tv;s=xxx;e=xxx]} to directly specify TMDBID/Douban ID, where s and e are season and episode numbers (optional)',
|
||||
identifierSaveSuccess: 'Custom identifiers saved successfully',
|
||||
identifierSaveFailed: 'Failed to save custom identifiers!',
|
||||
|
||||
@@ -1205,7 +1246,7 @@ export default {
|
||||
},
|
||||
search: {
|
||||
basicSettings: 'Basic Settings',
|
||||
basicSettingsDesc: 'Set data sources, rule groups and other basic information.',
|
||||
basicSettingsDesc: 'Set data sources, rule groups and other basic information',
|
||||
recognizeSource: 'Recognition Data Source',
|
||||
recognizeSourceDesc:
|
||||
'Default is TMDB. Douban is usually more friendly for Chinese works, but some foreign works have incomplete information.',
|
||||
@@ -1306,8 +1347,7 @@ export default {
|
||||
},
|
||||
scheduler: {
|
||||
title: 'Scheduled Jobs',
|
||||
subtitle:
|
||||
"Includes built-in system services and plugin services. Manual execution will not affect the job's normal schedule.",
|
||||
subtitle: 'Includes built-in system services and plugin services',
|
||||
provider: 'Provider',
|
||||
taskName: 'Task Name',
|
||||
taskStatus: 'Task Status',
|
||||
@@ -1354,6 +1394,55 @@ export default {
|
||||
settingsSaveSuccess: 'Subscription basic settings saved successfully',
|
||||
settingsSaveFailed: 'Failed to save subscription basic settings!',
|
||||
},
|
||||
cache: {
|
||||
title: 'Cache Management',
|
||||
subtitle: 'Manage torrent cache data',
|
||||
filterByTitle: 'Filter by Title',
|
||||
filterBySite: 'Filter by Site',
|
||||
selectSite: 'Select Site',
|
||||
refresh: 'Refresh Cache',
|
||||
deleteSelected: 'Delete Selected',
|
||||
clearAll: 'Clear All Cache',
|
||||
refreshSuccess: 'Cache refresh completed',
|
||||
refreshFailed: 'Failed to refresh cache',
|
||||
clearSuccess: 'Cache clear completed',
|
||||
clearFailed: 'Failed to clear cache',
|
||||
deleteSuccess: 'Cache item deleted successfully',
|
||||
deleteFailed: 'Failed to delete cache item',
|
||||
deleteSelectedSuccess: 'Successfully deleted {count} cache items',
|
||||
deleteSelectedFailed: 'Failed to delete cache items',
|
||||
loadFailed: 'Failed to load cache data',
|
||||
selectDeleteWarning: 'Please select cache items to delete',
|
||||
reidentify: 'Re-identify',
|
||||
reidentifySuccess: 'Re-identification completed',
|
||||
reidentifyFailed: 'Re-identification failed',
|
||||
poster: 'Poster',
|
||||
torrentTitle: 'Title',
|
||||
site: 'Site',
|
||||
size: 'Size',
|
||||
publishTime: 'Publish Time',
|
||||
recognitionResult: 'Recognition Result',
|
||||
actions: 'Actions',
|
||||
unrecognized: 'Unrecognized',
|
||||
noData: 'No cache data',
|
||||
noDataHint: 'Click "Refresh Cache" button to get the latest torrent cache',
|
||||
reidentifyDialog: {
|
||||
title: 'Re-identify',
|
||||
torrentInfo: 'Torrent Info',
|
||||
tmdbId: 'TMDB ID',
|
||||
tmdbIdHint: 'Optional, manually specify TMDB ID for recognition',
|
||||
doubanId: 'Douban ID',
|
||||
doubanIdHint: 'Optional, manually specify Douban ID for recognition',
|
||||
autoHint: 'If no ID is specified, the torrent will be automatically re-identified',
|
||||
cancel: 'Cancel',
|
||||
confirm: 'Re-identify',
|
||||
},
|
||||
mediaType: {
|
||||
movie: 'Movie',
|
||||
tv: 'TV Show',
|
||||
},
|
||||
clearConfirm: 'Are you sure you want to clear all cache?',
|
||||
},
|
||||
},
|
||||
dialog: {
|
||||
progress: {
|
||||
@@ -1611,7 +1700,7 @@ export default {
|
||||
title: 'Plugin Market Settings',
|
||||
repoUrl: 'Plugin Repository URL',
|
||||
repoPlaceholder: 'Format: https://github.com/jxxghp/MoviePilot-Plugins/,https://github.com/xxxx/xxxxxx/',
|
||||
repoHint: 'Multiple URLs separated by commas, only Github repositories are supported',
|
||||
repoHint: 'Multiple URLs separated by lines, only Github repositories are supported',
|
||||
close: 'Close',
|
||||
save: 'Save',
|
||||
saveSuccess: 'Plugin repository saved successfully',
|
||||
@@ -1677,8 +1766,8 @@ export default {
|
||||
previous: 'Previous',
|
||||
confirm: 'Confirm',
|
||||
manualTitle: 'Manual Organization',
|
||||
multipleItemsTitle: 'Organize - {count} Items',
|
||||
singleItemTitle: 'Organize - {path}',
|
||||
multipleItemsTitle: '{count} Items',
|
||||
singleItemTitle: '{path}',
|
||||
targetStorage: 'Target Storage',
|
||||
targetStorageHint: 'Organization target storage',
|
||||
transferType: 'Organization Method',
|
||||
@@ -1727,7 +1816,7 @@ export default {
|
||||
},
|
||||
subscribeEdit: {
|
||||
titleDefault: 'Default Subscription Rules',
|
||||
titleEditFormat: 'Edit Subscription - {name} {season}',
|
||||
titleEdit: 'Edit Subscription',
|
||||
seasonFormat: 'Season {number}',
|
||||
tabs: {
|
||||
basic: 'Basic',
|
||||
@@ -1840,6 +1929,7 @@ export default {
|
||||
peersColumn: 'Peers',
|
||||
viewDetails: 'View Details',
|
||||
downloadTorrent: 'Download Torrent',
|
||||
pageText: '{0}-{1} of {2} items',
|
||||
},
|
||||
forkSubscribe: {
|
||||
title: 'Copy Subscription',
|
||||
@@ -1972,6 +2062,54 @@ export default {
|
||||
updateHistoryTitle: '{name} Update History',
|
||||
updateToLatest: 'Update to Latest Version',
|
||||
updatingTo: 'Updating {name} to v{version} ...',
|
||||
folderNameEmpty: 'Folder name cannot be empty',
|
||||
folderExists: 'Folder already exists',
|
||||
folderCreateSuccess: 'Folder created successfully',
|
||||
folderRenameSuccess: 'Folder renamed successfully',
|
||||
folderRenameFailed: 'Failed to rename folder',
|
||||
folderDeleteSuccess: 'Folder deleted successfully',
|
||||
folderDeleteFailed: 'Failed to delete folder',
|
||||
removeFromFolderSuccess: 'Plugin removed from folder',
|
||||
operationFailed: 'Operation failed',
|
||||
saveFolderConfigFailed: 'Failed to save folder config',
|
||||
newFolder: 'New Folder',
|
||||
folderName: 'Folder Name',
|
||||
cancel: 'Cancel',
|
||||
create: 'Create',
|
||||
clone: 'Clone',
|
||||
cloneTitle: 'Create Plugin Clone',
|
||||
cloneSubtitle: 'Create an independent clone instance for {name}',
|
||||
cloneFeature: 'Plugin Clone Feature',
|
||||
cloneDescription:
|
||||
'Create an independent copy of the plugin with separate configuration and data, suitable for multi-account, testing environments, etc.',
|
||||
suffix: 'Clone Suffix',
|
||||
suffixPlaceholder: 'e.g.: Test, Backup, Site1',
|
||||
suffixHint: 'Unique identifier to distinguish clones, only letters and numbers allowed',
|
||||
suffixRequired: 'Clone suffix cannot be empty',
|
||||
suffixFormatError: 'Only letters and numbers allowed',
|
||||
suffixLengthError: 'Length cannot exceed 20 characters',
|
||||
cloneName: 'Clone Name',
|
||||
cloneNamePlaceholder: 'e.g.: Auto Backup Test Version',
|
||||
cloneNameHint: 'Display name for the clone plugin (optional)',
|
||||
cloneDefaultName: '{name} Clone',
|
||||
cloneDescriptionLabel: 'Clone Description',
|
||||
cloneDescriptionPlaceholder: 'Describe the purpose and features of this clone...',
|
||||
cloneDescriptionHint: 'Detailed description of the clone plugin purpose (optional)',
|
||||
cloneDefaultDescription: '{description} (Clone Version)',
|
||||
cloneVersion: 'Version',
|
||||
cloneVersionPlaceholder: 'e.g.: 1.0, 2.1.0',
|
||||
cloneVersionHint: 'Custom version number for the clone plugin (optional)',
|
||||
cloneIcon: 'Icon URL',
|
||||
cloneIconPlaceholder: 'https://example.com/icon.png',
|
||||
cloneIconHint: 'Custom icon for the clone plugin (optional)',
|
||||
cloneNotice:
|
||||
'Clone plugins are disabled by default after creation and need to be manually configured and enabled. The clone suffix cannot be modified once set.',
|
||||
createClone: 'Create Clone',
|
||||
cloning: 'Creating clone for {name}...',
|
||||
cloneSuccess: 'Plugin clone {name} created successfully!',
|
||||
cloneFailed: 'Plugin clone creation failed: {message}',
|
||||
cloneFailedGeneral: 'Plugin clone creation failed',
|
||||
logTitle: 'Plugin Logging',
|
||||
},
|
||||
profile: {
|
||||
personalInfo: 'Personal Information',
|
||||
@@ -2354,4 +2492,23 @@ export default {
|
||||
required: 'This field is required',
|
||||
number: 'Please enter a number',
|
||||
},
|
||||
folder: {
|
||||
settingAppearance: 'Appearance Settings',
|
||||
rename: 'Rename',
|
||||
deleteFolder: 'Delete Folder',
|
||||
folderNameCannotBeEmpty: 'Folder name cannot be empty',
|
||||
confirmDeleteFolder:
|
||||
'Are you sure you want to delete folder "{folderName}"? Plugins in this folder will be moved back to the main list.',
|
||||
folderSettingsSaved: 'Folder settings saved',
|
||||
renameFolder: 'Rename Folder',
|
||||
folderName: 'Folder Name',
|
||||
folderAppearanceSettings: 'Folder Appearance Settings',
|
||||
showFolderIcon: 'Show Folder Icon',
|
||||
icon: 'Icon',
|
||||
iconColor: 'Icon Color',
|
||||
backgroundGradient: 'Background Gradient',
|
||||
customBackgroundImageURL: 'Custom Background Image URL (Optional)',
|
||||
customBackgroundImageHint: 'Supports web image URLs, leave blank for gradient background',
|
||||
pluginCount: '{count} Plugins',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -39,6 +39,11 @@ export default {
|
||||
unsubscribe: '取消订阅',
|
||||
media: '媒体',
|
||||
unknown: '未知',
|
||||
notice: '注意',
|
||||
itemsPerPage: '每页条数',
|
||||
pageText: '{0}-{1} 共 {2} 条',
|
||||
noDataText: '没有数据',
|
||||
loadingText: '加载中...',
|
||||
},
|
||||
mediaType: {
|
||||
movie: '电影',
|
||||
@@ -71,6 +76,7 @@ export default {
|
||||
sendEvent: '发送事件',
|
||||
sendMessage: '发送消息',
|
||||
transferFile: '整理文件',
|
||||
invokePlugin: '调用插件',
|
||||
},
|
||||
qualityOptions: {
|
||||
all: '全部',
|
||||
@@ -200,6 +206,10 @@ export default {
|
||||
title: '服务',
|
||||
description: '定时作业',
|
||||
},
|
||||
cache: {
|
||||
title: '缓存',
|
||||
description: '种子缓存、图片文件缓存管理',
|
||||
},
|
||||
notification: {
|
||||
title: '通知',
|
||||
description: '通知渠道(微信、Telegram、Slack、SynologyChat、VoceChat、WebPush)、消息发送范围',
|
||||
@@ -277,6 +287,9 @@ export default {
|
||||
nameHint: '通知渠道名称',
|
||||
type: '类型',
|
||||
typeHint: '通知渠道类型',
|
||||
customTypeHint: '自定义通知类型,用于插件实现场景',
|
||||
customTypePlaceholder: 'custom',
|
||||
nameRequired: '请输入名称',
|
||||
enabled: '启用',
|
||||
config: '配置',
|
||||
wechat: {
|
||||
@@ -425,6 +438,8 @@ export default {
|
||||
forceResume: '强制继续',
|
||||
firstLastPiece: '优先首尾文件',
|
||||
onlyLack: '仅下载缺失资源',
|
||||
categoryPlaceholder: '多个使用,分隔',
|
||||
savePathPlaceholder: '留空自动',
|
||||
},
|
||||
addSubscribe: {
|
||||
title: '添加订阅',
|
||||
@@ -532,6 +547,14 @@ export default {
|
||||
exclude: '排除(关键字、正则式)',
|
||||
ruleGroups: '过滤规则组',
|
||||
},
|
||||
invokePlugin: {
|
||||
title: '调用插件',
|
||||
subtitle: '调用插件执行特定操作',
|
||||
plugin: '插件',
|
||||
actionid: '动作ID',
|
||||
actionParams: '动作参数',
|
||||
loadPluginSettingFailed: '加载插件设置失败',
|
||||
},
|
||||
title: '工作流',
|
||||
noWorkflow: '没有工作流',
|
||||
noWorkflowDescription: '点击添加按钮创建工作流任务。',
|
||||
@@ -835,8 +858,8 @@ export default {
|
||||
browserSimulation: '使用浏览器模拟真实访问该站点',
|
||||
},
|
||||
actions: {
|
||||
add: '新增',
|
||||
edit: '编辑',
|
||||
add: '新增站点',
|
||||
edit: '编辑站点',
|
||||
},
|
||||
messages: {
|
||||
addSuccess: '新增站点成功',
|
||||
@@ -922,7 +945,7 @@ export default {
|
||||
system: {
|
||||
custom: '自定义',
|
||||
basicSettings: '基础设置',
|
||||
basicSettingsDesc: '设置服务器的全局功能。',
|
||||
basicSettingsDesc: '设置服务器的全局功能',
|
||||
appDomain: '访问域名',
|
||||
appDomainHint: '用于发送通知时,添加快捷跳转地址',
|
||||
wallpaper: '背景壁纸',
|
||||
@@ -1005,6 +1028,8 @@ export default {
|
||||
metaCacheExpireMin: '元数据缓存时间必须大于等于0',
|
||||
scrapFollowTmdb: '跟随TMDB识别整理',
|
||||
scrapFollowTmdbHint: '关闭时以整理历史记录为准(如有),避免TMDB数据在订阅中途修改',
|
||||
scrapOriginalImage: 'TMDB 刮削原语种图片',
|
||||
scrapOriginalImageHint: '刮削原语种图片,否则刮削元数据语种图片',
|
||||
fanartEnable: 'Fanart图片数据源',
|
||||
fanartEnableHint: '使用 fanart.tv 的图片数据',
|
||||
githubProxy: 'Github加速代理',
|
||||
@@ -1058,13 +1083,27 @@ export default {
|
||||
bing: 'Bing每日壁纸',
|
||||
mediaserver: '媒体服务器',
|
||||
none: '无壁纸',
|
||||
customize: '自定义',
|
||||
},
|
||||
mb: 'MB',
|
||||
hour: '小时',
|
||||
customizeWallpaperApi: '自定义壁纸API地址',
|
||||
customizeWallpaperApiHint: '会获取API返回内容中所有允许的安全域名地址的图片,需要同步设置安全域名地址',
|
||||
customizeWallpaperApiRequired: '必填项;请输入自定义壁纸API',
|
||||
securityImageDomains: '安全图片域名',
|
||||
securityImageDomainsHint: '允许缓存的图片域名白名单,用于控制可信任的图片来源',
|
||||
noSecurityImageDomains: '暂无安全域名',
|
||||
securityImageDomainAdd: '添加域名,如:image.tmdb.org',
|
||||
proxyHost: '代理服务器',
|
||||
proxyHostHint: '设置代理服务器地址,支持:http(s)、socks5、socks5h 等协议',
|
||||
moviePilotAutoUpdate: '自动更新MoviePilot',
|
||||
moviePilotAutoUpdateHint: '重启时自动更新MoviePilot到最新发行版本',
|
||||
autoUpdateResource: '自动更新站点资源',
|
||||
autoUpdateResourceHint: '重启时自动检测和更新站点资源包',
|
||||
},
|
||||
site: {
|
||||
siteSync: '站点同步',
|
||||
siteSyncDesc: '从CookieCloud快速同步站点数据。',
|
||||
siteSyncDesc: '从CookieCloud快速同步站点数据',
|
||||
enableLocalCookieCloud: '启用本地CookieCloud服务器',
|
||||
enableLocalCookieCloudHint: '使用内建CookieCloud服务同步站点数据,服务地址为:http://localhost:3000/cookiecloud',
|
||||
serviceAddress: '服务地址',
|
||||
@@ -1107,7 +1146,7 @@ export default {
|
||||
},
|
||||
notification: {
|
||||
channels: '通知渠道',
|
||||
channelsDesc: '设置消息发送渠道参数。',
|
||||
channelsDesc: '设置消息发送渠道参数',
|
||||
organizeSuccess: '资源入库',
|
||||
downloadAdded: '资源下载',
|
||||
subscribeAdded: '添加订阅',
|
||||
@@ -1150,10 +1189,11 @@ export default {
|
||||
synologyChat: 'SynologyChat',
|
||||
voceChat: 'VoceChat',
|
||||
webPush: 'WebPush',
|
||||
custom: '自定义通知',
|
||||
},
|
||||
words: {
|
||||
customIdentifiers: '自定义识别词',
|
||||
identifiersDesc: '添加规则对种子名或者文件名进行预处理以校正识别。',
|
||||
identifiersDesc: '添加规则对种子名或者文件名进行预处理以校正识别',
|
||||
identifiersPlaceholder: '支持正则表达式,特殊字符需要\\转义,一行为一组',
|
||||
identifiersHint: '支持正则表达式,特殊字符需要\\转义,一行为一组',
|
||||
formatTitle: '支持的配置格式(注意空格):',
|
||||
@@ -1162,7 +1202,7 @@ export default {
|
||||
'被替换词 => 替换词\n' +
|
||||
'前定位词 <> 后定位词 >> 集偏移量(EP)\n' +
|
||||
'被替换词 => 替换词 && 前定位词 <> 后定位词 >> 集偏移量(EP)\n' +
|
||||
'其中替换词支持格式:{tmdbid/doubanid=xxx;type=movie/tv;s=xxx;e=xxx} 直接指定TMDBID/豆瓣ID识别,其中s、e为季数和集数(可选)',
|
||||
'其中替换词支持格式:{[tmdbid/doubanid=xxx;type=movie/tv;s=xxx;e=xxx]} 直接指定TMDBID/豆瓣ID识别,其中s、e为季数和集数(可选)',
|
||||
identifierSaveSuccess: '自定义识别词保存成功',
|
||||
identifierSaveFailed: '自定义识别词保存失败!',
|
||||
|
||||
@@ -1189,7 +1229,7 @@ export default {
|
||||
},
|
||||
search: {
|
||||
basicSettings: '基础设置',
|
||||
basicSettingsDesc: '设定数据源、规则组等基础信息。',
|
||||
basicSettingsDesc: '设定数据源、规则组等基础信息',
|
||||
recognizeSource: '识别数据源',
|
||||
recognizeSourceDesc: '默认使用TMDB。豆瓣识别中文作品通常更友好,但有些国外作品信息不完整。',
|
||||
themoviedb: 'TheMovieDb',
|
||||
@@ -1225,7 +1265,7 @@ export default {
|
||||
},
|
||||
directory: {
|
||||
storage: '存储',
|
||||
storageDesc: '设置本地或网盘存储。',
|
||||
storageDesc: '设置本地或网盘存储',
|
||||
directory: '目录',
|
||||
mediaType: '媒体类型',
|
||||
directoryDesc: '设置媒体文件整理目录结构,按先后顺序依次匹配。',
|
||||
@@ -1287,7 +1327,7 @@ export default {
|
||||
},
|
||||
scheduler: {
|
||||
title: '定时作业',
|
||||
subtitle: '包含系统内置服务以及插件提供的服务,手动执行不会影响作业正常的时间表。',
|
||||
subtitle: '包含系统内置服务以及插件提供的服务',
|
||||
provider: '提供者',
|
||||
taskName: '任务名称',
|
||||
taskStatus: '任务状态',
|
||||
@@ -1334,6 +1374,55 @@ export default {
|
||||
settingsSaveSuccess: '订阅基础设置保存成功',
|
||||
settingsSaveFailed: '订阅基础设置保存失败!',
|
||||
},
|
||||
cache: {
|
||||
title: '缓存管理',
|
||||
subtitle: '管理缓存的站点资源',
|
||||
filterByTitle: '按标题筛选',
|
||||
filterBySite: '按站点筛选',
|
||||
selectSite: '选择站点',
|
||||
refresh: '刷新缓存',
|
||||
deleteSelected: '删除选中',
|
||||
clearAll: '清空缓存',
|
||||
refreshSuccess: '缓存刷新完成',
|
||||
refreshFailed: '刷新缓存失败',
|
||||
clearSuccess: '缓存清理完成',
|
||||
clearFailed: '清理缓存失败',
|
||||
deleteSuccess: '缓存项删除成功',
|
||||
deleteFailed: '删除缓存项失败',
|
||||
deleteSelectedSuccess: '成功删除 {count} 个缓存项',
|
||||
deleteSelectedFailed: '删除缓存项失败',
|
||||
loadFailed: '加载缓存数据失败',
|
||||
selectDeleteWarning: '请选择要删除的缓存项',
|
||||
reidentify: '重新识别',
|
||||
reidentifySuccess: '重新识别完成',
|
||||
reidentifyFailed: '重新识别失败',
|
||||
poster: '海报',
|
||||
torrentTitle: '标题',
|
||||
site: '站点',
|
||||
size: '大小',
|
||||
publishTime: '发布时间',
|
||||
recognitionResult: '识别结果',
|
||||
actions: '操作',
|
||||
unrecognized: '未识别',
|
||||
noData: '暂无缓存数据',
|
||||
noDataHint: '点击"刷新缓存"按钮获取最新的种子缓存',
|
||||
reidentifyDialog: {
|
||||
title: '重新识别',
|
||||
torrentInfo: '种子信息',
|
||||
tmdbId: 'TMDB ID',
|
||||
tmdbIdHint: '可选,手动指定TMDB ID进行识别',
|
||||
doubanId: '豆瓣 ID',
|
||||
doubanIdHint: '可选,手动指定豆瓣ID进行识别',
|
||||
autoHint: '如果不指定ID,将自动重新识别该种子',
|
||||
cancel: '取消',
|
||||
confirm: '重新识别',
|
||||
},
|
||||
mediaType: {
|
||||
movie: '电影',
|
||||
tv: '电视剧',
|
||||
},
|
||||
clearConfirm: '确认清空所有缓存吗?',
|
||||
},
|
||||
},
|
||||
dialog: {
|
||||
progress: {
|
||||
@@ -1491,7 +1580,7 @@ export default {
|
||||
loginTypeOptions: {
|
||||
guest: '访客',
|
||||
username: '用户名密码',
|
||||
token: 'Token',
|
||||
token: '令牌',
|
||||
},
|
||||
complete: '完成',
|
||||
reset: '重置',
|
||||
@@ -1589,7 +1678,7 @@ export default {
|
||||
title: '插件市场设置',
|
||||
repoUrl: '插件仓库地址',
|
||||
repoPlaceholder: '格式:https://github.com/jxxghp/MoviePilot-Plugins/,https://github.com/xxxx/xxxxxx/',
|
||||
repoHint: '多个地址使用逗号分隔,仅支持Github仓库',
|
||||
repoHint: '多个地址使用换行分隔,仅支持Github仓库',
|
||||
close: '关闭',
|
||||
save: '保存',
|
||||
saveSuccess: '插件仓库保存成功',
|
||||
@@ -1655,8 +1744,8 @@ export default {
|
||||
previous: '上一步',
|
||||
confirm: '确认',
|
||||
manualTitle: '手动整理',
|
||||
multipleItemsTitle: '整理 - 共 {count} 项',
|
||||
singleItemTitle: '整理 - {path}',
|
||||
multipleItemsTitle: '共 {count} 项',
|
||||
singleItemTitle: '{path}',
|
||||
targetStorage: '目的存储',
|
||||
targetStorageHint: '整理目的存储',
|
||||
transferType: '整理方式',
|
||||
@@ -1705,7 +1794,7 @@ export default {
|
||||
},
|
||||
subscribeEdit: {
|
||||
titleDefault: '默认订阅规则',
|
||||
titleEditFormat: '编辑订阅 - {name} {season}',
|
||||
titleEdit: '编辑订阅',
|
||||
seasonFormat: '第 {number} 季',
|
||||
tabs: {
|
||||
basic: '基础',
|
||||
@@ -1818,6 +1907,7 @@ export default {
|
||||
peersColumn: '下载',
|
||||
viewDetails: '查看详情',
|
||||
downloadTorrent: '下载种子文件',
|
||||
pageText: '{0}-{1} 共 {2} 条',
|
||||
},
|
||||
forkSubscribe: {
|
||||
title: '复制订阅',
|
||||
@@ -1949,6 +2039,52 @@ export default {
|
||||
updateHistoryTitle: '{name} 更新说明',
|
||||
updateToLatest: '更新到最新版本',
|
||||
updatingTo: '更新 {name} 到 {version} 版本...',
|
||||
folderNameEmpty: '文件夹名称不能为空',
|
||||
folderExists: '文件夹已存在',
|
||||
folderCreateSuccess: '文件夹创建成功',
|
||||
folderRenameSuccess: '文件夹重命名成功',
|
||||
folderRenameFailed: '重命名文件夹失败',
|
||||
folderDeleteSuccess: '文件夹删除成功',
|
||||
folderDeleteFailed: '删除文件夹失败',
|
||||
removeFromFolderSuccess: '插件已移出文件夹',
|
||||
operationFailed: '操作失败',
|
||||
saveFolderConfigFailed: '保存文件夹配置失败',
|
||||
newFolder: '新建文件夹',
|
||||
folderName: '文件夹名称',
|
||||
cancel: '取消',
|
||||
create: '创建',
|
||||
clone: '分身',
|
||||
cloneTitle: '创建插件分身',
|
||||
cloneSubtitle: '为 {name} 创建独立的分身实例',
|
||||
cloneFeature: '插件分身功能',
|
||||
cloneDescription: '创建插件的独立副本,拥有独立的配置和数据,适用于多账号、测试环境等场景',
|
||||
suffix: '分身后缀',
|
||||
suffixPlaceholder: '例如:Test、Backup、Site1',
|
||||
suffixHint: '用于区分分身的唯一标识,只能包含英文字母和数字',
|
||||
suffixRequired: '分身后缀不能为空',
|
||||
suffixFormatError: '只能包含英文字母和数字',
|
||||
suffixLengthError: '长度不能超过20个字符',
|
||||
cloneName: '分身名称',
|
||||
cloneNamePlaceholder: '例如:自动备份 测试版',
|
||||
cloneNameHint: '分身插件的显示名称(可选)',
|
||||
cloneDefaultName: '{name} 分身',
|
||||
cloneDescriptionLabel: '分身描述',
|
||||
cloneDescriptionPlaceholder: '描述这个分身的用途和特点...',
|
||||
cloneDescriptionHint: '详细描述分身插件的用途(可选)',
|
||||
cloneDefaultDescription: '{description} (分身版本)',
|
||||
cloneVersion: '版本号',
|
||||
cloneVersionPlaceholder: '例如:1.0、2.1.0',
|
||||
cloneVersionHint: '自定义分身插件的版本号(可选)',
|
||||
cloneIcon: '图标URL',
|
||||
cloneIconPlaceholder: 'https://example.com/icon.png',
|
||||
cloneIconHint: '自定义分身插件的图标(可选)',
|
||||
cloneNotice: '分身插件创建后默认为禁用状态,需要手动配置启用。分身后缀一旦确定无法修改。',
|
||||
createClone: '创建分身',
|
||||
cloning: '正在创建 {name} 的分身...',
|
||||
cloneSuccess: '插件分身 {name} 创建成功!',
|
||||
cloneFailed: '插件分身创建失败:{message}',
|
||||
cloneFailedGeneral: '插件分身创建失败',
|
||||
logTitle: '插件日志',
|
||||
},
|
||||
profile: {
|
||||
personalInfo: '个人信息',
|
||||
@@ -2330,4 +2466,22 @@ export default {
|
||||
required: '此项为必填项',
|
||||
number: '请输入数字',
|
||||
},
|
||||
folder: {
|
||||
settingAppearance: '设置外观',
|
||||
rename: '重命名',
|
||||
deleteFolder: '删除文件夹',
|
||||
folderNameCannotBeEmpty: '文件夹名称不能为空',
|
||||
confirmDeleteFolder: '确定要删除文件夹 "{folderName}" 吗?文件夹中的插件将移回主列表。',
|
||||
folderSettingsSaved: '文件夹设置已保存',
|
||||
renameFolder: '重命名文件夹',
|
||||
folderName: '文件夹名称',
|
||||
folderAppearanceSettings: '文件夹外观设置',
|
||||
showFolderIcon: '显示文件夹图标',
|
||||
icon: '图标',
|
||||
iconColor: '图标颜色',
|
||||
backgroundGradient: '背景渐变',
|
||||
customBackgroundImageURL: '自定义背景图片URL(可选)',
|
||||
customBackgroundImageHint: '支持网络图片URL,留空则使用渐变背景',
|
||||
pluginCount: '{count} 个插件',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -39,6 +39,11 @@ export default {
|
||||
unsubscribe: '取消訂閱',
|
||||
media: '媒體',
|
||||
unknown: '未知',
|
||||
notice: '注意',
|
||||
itemsPerPage: '每頁條數',
|
||||
pageText: '{0}-{1} 共 {2} 條',
|
||||
noDataText: '沒有數據',
|
||||
loadingText: '加載中...',
|
||||
},
|
||||
mediaType: {
|
||||
movie: '電影',
|
||||
@@ -71,6 +76,7 @@ export default {
|
||||
sendEvent: '發送事件',
|
||||
sendMessage: '發送消息',
|
||||
transferFile: '整理文件',
|
||||
invokePlugin: '調用插件',
|
||||
},
|
||||
qualityOptions: {
|
||||
all: '全部',
|
||||
@@ -201,6 +207,10 @@ export default {
|
||||
title: '服務',
|
||||
description: '定時作業',
|
||||
},
|
||||
cache: {
|
||||
title: '緩存',
|
||||
description: '種子緩存、識別媒體數據緩存、圖片文件緩存管理',
|
||||
},
|
||||
notification: {
|
||||
title: '通知',
|
||||
description: '通知渠道(微信、Telegram、Slack、SynologyChat、VoceChat、WebPush)、消息發送範圍',
|
||||
@@ -278,6 +288,9 @@ export default {
|
||||
nameHint: '通知渠道名稱',
|
||||
type: '類型',
|
||||
typeHint: '通知渠道類型',
|
||||
customTypeHint: '自定義通知類型,用於插件實現場景',
|
||||
customTypePlaceholder: 'custom',
|
||||
nameRequired: '請輸入名稱',
|
||||
enabled: '啟用',
|
||||
config: '配置',
|
||||
wechat: {
|
||||
@@ -426,6 +439,8 @@ export default {
|
||||
forceResume: '强制继续',
|
||||
firstLastPiece: '优先首尾文件',
|
||||
onlyLack: '仅下载缺失资源',
|
||||
categoryPlaceholder: '多個使用,分隔',
|
||||
savePathPlaceholder: '留空自動',
|
||||
},
|
||||
addSubscribe: {
|
||||
title: '添加订阅',
|
||||
@@ -533,6 +548,14 @@ export default {
|
||||
exclude: '排除(關鍵字、正則式)',
|
||||
ruleGroups: '過濾規則組',
|
||||
},
|
||||
invokePlugin: {
|
||||
title: '調用插件',
|
||||
subtitle: '調用插件執行特定操作',
|
||||
plugin: '插件',
|
||||
actionid: '動作ID',
|
||||
actionParams: '動作參數',
|
||||
loadPluginSettingFailed: '加載插件設置失敗',
|
||||
},
|
||||
title: '工作流',
|
||||
noWorkflow: '沒有工作流',
|
||||
noWorkflowDescription: '點擊添加按鈕創建工作流任務。',
|
||||
@@ -837,8 +860,8 @@ export default {
|
||||
browserSimulation: '使用瀏覽器模擬真實訪問該站點',
|
||||
},
|
||||
actions: {
|
||||
add: '新增',
|
||||
edit: '編輯',
|
||||
add: '新增站點',
|
||||
edit: '編輯站點',
|
||||
},
|
||||
messages: {
|
||||
addSuccess: '新增站點成功',
|
||||
@@ -924,7 +947,7 @@ export default {
|
||||
system: {
|
||||
custom: '自定義',
|
||||
basicSettings: '基礎設置',
|
||||
basicSettingsDesc: '設置服務器的全局功能。',
|
||||
basicSettingsDesc: '設置服務器的全局功能',
|
||||
appDomain: '訪問域名',
|
||||
appDomainHint: '用於發送通知時,添加快捷跳轉地址',
|
||||
wallpaper: '背景壁紙',
|
||||
@@ -1007,6 +1030,8 @@ export default {
|
||||
metaCacheExpireMin: '元數據緩存時間必須大於等於0',
|
||||
scrapFollowTmdb: '跟隨TMDB識別整理',
|
||||
scrapFollowTmdbHint: '關閉時以整理歷史記錄為準(如有),避免TMDB數據在訂閱中途修改',
|
||||
scrapOriginalImage: 'TMDB 刮削原語种圖片',
|
||||
scrapOriginalImageHint: '刮削原語种圖片,否则數據元数据語种圖片',
|
||||
fanartEnable: 'Fanart圖片數據源',
|
||||
fanartEnableHint: '使用 fanart.tv 的圖片數據',
|
||||
githubProxy: 'Github加速代理',
|
||||
@@ -1060,13 +1085,27 @@ export default {
|
||||
bing: 'Bing每日壁紙',
|
||||
mediaserver: '媒體服務器',
|
||||
none: '無壁紙',
|
||||
customize: '自定義',
|
||||
},
|
||||
mb: 'MB',
|
||||
hour: '小時',
|
||||
customizeWallpaperApi: '自定義壁紙API',
|
||||
customizeWallpaperApiHint: '會獲取 API 返回內容中所有安全設置中允許的圖片地址,需要設置安全域名白名單',
|
||||
customizeWallpaperApiRequired: '必填項;請輸出自定義壁紙API',
|
||||
securityImageDomains: '安全圖片域名',
|
||||
securityImageDomainsHint: '允許緩存的圖片域名白名單,用於控制可信任的圖片來源',
|
||||
noSecurityImageDomains: '暫無安全域名',
|
||||
securityImageDomainAdd: '添加域名,如:image.tmdb.org',
|
||||
proxyHost: '代理服務器',
|
||||
proxyHostHint: '設置代理服務器地址,支持:http(s)、socks5、socks5h 等協議',
|
||||
moviePilotAutoUpdate: '自動更新MoviePilot',
|
||||
moviePilotAutoUpdateHint: '重啟時自動更新MoviePilot到最新發行版本',
|
||||
autoUpdateResource: '自動更新站點資源',
|
||||
autoUpdateResourceHint: '重啟時自動檢測和更新站點資源包',
|
||||
},
|
||||
site: {
|
||||
siteSync: '站點同步',
|
||||
siteSyncDesc: '從CookieCloud快速同步站點數據。',
|
||||
siteSyncDesc: '從CookieCloud快速同步站點數據',
|
||||
enableLocalCookieCloud: '啟用本地CookieCloud服務器',
|
||||
enableLocalCookieCloudHint: '使用內建CookieCloud服務同步站點數據,服務地址為:http://localhost:3000/cookiecloud',
|
||||
serviceAddress: '服務地址',
|
||||
@@ -1109,7 +1148,7 @@ export default {
|
||||
},
|
||||
notification: {
|
||||
channels: '通知渠道',
|
||||
channelsDesc: '設置消息發送渠道參數。',
|
||||
channelsDesc: '設置消息發送渠道參數',
|
||||
organizeSuccess: '資源入庫',
|
||||
downloadAdded: '資源下載',
|
||||
subscribeAdded: '添加訂閱',
|
||||
@@ -1152,10 +1191,11 @@ export default {
|
||||
synologyChat: 'SynologyChat',
|
||||
voceChat: 'VoceChat',
|
||||
webPush: 'WebPush',
|
||||
custom: '自定義通知',
|
||||
},
|
||||
words: {
|
||||
customIdentifiers: '自定義識別詞',
|
||||
identifiersDesc: '添加規則對種子名或者文件名進行預處理以校正識別。',
|
||||
identifiersDesc: '添加規則對種子名或者文件名進行預處理以校正識別',
|
||||
identifiersPlaceholder: '支持正則表達式,特殊字符需要\\轉義,一行為一組',
|
||||
identifiersHint: '支持正則表達式,特殊字符需要\\轉義,一行為一組',
|
||||
formatTitle: '支持的配置格式(注意空格):',
|
||||
@@ -1164,7 +1204,7 @@ export default {
|
||||
'被替換詞 => 替換詞\n' +
|
||||
'前定位詞 <> 後定位詞 >> 集偏移量(EP)\n' +
|
||||
'被替換詞 => 替換詞 && 前定位詞 <> 後定位詞 >> 集偏移量(EP)\n' +
|
||||
'其中替換詞支持格式:{tmdbid/doubanid=xxx;type=movie/tv;s=xxx;e=xxx} 直接指定TMDBID/豆瓣ID識別,其中s、e為季數和集數(可選)',
|
||||
'其中替換詞支持格式:{[tmdbid/doubanid=xxx;type=movie/tv;s=xxx;e=xxx]} 直接指定TMDBID/豆瓣ID識別,其中s、e為季數和集數(可選)',
|
||||
identifierSaveSuccess: '自定義識別詞保存成功',
|
||||
identifierSaveFailed: '自定義識別詞保存失敗!',
|
||||
|
||||
@@ -1191,7 +1231,7 @@ export default {
|
||||
},
|
||||
search: {
|
||||
basicSettings: '基礎設置',
|
||||
basicSettingsDesc: '設定數據源、規則組等基礎信息。',
|
||||
basicSettingsDesc: '設定數據源、規則組等基礎信息',
|
||||
recognizeSource: '識別數據源',
|
||||
recognizeSourceDesc: '默認使用TMDB。豆瓣識別中文作品通常更友好,但有些國外作品信息不完整。',
|
||||
themoviedb: 'TheMovieDb',
|
||||
@@ -1227,7 +1267,7 @@ export default {
|
||||
},
|
||||
directory: {
|
||||
storage: '存儲',
|
||||
storageDesc: '設置本地或網盤存儲。',
|
||||
storageDesc: '設置本地或網盤存儲',
|
||||
directory: '目錄',
|
||||
directoryDesc: '設置媒體文件整理目錄結構,按先後順序依次匹配。',
|
||||
organizeAndScrap: '整理 & 刮削',
|
||||
@@ -1288,7 +1328,7 @@ export default {
|
||||
},
|
||||
scheduler: {
|
||||
scheduledTasks: '定時作業',
|
||||
scheduledTasksDesc: '包含系統內置服務以及插件提供的服務,手動執行不會影響作業正常的時間表。',
|
||||
scheduledTasksDesc: '包含系統內置服務以及插件提供的服務',
|
||||
provider: '提供者',
|
||||
taskName: '任務名稱',
|
||||
taskStatus: '任務狀態',
|
||||
@@ -1335,6 +1375,56 @@ export default {
|
||||
settingsSaveSuccess: '訂閱基礎設置保存成功',
|
||||
settingsSaveFailed: '訂閱基礎設置保存失敗!',
|
||||
},
|
||||
cache: {
|
||||
title: '緩存',
|
||||
description: '種子緩存、圖片文件緩存管理',
|
||||
subtitle: '管理緩存的站點資源',
|
||||
filterByTitle: '按標題篩選',
|
||||
filterBySite: '按站點篩選',
|
||||
selectSite: '選擇站點',
|
||||
refresh: '刷新緩存',
|
||||
deleteSelected: '刪除選中',
|
||||
clearAll: '清空緩存',
|
||||
refreshSuccess: '緩存刷新完成',
|
||||
refreshFailed: '刷新緩存失敗',
|
||||
clearSuccess: '緩存清理完成',
|
||||
clearFailed: '清理緩存失敗',
|
||||
deleteSuccess: '緩存項刪除成功',
|
||||
deleteFailed: '刪除緩存項失敗',
|
||||
deleteSelectedSuccess: '成功刪除 {count} 個緩存項',
|
||||
deleteSelectedFailed: '刪除緩存項失敗',
|
||||
loadFailed: '加載緩存數據失敗',
|
||||
selectDeleteWarning: '請選擇要刪除的緩存項',
|
||||
reidentify: '重新識別',
|
||||
reidentifySuccess: '重新識別完成',
|
||||
reidentifyFailed: '重新識別失敗',
|
||||
poster: '海報',
|
||||
torrentTitle: '標題',
|
||||
site: '站點',
|
||||
size: '大小',
|
||||
publishTime: '發布時間',
|
||||
recognitionResult: '識別結果',
|
||||
actions: '操作',
|
||||
unrecognized: '未識別',
|
||||
noData: '暫無緩存數據',
|
||||
noDataHint: '點擊"刷新緩存"按鈕獲取最新的種子緩存',
|
||||
reidentifyDialog: {
|
||||
title: '重新識別',
|
||||
torrentInfo: '種子信息',
|
||||
tmdbId: 'TMDB ID',
|
||||
tmdbIdHint: '可選,手動指定TMDB ID進行識別',
|
||||
doubanId: '豆瓣 ID',
|
||||
doubanIdHint: '可選,手動指定豆瓣ID進行識別',
|
||||
autoHint: '如果不指定ID,將自動重新識別該種子',
|
||||
cancel: '取消',
|
||||
confirm: '重新識別',
|
||||
},
|
||||
mediaType: {
|
||||
movie: '電影',
|
||||
tv: '電視劇',
|
||||
},
|
||||
clearConfirm: '確認清空所有緩存嗎?',
|
||||
},
|
||||
},
|
||||
dialog: {
|
||||
progress: {
|
||||
@@ -1590,7 +1680,7 @@ export default {
|
||||
title: '插件市場設置',
|
||||
repoUrl: '插件倉庫地址',
|
||||
repoPlaceholder: '格式:https://github.com/jxxghp/MoviePilot-Plugins/,https://github.com/xxxx/xxxxxx/',
|
||||
repoHint: '多個地址使用逗號分隔,僅支援Github倉庫',
|
||||
repoHint: '多個地址使用换行分隔,僅支援Github倉庫',
|
||||
close: '關閉',
|
||||
save: '儲存',
|
||||
saveSuccess: '插件倉庫儲存成功',
|
||||
@@ -1656,8 +1746,8 @@ export default {
|
||||
previous: '上一步',
|
||||
confirm: '確認',
|
||||
manualTitle: '手動整理',
|
||||
multipleItemsTitle: '整理 - 共 {count} 項',
|
||||
singleItemTitle: '整理 - {path}',
|
||||
multipleItemsTitle: '共 {count} 項',
|
||||
singleItemTitle: '{path}',
|
||||
targetStorage: '目的存儲',
|
||||
targetStorageHint: '整理目的存儲',
|
||||
transferType: '整理方式',
|
||||
@@ -1706,7 +1796,7 @@ export default {
|
||||
},
|
||||
subscribeEdit: {
|
||||
titleDefault: '默認訂閱規則',
|
||||
titleEditFormat: '編輯訂閱 - {name} {season}',
|
||||
titleEdit: '編輯訂閱',
|
||||
seasonFormat: '第 {number} 季',
|
||||
tabs: {
|
||||
basic: '基礎',
|
||||
@@ -1951,6 +2041,52 @@ export default {
|
||||
updateHistoryTitle: '{name} 更新說明',
|
||||
updateToLatest: '更新到最新版本',
|
||||
updatingTo: '正在更新 {name} 至 v{version} ...',
|
||||
folderNameEmpty: '文件夾名稱不能為空',
|
||||
folderExists: '文件夾已存在',
|
||||
folderCreateSuccess: '文件夾創建成功',
|
||||
folderRenameSuccess: '文件夾重命名成功',
|
||||
folderRenameFailed: '重命名文件夾失敗',
|
||||
folderDeleteSuccess: '文件夾刪除成功',
|
||||
folderDeleteFailed: '刪除文件夾失敗',
|
||||
removeFromFolderSuccess: '插件已移出文件夾',
|
||||
operationFailed: '操作失敗',
|
||||
saveFolderConfigFailed: '保存文件夾配置失敗',
|
||||
newFolder: '新建文件夾',
|
||||
folderName: '文件夾名稱',
|
||||
cancel: '取消',
|
||||
create: '創建',
|
||||
clone: '分身',
|
||||
cloneTitle: '創建插件分身',
|
||||
cloneSubtitle: '為 {name} 創建獨立的分身實例',
|
||||
cloneFeature: '插件分身功能',
|
||||
cloneDescription: '創建插件的獨立副本,擁有獨立的配置和數據,適用於多賬號、測試環境等場景',
|
||||
suffix: '分身後綴',
|
||||
suffixPlaceholder: '例如:Test、Backup、Site1',
|
||||
suffixHint: '用於區分分身的唯一標識,只能包含英文字母和數字',
|
||||
suffixRequired: '分身後綴不能為空',
|
||||
suffixFormatError: '只能包含英文字母和數字',
|
||||
suffixLengthError: '長度不能超過20個字符',
|
||||
cloneName: '分身名稱',
|
||||
cloneNamePlaceholder: '例如:自動備份 測試版',
|
||||
cloneNameHint: '分身插件的顯示名稱(可選)',
|
||||
cloneDefaultName: '{name} 分身',
|
||||
cloneDescriptionLabel: '分身描述',
|
||||
cloneDescriptionPlaceholder: '描述這個分身的用途和特點...',
|
||||
cloneDescriptionHint: '詳細描述分身插件的用途(可選)',
|
||||
cloneDefaultDescription: '{description} (分身版本)',
|
||||
cloneVersion: '版本號',
|
||||
cloneVersionPlaceholder: '例如:1.0、2.1.0',
|
||||
cloneVersionHint: '自定義分身插件的版本號(可選)',
|
||||
cloneIcon: '圖標URL',
|
||||
cloneIconPlaceholder: 'https://example.com/icon.png',
|
||||
cloneIconHint: '自定義分身插件的圖標(可選)',
|
||||
cloneNotice: '分身插件創建後默認為禁用狀態,需要手動配置啟用。分身後綴一旦確定無法修改。',
|
||||
createClone: '創建分身',
|
||||
cloning: '正在創建 {name} 的分身...',
|
||||
cloneSuccess: '插件分身 {name} 創建成功!',
|
||||
cloneFailed: '插件分身創建失敗:{message}',
|
||||
cloneFailedGeneral: '插件分身創建失敗',
|
||||
logTitle: '插件日誌',
|
||||
},
|
||||
profile: {
|
||||
personalInfo: '個人信息',
|
||||
@@ -2332,4 +2468,22 @@ export default {
|
||||
required: '此項為必填項',
|
||||
number: '請輸入數字',
|
||||
},
|
||||
folder: {
|
||||
settingAppearance: '設定外觀',
|
||||
rename: '重新命名',
|
||||
deleteFolder: '刪除資料夾',
|
||||
folderNameCannotBeEmpty: '資料夾名稱不能為空',
|
||||
confirmDeleteFolder: '確定要刪除資料夾 "{folderName}" 嗎?資料夾中的插件將移回主列表。',
|
||||
folderSettingsSaved: '資料夾設定已儲存',
|
||||
renameFolder: '重新命名資料夾',
|
||||
folderName: '資料夾名稱',
|
||||
folderAppearanceSettings: '資料夾外觀設定',
|
||||
showFolderIcon: '顯示資料夾圖示',
|
||||
icon: '圖示',
|
||||
iconColor: '圖示顏色',
|
||||
backgroundGradient: '背景漸變',
|
||||
customBackgroundImageURL: '自定義背景圖片URL(可選)',
|
||||
customBackgroundImageHint: '支援網路圖片URL,留空則使用漸變背景',
|
||||
pluginCount: '{count} 個插件',
|
||||
},
|
||||
}
|
||||
|
||||
23
src/main.ts
23
src/main.ts
@@ -24,7 +24,7 @@ import { fetchGlobalSettings } from './utils/globalSetting'
|
||||
|
||||
// 5. 其他插件和功能模块
|
||||
import ToastPlugin from 'vue-toast-notification'
|
||||
import VuetifyUseDialog from 'vuetify-use-dialog'
|
||||
import ConfirmDialog from '@/composables/useConfirm'
|
||||
import VueApexCharts from 'vue3-apexcharts'
|
||||
|
||||
// 6. 注册自定义组件
|
||||
@@ -102,26 +102,7 @@ initializeApp().then(() => {
|
||||
.use(ToastPlugin, {
|
||||
position: 'bottom-right',
|
||||
})
|
||||
.use(VuetifyUseDialog, {
|
||||
confirmDialog: {
|
||||
dialogProps: {
|
||||
maxWidth: '30rem',
|
||||
},
|
||||
confirmationButtonProps: {
|
||||
variant: 'elevated',
|
||||
color: 'primary',
|
||||
class: 'me-3 px-5',
|
||||
'prepend-icon': 'mdi-check',
|
||||
},
|
||||
cancellationButtonProps: {
|
||||
variant: 'outlined',
|
||||
color: 'secondary',
|
||||
class: 'me-3',
|
||||
},
|
||||
confirmationText: i18n.global.t('common.confirm'),
|
||||
cancellationText: i18n.global.t('common.cancel'),
|
||||
},
|
||||
})
|
||||
.use(ConfirmDialog)
|
||||
.use(i18n)
|
||||
.mount('#app')
|
||||
})
|
||||
|
||||
@@ -1,24 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import PersonCardListView from '@/views/discover/PersonCardListView.vue'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
// API路径
|
||||
paths: Array as PropType<string[]> | PropType<string>,
|
||||
})
|
||||
|
||||
// 路由参数
|
||||
const route = useRoute()
|
||||
const id = route.query?.id?.toString()
|
||||
const title = route.query?.title?.toString()
|
||||
const source = route.query?.source?.toString()
|
||||
const type = route.query?.type?.toString()
|
||||
const apipath = route.query?.apipath?.toString()
|
||||
|
||||
// 标题
|
||||
let title = route.query?.title?.toString()
|
||||
|
||||
// 计算API路径
|
||||
function getApiPath(paths: string[] | string) {
|
||||
if (Array.isArray(paths)) return paths.join('/')
|
||||
else return paths
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<VPageContentTitle :title="title" />
|
||||
<PersonCardListView
|
||||
:credits-id="id"
|
||||
:credits-name="title"
|
||||
:credits-source="source"
|
||||
:credits-type="type"
|
||||
:credits-apipath="apipath"
|
||||
/>
|
||||
<PersonCardListView :apipath="getApiPath(props.paths || '')" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -8,6 +8,7 @@ import DashboardElement from '@/components/misc/DashboardElement.vue'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import { useDynamicButton } from '@/composables/useDynamicButton'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { VCardActions } from 'vuetify/components'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
@@ -353,7 +354,7 @@ onDeactivated(() => {
|
||||
/>
|
||||
|
||||
<!-- 弹窗,根据配置生成选项 -->
|
||||
<VDialog v-if="dialog" v-model="dialog" max-width="35rem" scrollable>
|
||||
<VDialog v-if="dialog" v-model="dialog" max-width="35rem" :fullscreen="!display.mdAndUp.value" scrollable>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>
|
||||
@@ -396,8 +397,7 @@ onDeactivated(() => {
|
||||
<VSwitch v-model="isElevated" :label="t('dashboard.adaptiveHeight')" />
|
||||
</p>
|
||||
</VCardText>
|
||||
<VDivider />
|
||||
<VCardText class="pt-5 text-end">
|
||||
<VCardActions class="pt-3">
|
||||
<VSpacer />
|
||||
<VBtn @click="saveDashboardConfig">
|
||||
<template #prepend>
|
||||
@@ -405,7 +405,7 @@ onDeactivated(() => {
|
||||
</template>
|
||||
{{ t('common.save') }}
|
||||
</VBtn>
|
||||
</VCardText>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
@@ -8,6 +8,9 @@ import ExtraSourceView from '@/views/discover/ExtraSourceView.vue'
|
||||
import { DiscoverSource } from '@/api/types'
|
||||
import api from '@/api'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useDisplay } from 'vuetify'
|
||||
|
||||
const display = useDisplay()
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
@@ -179,7 +182,13 @@ onActivated(async () => {
|
||||
</VWindowItem>
|
||||
</VWindow>
|
||||
<!-- 弹窗,根据配置生成选项 -->
|
||||
<VDialog v-if="orderConfigDialog" v-model="orderConfigDialog" max-width="35rem" scrollable>
|
||||
<VDialog
|
||||
v-if="orderConfigDialog"
|
||||
v-model="orderConfigDialog"
|
||||
max-width="35rem"
|
||||
scrollable
|
||||
:fullscreen="!display.mdAndUp.value"
|
||||
>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>
|
||||
@@ -199,16 +208,15 @@ onActivated(async () => {
|
||||
:component-data="{ 'class': 'settings-grid' }"
|
||||
>
|
||||
<template #item="{ element }">
|
||||
<div class="setting-item enabled">
|
||||
<VCard variant="text" class="setting-item enabled">
|
||||
<div class="setting-item-inner cursor-move text-center">
|
||||
<span class="setting-label">{{ element.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</VCard>
|
||||
</template>
|
||||
</draggable>
|
||||
</VCardText>
|
||||
<VDivider />
|
||||
<VCardText class="pt-5 text-end">
|
||||
<VCardActions class="pt-3">
|
||||
<VSpacer />
|
||||
<VBtn @click="saveTabOrder">
|
||||
<template #prepend>
|
||||
@@ -216,7 +224,7 @@ onActivated(async () => {
|
||||
</template>
|
||||
{{ t('common.save') }}
|
||||
</VBtn>
|
||||
</VCardText>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
<!-- 快速滚动到顶部按钮 -->
|
||||
@@ -261,6 +269,7 @@ onActivated(async () => {
|
||||
&::before {
|
||||
position: absolute;
|
||||
background-color: transparent;
|
||||
background-color: rgb(var(--v-theme-primary));
|
||||
block-size: 100%;
|
||||
content: '';
|
||||
inline-size: 4px;
|
||||
|
||||
@@ -10,6 +10,7 @@ import logo from '@images/logo.png'
|
||||
import { urlBase64ToUint8Array } from '@/@core/utils/navigator'
|
||||
import { SUPPORTED_LOCALES, SupportedLocale } from '@/types/i18n'
|
||||
import { getCurrentLocale, setI18nLanguage } from '@/plugins/i18n'
|
||||
import { useTheme } from 'vuetify'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
@@ -42,12 +43,24 @@ const usernameInput = ref()
|
||||
|
||||
// 语言选择菜单
|
||||
const langMenu = ref(false)
|
||||
|
||||
// 当前语言
|
||||
const currentLocale = ref(getCurrentLocale())
|
||||
|
||||
// 当前主题
|
||||
const vuetifyTheme = useTheme()
|
||||
|
||||
// 判断是否为透明主题
|
||||
const isTransparentTheme = computed(() => {
|
||||
return vuetifyTheme.name.value === 'transparent'
|
||||
})
|
||||
|
||||
// 可用的语言列表
|
||||
const locales = Object.values(SUPPORTED_LOCALES)
|
||||
|
||||
// 登录按钮 loading
|
||||
const loading = ref(false)
|
||||
|
||||
// 切换语言
|
||||
async function switchLanguage(locale: SupportedLocale) {
|
||||
await setI18nLanguage(locale)
|
||||
@@ -103,6 +116,8 @@ async function afterLogin(superuser: boolean) {
|
||||
router.push(authStore.originalPath ?? '/')
|
||||
// 订阅推送通知
|
||||
if (superuser) await subscribeForPushNotifications()
|
||||
// 登录按钮 loading
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
// 登录获取token事件
|
||||
@@ -113,6 +128,10 @@ function login() {
|
||||
if (!form.value.username || !form.value.password || (isOTP.value && !form.value.otp_password)) {
|
||||
return
|
||||
}
|
||||
|
||||
// 登录按钮 loading
|
||||
loading.value = true
|
||||
|
||||
// 用户名密码
|
||||
const formData = new FormData()
|
||||
|
||||
@@ -155,6 +174,8 @@ function login() {
|
||||
else if (error.response.status === 403) errorMessage.value = t('login.permissionDenied')
|
||||
else if (error.response.status === 500) errorMessage.value = t('login.serverError')
|
||||
else errorMessage.value = `${t('login.loginFailed')} ${error.response.status},${t('login.checkCredentials')}`
|
||||
// 登录按钮 loading
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
@@ -176,7 +197,12 @@ onMounted(async () => {
|
||||
<div class="relative flex min-h-screen flex-col items-center justify-center">
|
||||
<!-- 登录表单 -->
|
||||
<div class="auth-wrapper d-flex align-center justify-center">
|
||||
<VCard class="auth-card px-7 py-3 w-full h-full" max-width="24rem" border>
|
||||
<VCard
|
||||
class="auth-card px-7 py-3 w-full h-full"
|
||||
:class="{ 'glass-effect': !isTransparentTheme }"
|
||||
max-width="24rem"
|
||||
border
|
||||
>
|
||||
<VCardItem class="justify-center">
|
||||
<template #prepend>
|
||||
<div class="d-flex pe-0">
|
||||
@@ -252,7 +278,9 @@ onMounted(async () => {
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<!-- login button -->
|
||||
<VBtn block type="submit" @click="login" prepend-icon="mdi-login"> {{ t('login.login') }} </VBtn>
|
||||
<VBtn block type="submit" @click="login" prepend-icon="mdi-login" :loading="loading">
|
||||
{{ t('login.login') }}
|
||||
</VBtn>
|
||||
<VAlert v-if="errorMessage" type="error" variant="tonal" class="mt-3">
|
||||
{{ errorMessage }}
|
||||
</VAlert>
|
||||
@@ -282,4 +310,9 @@ onMounted(async () => {
|
||||
inset-block-start: 8px;
|
||||
inset-inline-end: 8px;
|
||||
}
|
||||
|
||||
.glass-effect {
|
||||
backdrop-filter: blur(10px) !important;
|
||||
background: rgba(var(--v-theme-surface), 0.7) !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -14,12 +14,6 @@ const mediaid = route.query?.mediaid?.toString()
|
||||
// 类型:电影、电视剧
|
||||
const type = route.query?.type?.toString()
|
||||
|
||||
// 媒体信息来源:TMDB、豆瓣
|
||||
const source = route.query?.source?.toString() || 'themoviedb'
|
||||
|
||||
// TMDB ID
|
||||
const page = route.query?.page?.toString() || '1'
|
||||
|
||||
// 标题
|
||||
const title = route.query?.title?.toString()
|
||||
|
||||
@@ -29,6 +23,6 @@ const year = route.query?.year?.toString()
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<MediaDetailView :mediaid="mediaid" :type="type" :source="source" :page="page" :title="title" :year="year" />
|
||||
<MediaDetailView :mediaid="mediaid" :type="type" :title="title" :year="year" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -3,6 +3,9 @@ import api from '@/api'
|
||||
import { RecommendSource } from '@/api/types'
|
||||
import MediaCardSlideView from '@/views/discover/MediaCardSlideView.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useDisplay } from 'vuetify'
|
||||
|
||||
const display = useDisplay()
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
@@ -115,14 +118,16 @@ async function loadExtraRecommendSources() {
|
||||
try {
|
||||
extraRecommendSources.value = await api.get('recommend/source')
|
||||
if (extraRecommendSources.value.length > 0) {
|
||||
viewList.push(
|
||||
...extraRecommendSources.value.map(source => ({
|
||||
apipath: source.api_path,
|
||||
linkurl: `/browse/recommend/${source.api_path}?title=${source.name}`,
|
||||
title: source.name,
|
||||
type: source.type,
|
||||
})),
|
||||
)
|
||||
extraRecommendSources.value.map(source => {
|
||||
if (!viewList.some(item => item.apipath === source.api_path)) {
|
||||
viewList.push({
|
||||
apipath: source.api_path,
|
||||
linkurl: `/browse/${source.api_path}&title=${source.name}`,
|
||||
title: source.name,
|
||||
type: source.type,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
@@ -233,7 +238,7 @@ onActivated(async () => {
|
||||
</div>
|
||||
|
||||
<!-- 设置面板 -->
|
||||
<VDialog v-model="dialog" width="35rem" class="settings-dialog" scrollable>
|
||||
<VDialog v-model="dialog" width="35rem" class="settings-dialog" scrollable :fullscreen="!display.mdAndUp.value">
|
||||
<VCard class="settings-card">
|
||||
<VCardItem class="settings-card-header">
|
||||
<VCardTitle>
|
||||
@@ -246,7 +251,7 @@ onActivated(async () => {
|
||||
<VCardText>
|
||||
<p class="settings-hint">{{ t('recommend.selectContentToDisplay') }}</p>
|
||||
<div class="settings-grid">
|
||||
<div
|
||||
<VCard
|
||||
v-for="item in viewList"
|
||||
:key="item.title"
|
||||
class="setting-item"
|
||||
@@ -266,11 +271,10 @@ onActivated(async () => {
|
||||
</div>
|
||||
<span class="setting-label">{{ item.title }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</VCard>
|
||||
</div>
|
||||
</VCardText>
|
||||
<VDivider />
|
||||
<VCardActions class="pt-5">
|
||||
<VCardActions class="pt-3">
|
||||
<VBtn variant="text" @click="Object.keys(enableConfig).forEach(key => (enableConfig[key] = true))">
|
||||
{{ t('recommend.selectAll') }}
|
||||
</VBtn>
|
||||
@@ -278,7 +282,7 @@ onActivated(async () => {
|
||||
{{ t('recommend.selectNone') }}
|
||||
</VBtn>
|
||||
<VSpacer />
|
||||
<VBtn @click="saveConfig" variant="elevated" color="primary" class="px-5">
|
||||
<VBtn @click="saveConfig" color="primary" class="px-5">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-content-save" />
|
||||
</template>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { debounce } from 'lodash-es'
|
||||
import NoDataFound from '@/components/NoDataFound.vue'
|
||||
import api from '@/api'
|
||||
import type { Context } from '@/api/types'
|
||||
@@ -51,6 +52,9 @@ const progressText = ref('')
|
||||
// 加载进度
|
||||
const progressValue = ref(0)
|
||||
|
||||
// 进度是否有效
|
||||
const progressEnabled = ref(false)
|
||||
|
||||
// 加载进度SSE
|
||||
const progressEventSource = ref<EventSource>()
|
||||
|
||||
@@ -60,23 +64,30 @@ const errorTitle = ref(t('resource.noData'))
|
||||
// 错误描述
|
||||
const errorDescription = ref(t('resource.noResourceFound'))
|
||||
|
||||
// 添加安全超时,确保进度条不会永远卡住
|
||||
const watchProgressValue = watch(
|
||||
progressValue,
|
||||
debounce(async () => {
|
||||
if (progressEventSource.value && progressValue.value < 100) {
|
||||
console.warn('卡进度超时 关闭进度条')
|
||||
stopLoadingProgress()
|
||||
}
|
||||
}, 60_000),
|
||||
)
|
||||
|
||||
// 使用SSE监听加载进度
|
||||
function startLoadingProgress() {
|
||||
watchProgressValue.resume()
|
||||
progressText.value = t('resource.searching')
|
||||
progressValue.value = 10 // 初始进度设为10%,确保进度条显示
|
||||
progressValue.value = 0
|
||||
progressEnabled.value = false
|
||||
progressEventSource.value = new EventSource(`${import.meta.env.VITE_API_BASE_URL}system/progress/search`)
|
||||
progressEventSource.value.onmessage = event => {
|
||||
const progress = JSON.parse(event.data)
|
||||
if (progress) {
|
||||
progressText.value = progress.text
|
||||
progressValue.value = progress.value
|
||||
|
||||
// 搜索完成条件调整:只有明确完成时才关闭
|
||||
if (progress.text.includes('完成') && progress.value >= 99) {
|
||||
setTimeout(() => {
|
||||
stopLoadingProgress()
|
||||
}, 1000) // 延迟1秒关闭,确保用户能看到100%
|
||||
}
|
||||
progressEnabled.value = progress.enable
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,26 +97,22 @@ function startLoadingProgress() {
|
||||
stopLoadingProgress()
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
// 添加安全超时,确保不会永远卡住
|
||||
setTimeout(() => {
|
||||
if (progressEventSource.value && progressValue.value < 100) {
|
||||
stopLoadingProgress()
|
||||
}
|
||||
}, 60000) // 60秒超时
|
||||
}
|
||||
|
||||
// 停止监听加载进度
|
||||
function stopLoadingProgress() {
|
||||
watchProgressValue.pause()
|
||||
if (progressEventSource.value) {
|
||||
progressEventSource.value.close()
|
||||
progressEventSource.value = undefined
|
||||
|
||||
// 确保进度显示100%,然后再渐进清零
|
||||
progressValue.value = 100
|
||||
setTimeout(() => {
|
||||
progressValue.value = 0
|
||||
progressEnabled.value = false
|
||||
}, 1500) // 延长到1.5秒,让用户有足够时间看到完成状态
|
||||
}
|
||||
// 确保进度显示100%,然后再渐进清零
|
||||
progressValue.value = 100
|
||||
setTimeout(() => {
|
||||
progressValue.value = 0
|
||||
}, 1500) // 延长到1.5秒,让用户有足够时间看到完成状态
|
||||
}
|
||||
|
||||
// 设置视图类型
|
||||
@@ -186,7 +193,7 @@ onUnmounted(() => {
|
||||
<div>
|
||||
<!-- 加载进度条 -->
|
||||
<VFadeTransition>
|
||||
<div v-if="progressValue > 0" class="search-progress-container">
|
||||
<div v-if="progressValue > 0 || progressEnabled" class="search-progress-container">
|
||||
<VCard elevation="3" class="search-progress-card">
|
||||
<div class="progress-header">
|
||||
<VIcon icon="mdi-movie-search" color="primary" size="small" class="me-2" />
|
||||
@@ -273,17 +280,7 @@ onUnmounted(() => {
|
||||
</div>
|
||||
|
||||
<!-- 初始加载状态 -->
|
||||
<div v-else-if="!isRefreshed && !progressValue" class="initial-loading-container">
|
||||
<div class="initial-loading-content">
|
||||
<div class="wave-loader">
|
||||
<div class="wave-dot"></div>
|
||||
<div class="wave-dot"></div>
|
||||
<div class="wave-dot"></div>
|
||||
<div class="wave-dot"></div>
|
||||
</div>
|
||||
<div class="initial-loading-text">{{ t('resource.searching') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<LoadingBanner v-else-if="!isRefreshed && !(progressEnabled || progressValue > 0)" />
|
||||
<!-- 滚动到顶部按钮 -->
|
||||
<VScrollToTopBtn />
|
||||
</div>
|
||||
@@ -452,70 +449,6 @@ onUnmounted(() => {
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
/* 初始的加载状态 */
|
||||
.initial-loading-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-block-size: 50vh;
|
||||
}
|
||||
|
||||
.initial-loading-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.wave-loader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
block-size: 40px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.wave-dot {
|
||||
border-radius: 50%;
|
||||
animation: wave 1.5s ease-in-out infinite;
|
||||
background-color: rgb(var(--v-theme-primary));
|
||||
block-size: 8px;
|
||||
inline-size: 8px;
|
||||
}
|
||||
|
||||
.wave-dot:nth-child(1) {
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.wave-dot:nth-child(2) {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
.wave-dot:nth-child(3) {
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
|
||||
.wave-dot:nth-child(4) {
|
||||
animation-delay: 0.6s;
|
||||
}
|
||||
|
||||
@keyframes wave {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateY(-15px);
|
||||
}
|
||||
}
|
||||
|
||||
.initial-loading-text {
|
||||
color: rgb(var(--v-theme-primary));
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.search-results-container {
|
||||
position: relative;
|
||||
min-block-size: 50vh;
|
||||
|
||||
@@ -8,9 +8,10 @@ import AccountSettingAbout from '@/views/setting/AccountSettingAbout.vue'
|
||||
import AccountSettingSearch from '@/views/setting/AccountSettingSearch.vue'
|
||||
import AccountSettingSubscribe from '@/views/setting/AccountSettingSubscribe.vue'
|
||||
import AccountSettingSystem from '@/views/setting/AccountSettingSystem.vue'
|
||||
import AccountSettingScheduler from '@/views/setting/AccountSettingScheduler.vue'
|
||||
import AccountSettingService from '@/views/setting/AccountSettingService.vue'
|
||||
import AccountSettingDirectory from '@/views/setting/AccountSettingDirectory.vue'
|
||||
import AccountSettingRule from '@/views/setting/AccountSettingRule.vue'
|
||||
import AccountSettingCache from '@/views/setting/AccountSettingCache.vue'
|
||||
import { getSettingTabs } from '@/router/i18n-menu'
|
||||
|
||||
const route = useRoute()
|
||||
@@ -81,7 +82,16 @@ const settingTabs = computed(() => getSettingTabs())
|
||||
<VWindowItem value="scheduler">
|
||||
<transition name="fade-slide" appear>
|
||||
<div>
|
||||
<AccountSettingScheduler />
|
||||
<AccountSettingService />
|
||||
</div>
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
|
||||
<!-- 缓存 -->
|
||||
<VWindowItem value="cache">
|
||||
<transition name="fade-slide" appear>
|
||||
<div>
|
||||
<AccountSettingCache />
|
||||
</div>
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
|
||||
@@ -39,7 +39,7 @@ export function getBrowserLocale(): SupportedLocale | null {
|
||||
return navigatorLocale.includes(locale.split('-')[0])
|
||||
})
|
||||
|
||||
return (locale as SupportedLocale) || null
|
||||
return (locale as SupportedLocale) || 'zh-CN'
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -170,6 +170,12 @@ export function getSettingTabs() {
|
||||
tab: 'scheduler',
|
||||
description: t('settingTabs.scheduler.description'),
|
||||
},
|
||||
{
|
||||
title: t('settingTabs.cache.title'),
|
||||
icon: 'mdi-database',
|
||||
tab: 'cache',
|
||||
description: t('settingTabs.cache.description'),
|
||||
},
|
||||
{
|
||||
title: t('settingTabs.notification.title'),
|
||||
icon: 'mdi-bell',
|
||||
|
||||
@@ -58,6 +58,10 @@ html.v-overlay-scroll-blocked {
|
||||
margin-block-start: env(safe-area-inset-top);
|
||||
}
|
||||
|
||||
.v-dialog > .v-overlay__content > .v-card > .v-card-item {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
/* router view transition fade-slide */
|
||||
.fade-slide-leave-active,
|
||||
.fade-slide-enter-active {
|
||||
@@ -115,12 +119,14 @@ html.v-overlay-scroll-blocked {
|
||||
|
||||
// 美化滚动条
|
||||
::-webkit-scrollbar {
|
||||
block-size: 8px;
|
||||
inline-size: 8px;
|
||||
block-size: 4px;
|
||||
inline-size: 4px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
border-radius: 3px;
|
||||
border-radius: 2px;
|
||||
background: rgb(var(--v-theme-perfect-scrollbar-thumb));
|
||||
box-shadow: inset 0 0 10px rgba(0,0,0,20%);
|
||||
|
||||
@@ -131,6 +137,16 @@ html.v-overlay-scroll-blocked {
|
||||
}
|
||||
}
|
||||
|
||||
// 当鼠标悬停在可滚动元素上时显示滚动条
|
||||
*:hover::-webkit-scrollbar {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
// 当元素正在滚动时显示滚动条
|
||||
*:active::-webkit-scrollbar {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.v-alert--variant-elevated, .v-alert--variant-flat {
|
||||
background: rgb(var(--v-table-header-background));
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
||||
@@ -160,88 +176,56 @@ html.v-overlay-scroll-blocked {
|
||||
}
|
||||
|
||||
.grid-site-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
||||
grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
|
||||
.grid-media-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(9.375rem, 1fr));
|
||||
grid-template-columns: repeat(auto-fill, minmax(9rem, 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(18rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
|
||||
}
|
||||
|
||||
.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-customrule-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
|
||||
.grid-subscribe-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
|
||||
}
|
||||
|
||||
.grid-user-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
|
||||
@media (width <= 600px) {
|
||||
.user-list-container {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.grid-user-card {
|
||||
gap: 1rem;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 601px) and (width <= 960px) {
|
||||
.grid-user-card{
|
||||
gap: 1rem;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 961px) {
|
||||
.grid-user-card {
|
||||
gap: 1.5rem;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
.grid-app-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
|
||||
}
|
||||
|
||||
.grid-workflow-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
||||
}
|
||||
|
||||
.v-tabs:not(.v-tabs-pill).v-tabs--horizontal {
|
||||
|
||||
@@ -12,6 +12,20 @@ interface RemoteModule {
|
||||
url: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个远程模块信息
|
||||
* @param id 远程模块ID
|
||||
*/
|
||||
async function fetchSingleRemoteModule(id: string): Promise<RemoteModule | null> {
|
||||
try {
|
||||
const modules = await fetchRemoteModules()
|
||||
return modules.find(module => module.id === id) || null
|
||||
} catch (error) {
|
||||
console.error(`获取远程模块信息失败: ${id}`, error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载远程组件
|
||||
* @param id 远程模块ID
|
||||
@@ -22,8 +36,24 @@ export async function loadRemoteComponent(id: string, componentName: string = 'P
|
||||
const module = await __federation_method_getRemote(id, `./${componentName}`)
|
||||
return __federation_method_unwrapDefault(module)
|
||||
} catch (error) {
|
||||
console.error(`加载远程组件失败: ${id}/${componentName}`, error)
|
||||
throw error
|
||||
// 组件未注册,尝试重新注册
|
||||
try {
|
||||
const moduleInfo = await fetchSingleRemoteModule(id)
|
||||
if (moduleInfo) {
|
||||
console.log(`组件未注册,正在重新注册: ${id}`)
|
||||
injectRemoteModule(moduleInfo)
|
||||
|
||||
// 重新尝试加载组件
|
||||
const module = await __federation_method_getRemote(id, `./${componentName}`)
|
||||
return __federation_method_unwrapDefault(module)
|
||||
} else {
|
||||
console.error(`无法找到远程模块信息: ${id}`)
|
||||
throw new Error(`无法找到远程模块信息: ${id}`)
|
||||
}
|
||||
} catch (retryError) {
|
||||
console.error(`重新注册并加载组件失败: ${id}/${componentName}`, retryError)
|
||||
throw retryError
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ onActivated(() => {
|
||||
</template>
|
||||
<VCardTitle>{{ t('dashboard.library') }}</VCardTitle>
|
||||
</VCardItem>
|
||||
<div class="grid gap-4 grid-backdrop-card mx-3" tabindex="0">
|
||||
<div class="grid gap-4 grid-backdrop-card mx-3 mb-3" tabindex="0">
|
||||
<LibraryCard v-for="item in libraryList" :key="item.id" :media="item" height="10rem" />
|
||||
</div>
|
||||
</VCard>
|
||||
|
||||
@@ -70,7 +70,7 @@ onActivated(() => {
|
||||
<VCardTitle>{{ t('dashboard.playing') }}</VCardTitle>
|
||||
</VCardItem>
|
||||
|
||||
<div class="grid gap-4 grid-backdrop-card mx-3" tabindex="0">
|
||||
<div class="grid gap-4 grid-backdrop-card mx-3 mb-3" tabindex="0">
|
||||
<BackdropCard v-for="item in playingList" :key="item.id" :media="item" height="10rem" />
|
||||
</div>
|
||||
</VCard>
|
||||
|
||||
@@ -112,7 +112,7 @@ async function fetchData({ done }: { done: any }) {
|
||||
|
||||
<template>
|
||||
<LoadingBanner v-if="!isRefreshed" class="mt-12" />
|
||||
<VInfiniteScroll mode="intersect" side="end" :items="dataList" class="overflow-visible pt-3" @load="fetchData">
|
||||
<VInfiniteScroll mode="intersect" side="end" :items="dataList" class="overflow-visible pt-3 px-2" @load="fetchData">
|
||||
<template #loading />
|
||||
<template #empty />
|
||||
<div v-if="dataList.length > 0" class="grid gap-4 grid-media-card" tabindex="0">
|
||||
|
||||
@@ -113,7 +113,7 @@ async function fetchData({ done }: { done: any }) {
|
||||
|
||||
<template>
|
||||
<LoadingBanner v-if="!isRefreshed" class="mt-12" />
|
||||
<VInfiniteScroll mode="intersect" side="end" :items="dataList" class="overflow-visible" @load="fetchData">
|
||||
<VInfiniteScroll mode="intersect" side="end" :items="dataList" class="overflow-visible px-3" @load="fetchData">
|
||||
<template #loading />
|
||||
<template #empty />
|
||||
<div v-if="dataList.length > 0" class="grid gap-4 grid-media-card" tabindex="0">
|
||||
|
||||
@@ -114,7 +114,7 @@ onBeforeMount(() => {
|
||||
'ring-1 ring-gray-700': isImageLoaded,
|
||||
}"
|
||||
>
|
||||
<VImg v-img :src="getPersonImage()" cover @load="isImageLoaded = true" />
|
||||
<VImg :src="getPersonImage()" cover @load="isImageLoaded = true" />
|
||||
</VAvatar>
|
||||
<div class="ms-3">
|
||||
<h1 class="text-3xl lg:text-4xl text-center text-lg-left">
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,6 @@ import router from '@/router'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import { formatFileSize } from '@/@core/utils/formatters'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { storageAttributes } from '@/api/constants'
|
||||
|
||||
// i18n
|
||||
const { t } = useI18n()
|
||||
@@ -683,7 +682,7 @@ onMounted(() => {
|
||||
<VDivider />
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="w-auto">
|
||||
<VSelect v-model="itemsPerPage" :items="pageRange" density="compact" flat />
|
||||
<VSelect v-model="itemsPerPage" :items="pageRange" density="compact" flat class="ms-1" />
|
||||
</div>
|
||||
<div class="w-auto text-sm">{{ t('transferHistory.pageInfo', pageTip) }} {{ totalItems }}</div>
|
||||
<VPagination
|
||||
|
||||
470
src/views/setting/AccountSettingCache.vue
Normal file
470
src/views/setting/AccountSettingCache.vue
Normal file
@@ -0,0 +1,470 @@
|
||||
<script lang="ts" setup>
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import api from '@/api'
|
||||
import type { TorrentCacheData, TorrentCacheItem } from '@/api/types'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import { formatFileSize, formatDateDifference } from '@core/utils/formatters'
|
||||
import { useConfirm } from '@/composables/useConfirm'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
|
||||
const display = useDisplay()
|
||||
const appMode = inject('pwaMode') && display.mdAndDown.value
|
||||
|
||||
// 从 provide 中获取全局设置
|
||||
const globalSettings: any = inject('globalSettings')
|
||||
|
||||
// 确认框
|
||||
const createConfirm = useConfirm()
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
// 缓存数据
|
||||
const cacheData = ref<TorrentCacheData>({
|
||||
count: 0,
|
||||
sites: 0,
|
||||
data: [],
|
||||
})
|
||||
|
||||
// 筛选条件
|
||||
const titleFilter = ref<string | null>(null)
|
||||
const siteFilter = ref<string | null>(null)
|
||||
|
||||
// 获取所有站点选项
|
||||
const siteOptions = computed(() => {
|
||||
const sites = new Set<string>()
|
||||
cacheData.value.data.forEach(item => {
|
||||
if (item.site_name) {
|
||||
sites.add(item.site_name)
|
||||
}
|
||||
})
|
||||
return Array.from(sites).sort()
|
||||
})
|
||||
|
||||
// 筛选后的数据
|
||||
const filteredData = computed(() => {
|
||||
return cacheData.value.data.filter(item => {
|
||||
const titleMatch = !titleFilter.value || item.title?.toLowerCase().includes(titleFilter.value?.toLowerCase())
|
||||
const siteMatch = !siteFilter.value || item.site_name === siteFilter.value
|
||||
return titleMatch && siteMatch
|
||||
})
|
||||
})
|
||||
|
||||
// 选中的缓存项
|
||||
const selectedItems = ref<string[]>([])
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
|
||||
// 重新识别对话框
|
||||
const reidentifyDialog = ref(false)
|
||||
const currentReidentifyItem = ref<TorrentCacheItem | null>(null)
|
||||
const tmdbId = ref<number | undefined>()
|
||||
const doubanId = ref<string | undefined>()
|
||||
|
||||
const tableStyle = computed(() => {
|
||||
return appMode ? '' : 'height: calc(100vh - 21rem - env(safe-area-inset-bottom)'
|
||||
})
|
||||
|
||||
// 调用API加载缓存数据
|
||||
async function loadCacheData() {
|
||||
try {
|
||||
loading.value = true
|
||||
const res: any = await api.get('torrent/cache')
|
||||
cacheData.value = res.data
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
$toast.error(t('setting.cache.loadFailed'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 清空所有缓存
|
||||
async function clearAllCache() {
|
||||
const isConfirmed = await createConfirm({
|
||||
type: 'warn',
|
||||
title: t('common.confirm'),
|
||||
content: t('setting.cache.clearConfirm'),
|
||||
})
|
||||
|
||||
if (!isConfirmed) return
|
||||
try {
|
||||
loading.value = true
|
||||
await api.delete('torrent/cache')
|
||||
$toast.success(t('setting.cache.clearSuccess'))
|
||||
await loadCacheData()
|
||||
selectedItems.value = []
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
$toast.error(t('setting.cache.clearFailed'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新缓存
|
||||
async function refreshCache() {
|
||||
try {
|
||||
loading.value = true
|
||||
const res: any = await api.post('torrent/cache/refresh')
|
||||
$toast.success(res.message || t('setting.cache.refreshSuccess'))
|
||||
await loadCacheData()
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
$toast.error(t('setting.cache.refreshFailed'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 删除选中的缓存项
|
||||
async function deleteSelectedItems() {
|
||||
if (selectedItems.value.length === 0) {
|
||||
$toast.warning(t('setting.cache.selectDeleteWarning'))
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
loading.value = true
|
||||
const deletePromises = selectedItems.value.map(hash => {
|
||||
const item = cacheData.value.data.find(d => d.hash === hash)
|
||||
if (item) {
|
||||
return api.delete(`torrent/cache/${item.domain}/${hash}`)
|
||||
}
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
await Promise.all(deletePromises)
|
||||
$toast.success(t('setting.cache.deleteSelectedSuccess', { count: selectedItems.value.length }))
|
||||
await loadCacheData()
|
||||
selectedItems.value = []
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
$toast.error(t('setting.cache.deleteSelectedFailed'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 删除单个缓存项
|
||||
async function deleteSingleItem(item: TorrentCacheItem) {
|
||||
try {
|
||||
loading.value = true
|
||||
await api.delete(`torrent/cache/${item.domain}/${item.hash}`)
|
||||
$toast.success(t('setting.cache.deleteSuccess'))
|
||||
await loadCacheData()
|
||||
// 从选中列表中移除
|
||||
const index = selectedItems.value.indexOf(item.hash)
|
||||
if (index > -1) {
|
||||
selectedItems.value.splice(index, 1)
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
$toast.error(t('setting.cache.deleteFailed'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 打开重新识别对话框
|
||||
function openReidentifyDialog(item: TorrentCacheItem) {
|
||||
currentReidentifyItem.value = item
|
||||
tmdbId.value = undefined
|
||||
doubanId.value = undefined
|
||||
reidentifyDialog.value = true
|
||||
}
|
||||
|
||||
// 重新识别
|
||||
async function performReidentify() {
|
||||
if (!currentReidentifyItem.value) return
|
||||
|
||||
try {
|
||||
loading.value = true
|
||||
const params: any = {}
|
||||
if (tmdbId.value) params.tmdbid = tmdbId.value
|
||||
if (doubanId.value) params.doubanid = doubanId.value
|
||||
|
||||
const res: any = await api.post(
|
||||
`torrent/cache/reidentify/${currentReidentifyItem.value.domain}/${currentReidentifyItem.value.hash}`,
|
||||
null,
|
||||
{
|
||||
params,
|
||||
},
|
||||
)
|
||||
|
||||
$toast.success(res.message || t('setting.cache.reidentifySuccess'))
|
||||
await loadCacheData()
|
||||
reidentifyDialog.value = false
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
$toast.error(t('setting.cache.reidentifyFailed'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取媒体类型颜色
|
||||
function getMediaTypeColor(type: string): string {
|
||||
switch (type) {
|
||||
case t('setting.cache.mediaType.movie'):
|
||||
return 'primary'
|
||||
case t('setting.cache.mediaType.tv'):
|
||||
return 'success'
|
||||
default:
|
||||
return 'default'
|
||||
}
|
||||
}
|
||||
|
||||
// 打开详情页面
|
||||
function openPageUrl(url: string) {
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadCacheData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>{{ t('setting.cache.title') }}</VCardTitle>
|
||||
<VCardSubtitle>{{ t('setting.cache.subtitle') }}</VCardSubtitle>
|
||||
|
||||
<template #append>
|
||||
<div class="d-flex gap-2">
|
||||
<VBtn icon color="primary" :loading="loading" @click="refreshCache">
|
||||
<VIcon>mdi-refresh</VIcon>
|
||||
<VTooltip activator="parent" location="bottom">{{ t('setting.cache.refresh') }}</VTooltip>
|
||||
</VBtn>
|
||||
|
||||
<VBtn
|
||||
icon
|
||||
color="warning"
|
||||
:loading="loading"
|
||||
:disabled="selectedItems.length === 0"
|
||||
@click="deleteSelectedItems"
|
||||
>
|
||||
<VIcon>mdi-delete-sweep</VIcon>
|
||||
<VTooltip activator="parent" location="bottom"
|
||||
>{{ t('setting.cache.deleteSelected') }} ({{ selectedItems.length }})</VTooltip
|
||||
>
|
||||
</VBtn>
|
||||
|
||||
<VBtn icon color="error" :loading="loading" @click="clearAllCache">
|
||||
<VIcon>mdi-delete-variant</VIcon>
|
||||
<VTooltip activator="parent" location="bottom">{{ t('setting.cache.clearAll') }}</VTooltip>
|
||||
</VBtn>
|
||||
</div>
|
||||
</template>
|
||||
</VCardItem>
|
||||
|
||||
<!-- 筛选框 -->
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol cols="6">
|
||||
<VTextField
|
||||
v-model="titleFilter"
|
||||
:label="t('setting.cache.filterByTitle')"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
clearable
|
||||
density="compact"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6">
|
||||
<VAutocomplete
|
||||
v-model="siteFilter"
|
||||
:label="t('setting.cache.filterBySite')"
|
||||
:items="siteOptions"
|
||||
prepend-inner-icon="mdi-web"
|
||||
clearable
|
||||
density="compact"
|
||||
:placeholder="t('setting.cache.selectSite')"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
|
||||
<!-- 缓存列表 -->
|
||||
<VDataTable
|
||||
v-model="selectedItems"
|
||||
:headers="[
|
||||
{ title: '', key: 'data-table-select', sortable: false, width: '48px' },
|
||||
{ title: t('setting.cache.poster'), key: 'poster', sortable: false, width: '80px' },
|
||||
{ title: t('setting.cache.torrentTitle'), key: 'title', sortable: true },
|
||||
{ title: t('setting.cache.site'), key: 'site_name', sortable: true, width: '120px' },
|
||||
{ title: t('setting.cache.size'), key: 'size', sortable: true, width: '100px' },
|
||||
{ title: t('setting.cache.publishTime'), key: 'pubdate', sortable: true, width: '150px' },
|
||||
{ title: t('setting.cache.recognitionResult'), key: 'media_info', sortable: false, width: '200px' },
|
||||
{ title: t('setting.cache.actions'), key: 'actions', sortable: false, width: '150px' },
|
||||
]"
|
||||
:items="filteredData"
|
||||
:loading="loading"
|
||||
item-value="hash"
|
||||
show-select
|
||||
hover
|
||||
fixed-header
|
||||
:items-per-page-text="t('common.itemsPerPage')"
|
||||
:no-data-text="t('common.noDataText')"
|
||||
:loading-text="t('common.loadingText')"
|
||||
:style="tableStyle"
|
||||
>
|
||||
<!-- 全选复选框 -->
|
||||
<template #header.data-table-select="{ allSelected, selectAll, someSelected }">
|
||||
<VCheckbox
|
||||
:indeterminate="someSelected && !allSelected"
|
||||
:model-value="allSelected"
|
||||
@update:model-value="(value: boolean | null) => selectAll(value as boolean)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- 海报列 -->
|
||||
<template #item.poster="{ item }">
|
||||
<div class="text-center">
|
||||
<VImg
|
||||
v-if="item.poster_path"
|
||||
:src="item.poster_path"
|
||||
:alt="item.media_name || item.title"
|
||||
cover
|
||||
rounded="md"
|
||||
class="w-12 my-1 ms-auto"
|
||||
/>
|
||||
<VIcon v-else size="x-large" color="grey-lighten-1">
|
||||
{{ item.media_type === 'movie' ? 'mdi-movie-open' : 'mdi-television-play' }}
|
||||
</VIcon>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 标题列 -->
|
||||
<template #item.title="{ item }">
|
||||
<div class="d-flex flex-column min-w-40">
|
||||
<div class="text-subtitle-2 font-weight-bold">
|
||||
{{ item.title }}
|
||||
</div>
|
||||
<div v-if="item.description" class="text-caption text-grey">
|
||||
{{ item.description }}
|
||||
</div>
|
||||
<div v-if="item.season_episode || item.resource_term" class="text-caption text-primary mt-1">
|
||||
{{ item.season_episode }} {{ item.resource_term }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 大小列 -->
|
||||
<template #item.size="{ item }">
|
||||
{{ formatFileSize(item.size) }}
|
||||
</template>
|
||||
|
||||
<!-- 发布时间列 -->
|
||||
<template #item.pubdate="{ item }">
|
||||
{{ formatDateDifference(item.pubdate || '') }}
|
||||
</template>
|
||||
|
||||
<!-- 识别结果列 -->
|
||||
<template #item.media_info="{ item }">
|
||||
<div v-if="item.media_name" class="d-flex flex-column">
|
||||
<div class="text-subtitle-2">
|
||||
{{ item.media_name }}
|
||||
<span v-if="item.media_year" class="text-caption text-grey"> ({{ item.media_year }}) </span>
|
||||
</div>
|
||||
<div>
|
||||
<VChip v-if="item.media_type" :color="getMediaTypeColor(item.media_type)" size="x-small">
|
||||
{{ item.media_type }}
|
||||
</VChip>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-caption text-grey">
|
||||
{{ t('setting.cache.unrecognized') }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 操作列 -->
|
||||
<template #item.actions="{ item }">
|
||||
<div class="d-flex gap-1">
|
||||
<VBtn icon size="small" color="primary" variant="text" @click="openReidentifyDialog(item)">
|
||||
<VIcon size="16">mdi-text-recognition</VIcon>
|
||||
</VBtn>
|
||||
|
||||
<VBtn icon size="small" color="error" variant="text" @click="deleteSingleItem(item)">
|
||||
<VIcon size="16">mdi-delete</VIcon>
|
||||
</VBtn>
|
||||
|
||||
<VBtn
|
||||
v-if="item.page_url"
|
||||
icon
|
||||
size="small"
|
||||
color="info"
|
||||
variant="text"
|
||||
@click="openPageUrl(item.page_url || '')"
|
||||
target="_blank"
|
||||
>
|
||||
<VIcon size="16">mdi-open-in-new</VIcon>
|
||||
</VBtn>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<template #no-data>
|
||||
<div class="text-center pa-4">
|
||||
<VIcon size="64" class="mb-4"> mdi-database-off </VIcon>
|
||||
<div class="text-body-2 text-grey">
|
||||
{{ t('setting.cache.noData') }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</VDataTable>
|
||||
</VCard>
|
||||
|
||||
<!-- 重新识别对话框 -->
|
||||
<VDialog v-model="reidentifyDialog" scrollable max-width="35rem">
|
||||
<VCard>
|
||||
<VCardItem class="py-2">
|
||||
<template #prepend>
|
||||
<VIcon>mdi-text-recognition</VIcon>
|
||||
</template>
|
||||
<VCardTitle>{{ t('setting.cache.reidentifyDialog.title') }}</VCardTitle>
|
||||
<VCardSubtitle>{{ currentReidentifyItem?.title }}</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VDialogCloseBtn @click="reidentifyDialog = false" />
|
||||
<VDivider />
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
v-if="globalSettings.RECOGNIZE_SOURCE === 'themoviedb'"
|
||||
v-model="tmdbId"
|
||||
:label="t('setting.cache.reidentifyDialog.tmdbId')"
|
||||
:hint="t('setting.cache.reidentifyDialog.tmdbIdHint')"
|
||||
clearable
|
||||
prepend-inner-icon="mdi-id-card"
|
||||
persistent-hint
|
||||
/>
|
||||
<VTextField
|
||||
v-else
|
||||
v-model="doubanId"
|
||||
:label="t('setting.cache.reidentifyDialog.doubanId')"
|
||||
:hint="t('setting.cache.reidentifyDialog.doubanIdHint')"
|
||||
clearable
|
||||
prepend-inner-icon="mdi-id-card"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VAlert type="info" variant="tonal" class="mt-4">
|
||||
{{ t('setting.cache.reidentifyDialog.autoHint') }}
|
||||
</VAlert>
|
||||
</VCardText>
|
||||
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
<VBtn color="primary" :loading="loading" prepend-icon="mdi-check" @click="performReidentify">
|
||||
{{ t('setting.cache.reidentifyDialog.confirm') }}
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
@@ -59,19 +59,6 @@ async function loadSystemSettings() {
|
||||
}
|
||||
}
|
||||
|
||||
// 重载系统生效配置
|
||||
async function reloadSystem() {
|
||||
progressDialog.value = true
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/reload')
|
||||
if (result.success) $toast.success(t('setting.system.reloadSuccess'))
|
||||
else $toast.error(t('setting.system.reloadFailed'))
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
progressDialog.value = false
|
||||
}
|
||||
|
||||
// 移动结束
|
||||
function orderDirectoryCards() {
|
||||
// 更新所有目录的优先级
|
||||
@@ -124,7 +111,6 @@ async function saveDirectories() {
|
||||
const result: { [key: string]: any } = await api.post('system/setting/Directories', directories.value)
|
||||
if (result.success) {
|
||||
$toast.success(t('setting.directory.directorySaveSuccess'))
|
||||
await reloadSystem()
|
||||
} else $toast.error(t('setting.directory.directorySaveFailed'))
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
@@ -239,7 +225,9 @@ onMounted(() => {
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<div class="d-flex flex-wrap gap-4 mt-4">
|
||||
<VBtn type="submit" class="me-2" @click="saveStorages"> {{ t('common.save') }} </VBtn>
|
||||
<VBtn type="submit" class="me-2" @click="saveStorages" prepend-icon="mdi-content-save">
|
||||
{{ t('common.save') }}
|
||||
</VBtn>
|
||||
<VBtn color="success" variant="tonal" @click="addStorage">
|
||||
<VIcon icon="mdi-plus" />
|
||||
</VBtn>
|
||||
@@ -279,7 +267,9 @@ onMounted(() => {
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<div class="d-flex flex-wrap gap-4 mt-4">
|
||||
<VBtn type="submit" @click="saveDirectories"> {{ t('common.save') }} </VBtn>
|
||||
<VBtn type="submit" @click="saveDirectories" prepend-icon="mdi-content-save">
|
||||
{{ t('common.save') }}
|
||||
</VBtn>
|
||||
<VBtn color="success" variant="tonal" @click="addDirectory">
|
||||
<VIcon icon="mdi-plus" />
|
||||
</VBtn>
|
||||
@@ -305,6 +295,7 @@ onMounted(() => {
|
||||
:label="t('setting.directory.scrapSource')"
|
||||
:hint="t('setting.directory.scrapSourceHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-database"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
@@ -315,6 +306,7 @@ onMounted(() => {
|
||||
persistent-hint
|
||||
clearable
|
||||
active
|
||||
prepend-inner-icon="mdi-movie-open"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
@@ -325,6 +317,7 @@ onMounted(() => {
|
||||
persistent-hint
|
||||
clearable
|
||||
active
|
||||
prepend-inner-icon="mdi-television"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -332,7 +325,9 @@ onMounted(() => {
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<div class="d-flex flex-wrap gap-4 mt-4">
|
||||
<VBtn type="submit" @click="saveSystemSettings(SystemSettings.Basic)"> {{ t('common.save') }}</VBtn>
|
||||
<VBtn type="submit" @click="saveSystemSettings(SystemSettings.Basic)" prepend-icon="mdi-content-save">
|
||||
{{ t('common.save') }}
|
||||
</VBtn>
|
||||
</div>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
|
||||
@@ -7,7 +7,10 @@ import NotificationChannelCard from '@/components/cards/NotificationChannelCard.
|
||||
import ProgressDialog from '@/components/dialog/ProgressDialog.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { notificationSwitchDict } from '@/api/constants'
|
||||
import { useTheme } from 'vuetify'
|
||||
import { useTheme, useDisplay } from 'vuetify'
|
||||
|
||||
// 显示器宽度
|
||||
const display = useDisplay()
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
@@ -100,19 +103,6 @@ const notificationTime = ref({
|
||||
end: '23:59',
|
||||
})
|
||||
|
||||
// 重载系统生效配置
|
||||
async function reloadSystem() {
|
||||
progressDialog.value = true
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/reload')
|
||||
if (result.success) $toast.success(t('setting.system.reloadSuccess'))
|
||||
else $toast.error(t('setting.system.reloadFailed'))
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
progressDialog.value = false
|
||||
}
|
||||
|
||||
// 添加通知渠道
|
||||
function addNotification(notification: string) {
|
||||
let name = `${t('setting.notification.channel')}${notifications.value.length + 1}`
|
||||
@@ -196,7 +186,6 @@ async function saveNotificationSetting() {
|
||||
const result: { [key: string]: any } = await api.post('system/setting/Notifications', notifications.value)
|
||||
if (result.success) {
|
||||
$toast.success(t('setting.notification.saveSuccess'))
|
||||
await reloadSystem()
|
||||
} else $toast.error(t('setting.notification.saveFailed'))
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
@@ -209,7 +198,6 @@ async function saveNotificationTime() {
|
||||
const result: { [key: string]: any } = await api.post('system/setting/NotificationSendTime', notificationTime.value)
|
||||
if (result.success) {
|
||||
$toast.success(t('setting.notification.timeSaveSuccess'))
|
||||
await reloadSystem()
|
||||
} else $toast.error(t('setting.notification.timeSaveFailed'))
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
@@ -290,7 +278,9 @@ onMounted(() => {
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<div class="d-flex flex-wrap gap-4 mt-4">
|
||||
<VBtn mtype="submit" @click="saveNotificationSetting"> {{ t('common.save') }} </VBtn>
|
||||
<VBtn mtype="submit" @click="saveNotificationSetting" prepend-icon="mdi-content-save">
|
||||
{{ t('common.save') }}
|
||||
</VBtn>
|
||||
<VBtn color="success" variant="tonal">
|
||||
<VIcon icon="mdi-plus" />
|
||||
<VMenu :activator="'parent'" :close-on-content-click="true">
|
||||
@@ -313,6 +303,9 @@ onMounted(() => {
|
||||
<VListItem @click="addNotification('webpush')">
|
||||
<VListItemTitle>{{ t('setting.notification.webPush') }}</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem @click="addNotification('custom')">
|
||||
<VListItemTitle>{{ t('setting.system.custom') }}</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</VBtn>
|
||||
@@ -392,11 +385,12 @@ onMounted(() => {
|
||||
</tr>
|
||||
</tbody>
|
||||
</VTable>
|
||||
<VDivider />
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<div class="d-flex flex-wrap gap-4 mt-4">
|
||||
<VBtn type="submit" @click="saveNotificationSwitchs"> {{ t('common.save') }} </VBtn>
|
||||
<VBtn type="submit" @click="saveNotificationSwitchs" prepend-icon="mdi-content-save">
|
||||
{{ t('common.save') }}
|
||||
</VBtn>
|
||||
</div>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
@@ -413,17 +407,29 @@ onMounted(() => {
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol cols="6">
|
||||
<VTextField v-model="notificationTime.start" :label="t('setting.notification.startTime')" type="time" />
|
||||
<VTextField
|
||||
v-model="notificationTime.start"
|
||||
:label="t('setting.notification.startTime')"
|
||||
type="time"
|
||||
prepend-inner-icon="mdi-clock-start"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6">
|
||||
<VTextField v-model="notificationTime.end" :label="t('setting.notification.endTime')" type="time" />
|
||||
<VTextField
|
||||
v-model="notificationTime.end"
|
||||
:label="t('setting.notification.endTime')"
|
||||
type="time"
|
||||
prepend-inner-icon="mdi-clock-end"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<div class="d-flex flex-wrap gap-4 mt-4">
|
||||
<VBtn type="submit" @click="saveNotificationTime"> {{ t('common.save') }} </VBtn>
|
||||
<VBtn type="submit" @click="saveNotificationTime" prepend-icon="mdi-content-save">
|
||||
{{ t('common.save') }}
|
||||
</VBtn>
|
||||
</div>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
@@ -438,13 +444,18 @@ onMounted(() => {
|
||||
:indeterminate="true"
|
||||
/>
|
||||
<!-- 模板编辑器对话框 -->
|
||||
<VDialog v-model="editorVisible" v-if="editorVisible" max-width="50rem">
|
||||
<VDialog v-model="editorVisible" v-if="editorVisible" max-width="50rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardItem class="py-2">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-code-json" class="me-2" />
|
||||
</template>
|
||||
<VCardTitle>
|
||||
{{ templateTypes.find(t => t.type === currentTemplate)?.label }}
|
||||
{{ t('setting.notification.templateConfigTitle') }}
|
||||
</VCardTitle>
|
||||
<VCardSubtitle>
|
||||
{{ templateTypes.find(t => t.type === currentTemplate)?.label }}
|
||||
</VCardSubtitle>
|
||||
<VDialogCloseBtn @click="editorVisible = false" />
|
||||
</VCardItem>
|
||||
<VCardText class="py-0">
|
||||
@@ -452,11 +463,11 @@ onMounted(() => {
|
||||
v-model:value="editorContent"
|
||||
lang="json"
|
||||
:theme="editorTheme"
|
||||
class="w-full min-h-[30rem] rounded"
|
||||
class="w-full h-full min-h-[30rem] rounded"
|
||||
/>
|
||||
</VCardText>
|
||||
<VCardActions class="mx-auto pt-3">
|
||||
<VBtn variant="elevated" color="primary" @click="saveTemplate" prepend-icon="mdi-content-save" class="px-5">
|
||||
<VCardActions class="pt-3">
|
||||
<VBtn color="primary" @click="saveTemplate" prepend-icon="mdi-content-save" class="px-5">
|
||||
{{ t('common.save') }}
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
@@ -466,10 +477,10 @@ onMounted(() => {
|
||||
<style scoped>
|
||||
/* Monaco编辑器容器样式 */
|
||||
.monaco-editor-container {
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
margin-top: 1rem;
|
||||
margin-block-start: 1rem;
|
||||
}
|
||||
|
||||
.template-card {
|
||||
|
||||
@@ -401,7 +401,9 @@ onMounted(() => {
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<div class="d-flex flex-wrap gap-4 mt-4">
|
||||
<VBtn type="submit" class="me-2" @click="saveCustomRules"> {{ t('common.save') }} </VBtn>
|
||||
<VBtn type="submit" class="me-2" @click="saveCustomRules" prepend-icon="mdi-content-save">
|
||||
{{ t('common.save') }}
|
||||
</VBtn>
|
||||
<VBtnGroup density="comfortable">
|
||||
<VBtn color="success" variant="tonal" @click="addCustomRule">
|
||||
<VIcon icon="mdi-plus" />
|
||||
@@ -452,7 +454,9 @@ onMounted(() => {
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<div class="d-flex flex-wrap gap-4 mt-4">
|
||||
<VBtn type="submit" class="me-2" @click="saveFilterRuleGroups"> {{ t('common.save') }} </VBtn>
|
||||
<VBtn type="submit" class="me-2" @click="saveFilterRuleGroups" prepend-icon="mdi-content-save">
|
||||
{{ t('common.save') }}
|
||||
</VBtn>
|
||||
<VBtnGroup density="comfortable">
|
||||
<VBtn color="success" variant="tonal" @click="addFilterRuleGroup">
|
||||
<VIcon icon="mdi-plus" />
|
||||
@@ -501,6 +505,7 @@ onMounted(() => {
|
||||
:label="t('setting.rule.currentPriorityRules')"
|
||||
:hint="t('setting.rule.currentPriorityRulesHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-priority-high"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -509,7 +514,9 @@ onMounted(() => {
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<div class="d-flex flex-wrap gap-4 mt-4">
|
||||
<VBtn type="submit" @click="saveTorrentPriority"> {{ t('common.save') }} </VBtn>
|
||||
<VBtn type="submit" @click="saveTorrentPriority" prepend-icon="mdi-content-save">
|
||||
{{ t('common.save') }}
|
||||
</VBtn>
|
||||
</div>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
|
||||
@@ -205,10 +205,11 @@ onMounted(() => {
|
||||
:label="t('setting.search.mediaSource')"
|
||||
:hint="t('setting.search.mediaSourceHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-database-search"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSelect
|
||||
<VAutocomplete
|
||||
v-model="selectedFilterGroup"
|
||||
multiple
|
||||
clearable
|
||||
@@ -217,6 +218,7 @@ onMounted(() => {
|
||||
:label="t('setting.search.filterRuleGroup')"
|
||||
:hint="t('setting.search.filterRuleGroupHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-filter"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -228,6 +230,7 @@ onMounted(() => {
|
||||
placeholder="MOVIEPILOT"
|
||||
:hint="t('setting.search.downloadLabelHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-tag"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -237,6 +240,7 @@ onMounted(() => {
|
||||
:placeholder="t('setting.search.downloadUserPlaceholder')"
|
||||
:hint="t('setting.search.downloadUserHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-account"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -260,7 +264,9 @@ onMounted(() => {
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<div class="d-flex flex-wrap gap-4 mt-4">
|
||||
<VBtn type="submit" @click="saveSearchSetting"> {{ t('common.save') }} </VBtn>
|
||||
<VBtn type="submit" @click="saveSearchSetting" prepend-icon="mdi-content-save">
|
||||
{{ t('common.save') }}
|
||||
</VBtn>
|
||||
</div>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
@@ -291,7 +297,9 @@ onMounted(() => {
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<div class="d-flex flex-wrap gap-4 mt-4">
|
||||
<VBtn type="submit" @click="saveSelectedSites"> {{ t('common.save') }} </VBtn>
|
||||
<VBtn type="submit" @click="saveSelectedSites" prepend-icon="mdi-content-save">
|
||||
{{ t('common.save') }}
|
||||
</VBtn>
|
||||
</div>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
|
||||
@@ -96,27 +96,12 @@ async function loadSiteSettings() {
|
||||
}
|
||||
}
|
||||
|
||||
// 重载系统生效配置
|
||||
async function reloadSystem() {
|
||||
progressDialog.value = true
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/reload')
|
||||
if (result.success) $toast.success(t('setting.system.reloadSuccess'))
|
||||
else $toast.error(t('setting.system.reloadFailed'))
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
progressDialog.value = false
|
||||
}
|
||||
|
||||
// 调用API保存设置
|
||||
async function saveSiteSetting(value: { [key: string]: any }) {
|
||||
console.log(`正在保存设置:${JSON.stringify(value)}`)
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post('system/env', value)
|
||||
if (result.success) {
|
||||
$toast.success(t('setting.site.saveSuccess'))
|
||||
await reloadSystem()
|
||||
} else {
|
||||
$toast.error(t('setting.site.saveFailed'))
|
||||
}
|
||||
@@ -161,6 +146,7 @@ onMounted(() => {
|
||||
:disabled="siteSetting.CookieCloud.COOKIECLOUD_ENABLE_LOCAL"
|
||||
:hint="t('setting.site.serviceAddressHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-server"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -169,6 +155,7 @@ onMounted(() => {
|
||||
:label="t('setting.site.userKey')"
|
||||
:hint="t('setting.site.userKeyHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-key"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -180,6 +167,7 @@ onMounted(() => {
|
||||
:label="t('setting.site.e2ePassword')"
|
||||
:hint="t('setting.site.e2ePasswordHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-lock"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -189,6 +177,7 @@ onMounted(() => {
|
||||
:items="CookieCloudIntervalItems"
|
||||
:hint="t('setting.site.autoSyncIntervalHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-timer"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -198,6 +187,7 @@ onMounted(() => {
|
||||
:placeholder="t('setting.site.syncBlacklistPlaceholder')"
|
||||
:hint="t('setting.site.syncBlacklistHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-block-helper"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -206,6 +196,7 @@ onMounted(() => {
|
||||
:label="t('setting.site.userAgent')"
|
||||
:hint="t('setting.site.userAgentHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-web"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -214,7 +205,9 @@ onMounted(() => {
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<div class="d-flex flex-wrap gap-4 mt-4">
|
||||
<VBtn type="submit" @click="saveSiteSetting(siteSetting.CookieCloud)"> {{ t('common.save') }} </VBtn>
|
||||
<VBtn type="submit" @click="saveSiteSetting(siteSetting.CookieCloud)" prepend-icon="mdi-content-save">
|
||||
{{ t('common.save') }}
|
||||
</VBtn>
|
||||
</div>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
@@ -234,6 +227,7 @@ onMounted(() => {
|
||||
:items="SiteDataRefreshIntervalItems"
|
||||
:hint="t('setting.site.siteDataRefreshIntervalHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-refresh"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -252,7 +246,9 @@ onMounted(() => {
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<div class="d-flex flex-wrap gap-4 mt-4">
|
||||
<VBtn type="submit" @click="saveSiteSetting(siteSetting.Site)"> {{ t('common.save') }} </VBtn>
|
||||
<VBtn type="submit" @click="saveSiteSetting(siteSetting.Site)" prepend-icon="mdi-content-save">
|
||||
{{ t('common.save') }}
|
||||
</VBtn>
|
||||
</div>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
|
||||
@@ -153,19 +153,6 @@ async function querySubscribeRules() {
|
||||
}
|
||||
}
|
||||
|
||||
// 重载系统生效配置
|
||||
async function reloadSystem() {
|
||||
progressDialog.value = true
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/reload')
|
||||
if (result.success) $toast.success(t('setting.system.reloadSuccess'))
|
||||
else $toast.error(t('setting.system.reloadFailed'))
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
progressDialog.value = false
|
||||
}
|
||||
|
||||
// 保存订阅设置
|
||||
async function saveSubscribeSetting() {
|
||||
try {
|
||||
@@ -183,7 +170,6 @@ async function saveSubscribeSetting() {
|
||||
|
||||
if (result1.success && result2.success && result3) {
|
||||
$toast.success(t('setting.subscribe.settingsSaveSuccess'))
|
||||
await reloadSystem()
|
||||
} else $toast.error(t('setting.subscribe.settingsSaveFailed'))
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
@@ -217,6 +203,7 @@ onMounted(() => {
|
||||
:label="t('setting.subscribe.mode')"
|
||||
:hint="t('setting.subscribe.modeHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-cog"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -226,10 +213,11 @@ onMounted(() => {
|
||||
:label="t('setting.subscribe.rssInterval')"
|
||||
:hint="t('setting.subscribe.rssIntervalHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-timer"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSelect
|
||||
<VAutocomplete
|
||||
v-model="selectedFilterRuleGroup"
|
||||
:items="filterRuleGroupOptions"
|
||||
chips
|
||||
@@ -238,10 +226,11 @@ onMounted(() => {
|
||||
:label="t('setting.subscribe.filterRuleGroup')"
|
||||
:hint="t('setting.subscribe.filterRuleGroupHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-filter"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSelect
|
||||
<VAutocomplete
|
||||
v-model="selectedBestVersionRuleGroup"
|
||||
:items="filterRuleGroupOptions"
|
||||
chips
|
||||
@@ -250,6 +239,7 @@ onMounted(() => {
|
||||
:label="t('setting.subscribe.bestVersionRuleGroup')"
|
||||
:hint="t('setting.subscribe.bestVersionRuleGroupHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-star"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -276,7 +266,9 @@ onMounted(() => {
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<div class="d-flex flex-wrap gap-4 mt-4">
|
||||
<VBtn type="submit" @click="saveSubscribeSetting"> {{ t('common.save') }} </VBtn>
|
||||
<VBtn type="submit" @click="saveSubscribeSetting" prepend-icon="mdi-content-save">
|
||||
{{ t('common.save') }}
|
||||
</VBtn>
|
||||
</div>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
@@ -307,7 +299,9 @@ onMounted(() => {
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<div class="d-flex flex-wrap gap-4 mt-4">
|
||||
<VBtn type="submit" @click="saveSelectedRssSites"> {{ t('common.save') }} </VBtn>
|
||||
<VBtn type="submit" @click="saveSelectedRssSites" prepend-icon="mdi-content-save">
|
||||
{{ t('common.save') }}
|
||||
</VBtn>
|
||||
</div>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
|
||||
@@ -11,6 +11,9 @@ import { copyToClipboard } from '@/@core/utils/navigator'
|
||||
import ProgressDialog from '@/components/dialog/ProgressDialog.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { downloaderOptions, mediaServerOptions } from '@/api/constants'
|
||||
import { useDisplay } from 'vuetify'
|
||||
|
||||
const display = useDisplay()
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
@@ -26,6 +29,7 @@ const SystemSettings = ref<any>({
|
||||
RECOGNIZE_SOURCE: 'themoviedb',
|
||||
GITHUB_TOKEN: null,
|
||||
OCR_HOST: null,
|
||||
CUSTOMIZE_WALLPAPER_API_URL: null,
|
||||
},
|
||||
// 高级系统设置
|
||||
Advanced: {
|
||||
@@ -36,6 +40,8 @@ const SystemSettings = ref<any>({
|
||||
PLUGIN_STATISTIC_SHARE: true,
|
||||
BIG_MEMORY_MODE: false,
|
||||
DB_WAL_ENABLE: false,
|
||||
AUTO_UPDATE_RESOURCE: true,
|
||||
MOVIEPILOT_AUTO_UPDATE: false,
|
||||
// 媒体
|
||||
TMDB_API_DOMAIN: null,
|
||||
TMDB_IMAGE_DOMAIN: null,
|
||||
@@ -43,6 +49,7 @@ const SystemSettings = ref<any>({
|
||||
META_CACHE_EXPIRE: 0,
|
||||
SCRAP_FOLLOW_TMDB: true,
|
||||
FANART_ENABLE: false,
|
||||
TMDB_SCRAP_ORIGINAL_IMAGE: null,
|
||||
// 网络
|
||||
PROXY_HOST: null,
|
||||
GITHUB_PROXY: null,
|
||||
@@ -50,6 +57,7 @@ const SystemSettings = ref<any>({
|
||||
DOH_ENABLE: false,
|
||||
DOH_RESOLVERS: null,
|
||||
DOH_DOMAINS: null,
|
||||
SECURITY_IMAGE_DOMAINS: [],
|
||||
// 日志
|
||||
DEBUG: false,
|
||||
LOG_LEVEL: 'INFO',
|
||||
@@ -90,6 +98,29 @@ const tmdbLanguageItems = [
|
||||
{ title: t('setting.system.tmdbLanguage.en'), value: 'en' },
|
||||
]
|
||||
|
||||
// 日志等级
|
||||
const logLevelItems = [
|
||||
{ title: t('setting.system.logLevelItems.debug'), value: 'DEBUG' },
|
||||
{ title: t('setting.system.logLevelItems.info'), value: 'INFO' },
|
||||
{ title: t('setting.system.logLevelItems.warning'), value: 'WARNING' },
|
||||
{ title: t('setting.system.logLevelItems.error'), value: 'ERROR' },
|
||||
{ title: t('setting.system.logLevelItems.critical'), value: 'CRITICAL' },
|
||||
]
|
||||
|
||||
// 安全域名添加变量
|
||||
const newSecurityDomain = ref('')
|
||||
|
||||
// 添加安全域名
|
||||
function addSecurityDomain() {
|
||||
if (
|
||||
newSecurityDomain.value &&
|
||||
!SystemSettings.value.Advanced.SECURITY_IMAGE_DOMAINS.includes(newSecurityDomain.value)
|
||||
) {
|
||||
SystemSettings.value.Advanced.SECURITY_IMAGE_DOMAINS.push(newSecurityDomain.value)
|
||||
newSecurityDomain.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
// 调用API查询下载器设置
|
||||
async function loadDownloaderSetting() {
|
||||
try {
|
||||
@@ -100,19 +131,6 @@ async function loadDownloaderSetting() {
|
||||
}
|
||||
}
|
||||
|
||||
// 重载系统生效配置
|
||||
async function reloadSystem() {
|
||||
progressDialog.value = true
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/reload')
|
||||
if (result.success) $toast.success(t('setting.system.reloadSuccess'))
|
||||
else $toast.error(t('setting.system.reloadFailed'))
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
progressDialog.value = false
|
||||
}
|
||||
|
||||
// 调用API保存下载器设置
|
||||
async function saveDownloaderSetting() {
|
||||
try {
|
||||
@@ -127,7 +145,6 @@ async function saveDownloaderSetting() {
|
||||
else $toast.error(t('setting.system.downloaderSaveFailed'))
|
||||
|
||||
await loadDownloaderSetting()
|
||||
await reloadSystem()
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
@@ -167,7 +184,6 @@ async function saveMediaServerSetting() {
|
||||
else $toast.error(t('setting.system.mediaServerSaveFailed'))
|
||||
|
||||
await loadMediaServerSetting()
|
||||
await reloadSystem()
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
@@ -210,7 +226,6 @@ async function saveSystemSetting(value: { [key: string]: any }) {
|
||||
async function saveBasicSettings() {
|
||||
if (await saveSystemSetting(SystemSettings.value.Basic)) {
|
||||
$toast.success(t('setting.system.basicSaveSuccess'))
|
||||
await reloadSystem()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,7 +236,6 @@ async function saveAdvancedSettings() {
|
||||
if (await saveSystemSetting(SystemSettings.value.Advanced)) {
|
||||
advancedDialog.value = false
|
||||
$toast.success(t('setting.system.advancedSaveSuccess'))
|
||||
await reloadSystem()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,6 +266,7 @@ const wallpaperItems = [
|
||||
{ title: t('setting.system.wallpaperItems.tmdb'), value: 'tmdb' },
|
||||
{ title: t('setting.system.wallpaperItems.bing'), value: 'bing' },
|
||||
{ title: t('setting.system.wallpaperItems.mediaserver'), value: 'mediaserver' },
|
||||
{ title: t('setting.system.wallpaperItems.customize'), value: 'customize' },
|
||||
{ title: t('setting.system.wallpaperItems.none'), value: '' },
|
||||
]
|
||||
|
||||
@@ -294,15 +309,6 @@ const pipProxyDisplay = computed({
|
||||
},
|
||||
})
|
||||
|
||||
// 日志等级
|
||||
const logLevelItems = [
|
||||
{ title: t('setting.system.logLevelItems.debug'), value: 'DEBUG' },
|
||||
{ title: t('setting.system.logLevelItems.info'), value: 'INFO' },
|
||||
{ title: t('setting.system.logLevelItems.warning'), value: 'WARNING' },
|
||||
{ title: t('setting.system.logLevelItems.error'), value: 'ERROR' },
|
||||
{ title: t('setting.system.logLevelItems.critical'), value: 'CRITICAL' },
|
||||
]
|
||||
|
||||
// 创建随机字符串
|
||||
function createRandomString() {
|
||||
const charset = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_'
|
||||
@@ -364,6 +370,16 @@ function onMediaServerChange(mediaserver: MediaServerConf, name: string) {
|
||||
if (index !== -1) mediaServers.value[index] = mediaserver
|
||||
}
|
||||
|
||||
// 添加计算属性
|
||||
const moviePilotAutoUpdate = computed({
|
||||
get: () => {
|
||||
return ['release', 'dev'].includes(SystemSettings.value.Advanced.MOVIEPILOT_AUTO_UPDATE)
|
||||
},
|
||||
set: val => {
|
||||
SystemSettings.value.Advanced.MOVIEPILOT_AUTO_UPDATE = val ? 'release' : 'false'
|
||||
},
|
||||
})
|
||||
|
||||
// 加载数据
|
||||
onMounted(() => {
|
||||
loadDownloaderSetting()
|
||||
@@ -405,16 +421,35 @@ onDeactivated(() => {
|
||||
:hint="t('setting.system.appDomainHint')"
|
||||
placeholder="http://localhost:3000"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-web"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12" md="6">
|
||||
<VSelect
|
||||
v-model="SystemSettings.Basic.WALLPAPER"
|
||||
:label="t('setting.system.wallpaper')"
|
||||
:hint="t('setting.system.wallpaperHint')"
|
||||
persistent-hint
|
||||
:items="wallpaperItems"
|
||||
/>
|
||||
<VRow>
|
||||
<VCol cols="12" :md="SystemSettings.Basic.WALLPAPER === 'customize' ? 6 : 12">
|
||||
<VSelect
|
||||
v-model="SystemSettings.Basic.WALLPAPER"
|
||||
:label="t('setting.system.wallpaper')"
|
||||
:hint="t('setting.system.wallpaperHint')"
|
||||
persistent-hint
|
||||
:items="wallpaperItems"
|
||||
prepend-inner-icon="mdi-image"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol v-if="SystemSettings.Basic.WALLPAPER === 'customize'" cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="SystemSettings.Basic.CUSTOMIZE_WALLPAPER_API_URL"
|
||||
:label="t('setting.system.customizeWallpaperApi')"
|
||||
:hint="t('setting.system.customizeWallpaperApiHint')"
|
||||
:placeholder="t('setting.system.customizeWallpaperApi')"
|
||||
persistent-hint
|
||||
:rules="[v => !!v || t('setting.system.customizeWallpaperApiRequired')]"
|
||||
prepend-inner-icon="mdi-api"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSelect
|
||||
@@ -426,6 +461,7 @@ onDeactivated(() => {
|
||||
{ title: 'TheMovieDb', value: 'themoviedb' },
|
||||
{ title: '豆瓣', value: 'douban' },
|
||||
]"
|
||||
prepend-inner-icon="mdi-database"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -442,6 +478,7 @@ onDeactivated(() => {
|
||||
(v: any) => !isNaN(v) || t('setting.system.numbersOnly'),
|
||||
(v: any) => v >= 1 || t('setting.system.minInterval'),
|
||||
]"
|
||||
prepend-inner-icon="mdi-sync"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -451,10 +488,11 @@ onDeactivated(() => {
|
||||
:hint="t('setting.system.apiTokenHint')"
|
||||
:placeholder="t('setting.system.apiTokenMinChars')"
|
||||
persistent-hint
|
||||
prependInnerIcon="mdi-reload"
|
||||
:appendInnerIcon="SystemSettings.Basic.API_TOKEN ? 'mdi-content-copy' : ''"
|
||||
@click:prependInner="createRandomString"
|
||||
@click:appendInner="copyValue(SystemSettings.Basic.API_TOKEN)"
|
||||
prepend-inner-icon="mdi-key"
|
||||
:append-inner-icon="SystemSettings.Basic.API_TOKEN ? 'mdi-content-copy' : 'mdi-reload'"
|
||||
@click:append-inner="
|
||||
SystemSettings.Basic.API_TOKEN ? copyValue(SystemSettings.Basic.API_TOKEN) : createRandomString()
|
||||
"
|
||||
:rules="[
|
||||
(v: string) => !!v || t('setting.system.apiTokenRequired'),
|
||||
(v: string) => v.length >= 16 || t('setting.system.apiTokenLength'),
|
||||
@@ -468,6 +506,7 @@ onDeactivated(() => {
|
||||
:placeholder="t('setting.system.githubTokenFormat')"
|
||||
:hint="t('setting.system.githubTokenHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-github"
|
||||
>
|
||||
</VTextField>
|
||||
</VCol>
|
||||
@@ -478,6 +517,7 @@ onDeactivated(() => {
|
||||
placeholder="https://movie-pilot.org"
|
||||
:hint="t('setting.system.ocrHostHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-text-recognition"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -486,7 +526,9 @@ onDeactivated(() => {
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<div class="d-flex flex-wrap gap-4 mt-4">
|
||||
<VBtn type="submit" @click="saveBasicSettings"> {{ t('common.save') }} </VBtn>
|
||||
<VBtn type="submit" @click="saveBasicSettings" prepend-icon="mdi-content-save">
|
||||
{{ t('common.save') }}
|
||||
</VBtn>
|
||||
<VSpacer />
|
||||
<VBtn
|
||||
color="error"
|
||||
@@ -531,7 +573,9 @@ onDeactivated(() => {
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<div class="d-flex flex-wrap gap-4 mt-4">
|
||||
<VBtn type="submit" @click="saveDownloaderSetting"> {{ t('common.save') }} </VBtn>
|
||||
<VBtn type="submit" @click="saveDownloaderSetting" prepend-icon="mdi-content-save">
|
||||
{{ t('common.save') }}
|
||||
</VBtn>
|
||||
<VBtn color="success" variant="tonal">
|
||||
<VIcon icon="mdi-plus" />
|
||||
<VMenu activator="parent" close-on-content-click>
|
||||
@@ -579,7 +623,9 @@ onDeactivated(() => {
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<div class="d-flex flex-wrap gap-4 mt-4">
|
||||
<VBtn type="submit" @click="saveMediaServerSetting"> {{ t('common.save') }} </VBtn>
|
||||
<VBtn type="submit" @click="saveMediaServerSetting" prepend-icon="mdi-content-save">
|
||||
{{ t('common.save') }}
|
||||
</VBtn>
|
||||
<VBtn color="success" variant="tonal">
|
||||
<VIcon icon="mdi-plus" />
|
||||
<VMenu activator="parent" close-on-content-click>
|
||||
@@ -599,8 +645,15 @@ onDeactivated(() => {
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
|
||||
<!-- 高级系统设置 -->
|
||||
<VDialog v-if="advancedDialog" v-model="advancedDialog" scrollable max-width="60rem">
|
||||
<VDialog
|
||||
v-if="advancedDialog"
|
||||
v-model="advancedDialog"
|
||||
scrollable
|
||||
max-width="60rem"
|
||||
:fullscreen="!display.mdAndUp.value"
|
||||
>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VDialogCloseBtn @click="advancedDialog = false" />
|
||||
@@ -677,6 +730,22 @@ onDeactivated(() => {
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="moviePilotAutoUpdate"
|
||||
:label="t('setting.system.moviePilotAutoUpdate')"
|
||||
:hint="t('setting.system.moviePilotAutoUpdateHint')"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="SystemSettings.Advanced.AUTO_UPDATE_RESOURCE"
|
||||
:label="t('setting.system.autoUpdateResource')"
|
||||
:hint="t('setting.system.autoUpdateResourceHint')"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</div>
|
||||
</VWindowItem>
|
||||
@@ -692,6 +761,7 @@ onDeactivated(() => {
|
||||
persistent-hint
|
||||
:items="['api.themoviedb.org', 'api.tmdb.org']"
|
||||
:rules="[(v: string) => !!v || t('setting.system.tmdbApiDomainRequired')]"
|
||||
prepend-inner-icon="mdi-api"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -703,6 +773,7 @@ onDeactivated(() => {
|
||||
persistent-hint
|
||||
:items="['image.tmdb.org', 'static-mdb.v.geilijiasu.com']"
|
||||
:rules="[(v: string) => !!v || t('setting.system.tmdbImageDomainRequired')]"
|
||||
prepend-inner-icon="mdi-image"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -713,6 +784,7 @@ onDeactivated(() => {
|
||||
:hint="t('setting.system.tmdbLocaleHint')"
|
||||
persistent-hint
|
||||
:items="tmdbLanguageItems"
|
||||
prepend-inner-icon="mdi-translate"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -728,6 +800,7 @@ onDeactivated(() => {
|
||||
(v: any) => v === 0 || !!v || t('setting.system.metaCacheExpireRequired'),
|
||||
(v: any) => v >= 0 || t('setting.system.metaCacheExpireMin'),
|
||||
]"
|
||||
prepend-inner-icon="mdi-timer"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -740,6 +813,14 @@ onDeactivated(() => {
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="SystemSettings.Advanced.TMDB_SCRAP_ORIGINAL_IMAGE"
|
||||
:label="t('setting.system.scrapOriginalImage')"
|
||||
:hint="t('setting.system.scrapOriginalImageHint')"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="SystemSettings.Advanced.FANART_ENABLE"
|
||||
@@ -754,6 +835,16 @@ onDeactivated(() => {
|
||||
<VWindowItem value="network">
|
||||
<div>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="SystemSettings.Advanced.PROXY_HOST"
|
||||
:label="t('setting.system.proxyHost')"
|
||||
placeholder="http://127.0.0.1:7890"
|
||||
:hint="t('setting.system.proxyHostHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-server-network"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VCombobox
|
||||
v-model="githubProxyDisplay"
|
||||
@@ -763,9 +854,10 @@ onDeactivated(() => {
|
||||
persistent-hint
|
||||
:items="githubMirrorsItems"
|
||||
clearable
|
||||
prepend-inner-icon="mdi-github"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VCol cols="12">
|
||||
<VCombobox
|
||||
v-model="pipProxyDisplay"
|
||||
:label="t('setting.system.pipProxy')"
|
||||
@@ -774,6 +866,7 @@ onDeactivated(() => {
|
||||
persistent-hint
|
||||
:items="pipMirrorsItems"
|
||||
clearable
|
||||
prepend-inner-icon="mdi-package"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -793,6 +886,7 @@ onDeactivated(() => {
|
||||
:placeholder="t('setting.system.dohResolversPlaceholder')"
|
||||
:hint="t('setting.system.dohResolversHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-dns"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" v-show="SystemSettings.Advanced.DOH_ENABLE">
|
||||
@@ -802,9 +896,51 @@ onDeactivated(() => {
|
||||
:placeholder="t('setting.system.dohDomainsPlaceholder')"
|
||||
:hint="t('setting.system.dohDomainsHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-domain"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<!-- 安全域名 -->
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>{{ t('setting.system.securityImageDomains') }}</VCardTitle>
|
||||
<VCardSubtitle>{{ t('setting.system.securityImageDomainsHint') }}</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<div class="d-flex flex-wrap gap-2 mb-3">
|
||||
<VChip
|
||||
v-for="(domain, index) in SystemSettings.Advanced.SECURITY_IMAGE_DOMAINS"
|
||||
:key="index"
|
||||
closable
|
||||
@click:close="SystemSettings.Advanced.SECURITY_IMAGE_DOMAINS.splice(index, 1)"
|
||||
>
|
||||
{{ domain }}
|
||||
</VChip>
|
||||
<VChip v-if="SystemSettings.Advanced.SECURITY_IMAGE_DOMAINS.length === 0" color="warning">
|
||||
{{ t('setting.system.noSecurityImageDomains') }}
|
||||
</VChip>
|
||||
</div>
|
||||
<div class="d-flex align-center gap-2">
|
||||
<VTextField
|
||||
v-model="newSecurityDomain"
|
||||
:placeholder="t('setting.system.securityImageDomainAdd')"
|
||||
hide-details
|
||||
density="compact"
|
||||
prepend-inner-icon="mdi-shield-check"
|
||||
>
|
||||
<template #append>
|
||||
<VBtn icon color="primary" @click="addSecurityDomain" :disabled="!newSecurityDomain">
|
||||
<VIcon icon="mdi-plus" />
|
||||
</VBtn>
|
||||
</template>
|
||||
</VTextField>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</div>
|
||||
</VWindowItem>
|
||||
<VWindowItem value="log">
|
||||
@@ -826,6 +962,7 @@ onDeactivated(() => {
|
||||
:hint="t('setting.system.logLevelHint')"
|
||||
persistent-hint
|
||||
:items="logLevelItems"
|
||||
prepend-inner-icon="mdi-format-list-bulleted"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -838,6 +975,7 @@ onDeactivated(() => {
|
||||
type="number"
|
||||
:suffix="t('setting.system.mb')"
|
||||
:rules="[(v: any) => v === 0 || !!v || t('setting.system.logMaxFileSizeRequired'), (v: any) => v >= 1 || t('setting.system.logMaxFileSizeMin')]"
|
||||
prepend-inner-icon="mdi-file-document"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -849,6 +987,7 @@ onDeactivated(() => {
|
||||
min="1"
|
||||
type="number"
|
||||
:rules="[(v: any) => v === 0 || !!v || t('setting.system.logBackupCountRequired'), (v: any) => v >= 1 || t('setting.system.logBackupCountMin')]"
|
||||
prepend-inner-icon="mdi-backup-restore"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
@@ -857,6 +996,7 @@ onDeactivated(() => {
|
||||
:label="t('setting.system.logFileFormat')"
|
||||
:hint="t('setting.system.logFileFormatHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-format-text"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -897,13 +1037,7 @@ onDeactivated(() => {
|
||||
<VCardActions class="pt-3">
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<div class="d-flex flex-wrap gap-4 mt-4">
|
||||
<VBtn
|
||||
color="primary"
|
||||
variant="elevated"
|
||||
prepend-icon="mdi-content-save"
|
||||
@click="saveAdvancedSettings"
|
||||
class="px-5"
|
||||
>
|
||||
<VBtn color="primary" prepend-icon="mdi-content-save" @click="saveAdvancedSettings" class="px-5">
|
||||
{{ t('common.save') }}
|
||||
</VBtn>
|
||||
</div>
|
||||
|
||||
@@ -143,6 +143,7 @@ onMounted(() => {
|
||||
:placeholder="t('setting.words.identifiersPlaceholder')"
|
||||
:hint="t('setting.words.identifiersHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-tag-text"
|
||||
/>
|
||||
</VCardText>
|
||||
<VCardText>
|
||||
@@ -153,7 +154,9 @@ onMounted(() => {
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<div class="d-flex flex-wrap gap-4 mt-4">
|
||||
<VBtn type="submit" @click="saveCustomIdentifiers">{{ t('common.save') }}</VBtn>
|
||||
<VBtn type="submit" @click="saveCustomIdentifiers" prepend-icon="mdi-content-save">
|
||||
{{ t('common.save') }}
|
||||
</VBtn>
|
||||
</div>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
@@ -173,12 +176,15 @@ onMounted(() => {
|
||||
:placeholder="t('setting.words.releaseGroupsPlaceholder')"
|
||||
:hint="t('setting.words.releaseGroupsHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-account-group"
|
||||
/>
|
||||
</VCardText>
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<div class="d-flex flex-wrap gap-4 mt-4">
|
||||
<VBtn type="submit" @click="saveCustomReleaseGroups">{{ t('common.save') }}</VBtn>
|
||||
<VBtn type="submit" @click="saveCustomReleaseGroups" prepend-icon="mdi-content-save">
|
||||
{{ t('common.save') }}
|
||||
</VBtn>
|
||||
</div>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
@@ -198,12 +204,15 @@ onMounted(() => {
|
||||
:placeholder="t('setting.words.customizationPlaceholder')"
|
||||
:hint="t('setting.words.customizationHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-code-braces"
|
||||
/>
|
||||
</VCardText>
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<div class="d-flex flex-wrap gap-4 mt-4">
|
||||
<VBtn type="submit" @click="saveCustomization">{{ t('common.save') }}</VBtn>
|
||||
<VBtn type="submit" @click="saveCustomization" prepend-icon="mdi-content-save">
|
||||
{{ t('common.save') }}
|
||||
</VBtn>
|
||||
</div>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
@@ -223,12 +232,15 @@ onMounted(() => {
|
||||
:placeholder="t('setting.words.excludeWordsPlaceholder')"
|
||||
:hint="t('setting.words.excludeWordsHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-block-helper"
|
||||
/>
|
||||
</VCardText>
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<div class="d-flex flex-wrap gap-4 mt-4">
|
||||
<VBtn type="submit" @click="saveTransferExcludeWords">{{ t('common.save') }}</VBtn>
|
||||
<VBtn type="submit" @click="saveTransferExcludeWords" prepend-icon="mdi-content-save">
|
||||
{{ t('common.save') }}
|
||||
</VBtn>
|
||||
</div>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user