mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-05-11 10:00:08 +08:00
Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6fdbc8104c | ||
|
|
433c14679c | ||
|
|
fcaa4476f0 | ||
|
|
85c5c3058c | ||
|
|
035122a08e | ||
|
|
0a76875f8e | ||
|
|
218eac54ce | ||
|
|
84deeff4f5 | ||
|
|
0c72d026f6 | ||
|
|
aec9ea83c5 | ||
|
|
effd13aedd | ||
|
|
42b43d65d7 | ||
|
|
c501d824dd | ||
|
|
384ac2faf1 | ||
|
|
dd2c4dd24b | ||
|
|
356ffddb1c | ||
|
|
de69be7c4e | ||
|
|
e962f555ae | ||
|
|
1987246585 | ||
|
|
393264f66b | ||
|
|
9b50020b3b | ||
|
|
5e5545fe01 | ||
|
|
0e8da35b0a | ||
|
|
4d2cf73330 | ||
|
|
5df89f2ce4 | ||
|
|
045c0b4c0c | ||
|
|
8b4ffa0795 | ||
|
|
14359a37ae | ||
|
|
a8e4a1c2e0 | ||
|
|
9048d181af | ||
|
|
1cb02994bf | ||
|
|
6fad85e957 | ||
|
|
db9b2ee6b3 | ||
|
|
8efeb77102 | ||
|
|
0215a800e2 | ||
|
|
87d282f98b | ||
|
|
60c392d3d0 | ||
|
|
34c3aa25da | ||
|
|
80690d4cc8 | ||
|
|
18f3dc2d44 | ||
|
|
e8256b4e1a | ||
|
|
4f67bb0250 | ||
|
|
5dd071adf4 | ||
|
|
aaf5e7f49d | ||
|
|
6a5958409a | ||
|
|
e0ff98b1d7 | ||
|
|
a815e07cdd | ||
|
|
aa2fe9740c | ||
|
|
75a358a4d2 | ||
|
|
d5646be6f8 | ||
|
|
cb04ebcd95 | ||
|
|
9889ccfc74 | ||
|
|
f528bd861a |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "moviepilot",
|
"name": "moviepilot",
|
||||||
"version": "1.9.7",
|
"version": "1.9.17",
|
||||||
"private": true,
|
"private": true,
|
||||||
"bin": "dist/service.js",
|
"bin": "dist/service.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -101,4 +101,4 @@
|
|||||||
"resolutions": {
|
"resolutions": {
|
||||||
"postcss": "8"
|
"postcss": "8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,8 @@ import modeYamlUrl from 'ace-builds/src-noconflict/mode-yaml?url'
|
|||||||
|
|
||||||
import modeCssUrl from 'ace-builds/src-noconflict/mode-css?url'
|
import modeCssUrl from 'ace-builds/src-noconflict/mode-css?url'
|
||||||
|
|
||||||
|
import modePythonUrl from 'ace-builds/src-noconflict/mode-python?url'
|
||||||
|
|
||||||
import themeGithubUrl from 'ace-builds/src-noconflict/theme-github?url'
|
import themeGithubUrl from 'ace-builds/src-noconflict/theme-github?url'
|
||||||
|
|
||||||
import themeChromeUrl from 'ace-builds/src-noconflict/theme-chrome?url'
|
import themeChromeUrl from 'ace-builds/src-noconflict/theme-chrome?url'
|
||||||
@@ -38,6 +40,8 @@ import snippetsJsonUrl from 'ace-builds/src-noconflict/snippets/json?url'
|
|||||||
|
|
||||||
import snippertsCssUrl from 'ace-builds/src-noconflict/snippets/css?url'
|
import snippertsCssUrl from 'ace-builds/src-noconflict/snippets/css?url'
|
||||||
|
|
||||||
|
import snippetsPythonUrl from 'ace-builds/src-noconflict/snippets/python?url'
|
||||||
|
|
||||||
import 'ace-builds/src-noconflict/ext-language_tools'
|
import 'ace-builds/src-noconflict/ext-language_tools'
|
||||||
|
|
||||||
ace.config.setModuleUrl('ace/mode/json', modeJsonUrl)
|
ace.config.setModuleUrl('ace/mode/json', modeJsonUrl)
|
||||||
@@ -45,6 +49,7 @@ ace.config.setModuleUrl('ace/mode/javascript', modeJavascriptUrl)
|
|||||||
ace.config.setModuleUrl('ace/mode/html', modeHtmlUrl)
|
ace.config.setModuleUrl('ace/mode/html', modeHtmlUrl)
|
||||||
ace.config.setModuleUrl('ace/mode/yaml', modeYamlUrl)
|
ace.config.setModuleUrl('ace/mode/yaml', modeYamlUrl)
|
||||||
ace.config.setModuleUrl('ace/mode/css', modeCssUrl)
|
ace.config.setModuleUrl('ace/mode/css', modeCssUrl)
|
||||||
|
ace.config.setModuleUrl('ace/mode/python', modePythonUrl)
|
||||||
ace.config.setModuleUrl('ace/theme/github', themeGithubUrl)
|
ace.config.setModuleUrl('ace/theme/github', themeGithubUrl)
|
||||||
ace.config.setModuleUrl('ace/theme/chrome', themeChromeUrl)
|
ace.config.setModuleUrl('ace/theme/chrome', themeChromeUrl)
|
||||||
ace.config.setModuleUrl('ace/theme/monokai', themeMonokaiUrl)
|
ace.config.setModuleUrl('ace/theme/monokai', themeMonokaiUrl)
|
||||||
@@ -59,5 +64,6 @@ ace.config.setModuleUrl('ace/snippets/javascript', snippetsJsUrl)
|
|||||||
ace.config.setModuleUrl('ace/snippets/javascript', snippetsYamlUrl)
|
ace.config.setModuleUrl('ace/snippets/javascript', snippetsYamlUrl)
|
||||||
ace.config.setModuleUrl('ace/snippets/json', snippetsJsonUrl)
|
ace.config.setModuleUrl('ace/snippets/json', snippetsJsonUrl)
|
||||||
ace.config.setModuleUrl('ace/snippets/css', snippertsCssUrl)
|
ace.config.setModuleUrl('ace/snippets/css', snippertsCssUrl)
|
||||||
|
ace.config.setModuleUrl('ace/snippets/python', snippetsPythonUrl)
|
||||||
|
|
||||||
ace.require('ace/ext/language_tools')
|
ace.require('ace/ext/language_tools')
|
||||||
|
|||||||
@@ -27,13 +27,12 @@ api.interceptors.response.use(
|
|||||||
return Promise.reject(new Error(error))
|
return Promise.reject(new Error(error))
|
||||||
} else if (error.response.status === 403) {
|
} else if (error.response.status === 403) {
|
||||||
// 清除登录状态信息
|
// 清除登录状态信息
|
||||||
store.dispatch('auth/clearToken')
|
store.dispatch('auth/logout')
|
||||||
|
|
||||||
// token验证失败,跳转到登录页面
|
// token验证失败,跳转到登录页面
|
||||||
router.push('/login')
|
router.push('/login')
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.reject(new Error(error))
|
return Promise.reject(error)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -765,6 +765,8 @@ export interface FileItem {
|
|||||||
thumbnail?: string
|
thumbnail?: string
|
||||||
// pickcode
|
// pickcode
|
||||||
pickcode?: string
|
pickcode?: string
|
||||||
|
// drive_id
|
||||||
|
drive_id?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// 媒体服务器播放条目
|
// 媒体服务器播放条目
|
||||||
|
|||||||
@@ -11,10 +11,6 @@ import { isNullOrEmptyObject } from '@/@core/utils'
|
|||||||
// 输入参数
|
// 输入参数
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
storages: String,
|
storages: String,
|
||||||
path: String,
|
|
||||||
fileid: String,
|
|
||||||
pickcode: String,
|
|
||||||
fileidstack: Array as PropType<string[]>,
|
|
||||||
tree: Boolean,
|
tree: Boolean,
|
||||||
endpoints: Object as PropType<EndPoints>,
|
endpoints: Object as PropType<EndPoints>,
|
||||||
axios: {
|
axios: {
|
||||||
@@ -22,6 +18,11 @@ const props = defineProps({
|
|||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
axiosconfig: Object,
|
axiosconfig: Object,
|
||||||
|
item: {
|
||||||
|
type: Object as PropType<FileItem>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
itemstack: Array as PropType<FileItem[]>,
|
||||||
})
|
})
|
||||||
|
|
||||||
// 对外事件
|
// 对外事件
|
||||||
@@ -165,11 +166,10 @@ function u115AuthDone() {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<VCard class="mx-auto" :loading="loading > 0">
|
<VCard class="mx-auto" :loading="loading > 0">
|
||||||
<div v-if="activeStorage && (path || fileid)">
|
<div v-if="activeStorage && item">
|
||||||
<FileToolbar
|
<FileToolbar
|
||||||
:path="path"
|
:item="item"
|
||||||
:fileid="fileid"
|
:itemstack="itemstack"
|
||||||
:fileidstack="fileidstack"
|
|
||||||
:storages="storagesArray"
|
:storages="storagesArray"
|
||||||
:storage="activeStorage"
|
:storage="activeStorage"
|
||||||
:endpoints="endpoints"
|
:endpoints="endpoints"
|
||||||
@@ -180,9 +180,7 @@ function u115AuthDone() {
|
|||||||
@sortchanged="sortChanged"
|
@sortchanged="sortChanged"
|
||||||
/>
|
/>
|
||||||
<FileList
|
<FileList
|
||||||
:path="path"
|
:item="item"
|
||||||
:fileid="fileid"
|
|
||||||
:pickcode="pickcode"
|
|
||||||
:storage="activeStorage"
|
:storage="activeStorage"
|
||||||
:icons="fileIcons"
|
:icons="fileIcons"
|
||||||
:endpoints="endpoints"
|
:endpoints="endpoints"
|
||||||
|
|||||||
@@ -21,6 +21,14 @@ function filtersChanged(value: string[]) {
|
|||||||
emit('changed', props.pri, value)
|
emit('changed', props.pri, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 清洗规则中的换行符和多余空格,并在前后添加空格
|
||||||
|
const cleanedRules = computed(() => {
|
||||||
|
return props.rules.map(rule => {
|
||||||
|
rule = rule ?? ''
|
||||||
|
return ` ${rule.replace(/[\r\n]/g, '').replace(/\s+/g, '')} `
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
// 过滤规则下拉框
|
// 过滤规则下拉框
|
||||||
const selectFilterOptions = ref<{ [key: string]: string }[]>([
|
const selectFilterOptions = ref<{ [key: string]: string }[]>([
|
||||||
{ title: '特效字幕', value: ' SPECSUB ' },
|
{ title: '特效字幕', value: ' SPECSUB ' },
|
||||||
@@ -77,7 +85,7 @@ const selectFilterOptions = ref<{ [key: string]: string }[]>([
|
|||||||
<VRow>
|
<VRow>
|
||||||
<VCol>
|
<VCol>
|
||||||
<VSelect
|
<VSelect
|
||||||
v-model="props.rules"
|
v-model="cleanedRules"
|
||||||
variant="underlined"
|
variant="underlined"
|
||||||
:items="selectFilterOptions"
|
:items="selectFilterOptions"
|
||||||
chips
|
chips
|
||||||
|
|||||||
@@ -150,55 +150,61 @@ const dropdownItems = ref([
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<VCard :width="props.width" :height="props.height" @click="installPlugin" class="flex flex-col">
|
<VCard :width="props.width" :height="props.height" @click="installPlugin" class="flex flex-col">
|
||||||
<div class="relative pa-3 text-center card-cover-blurred" :style="{ background: `${backgroundColor}` }">
|
<div class="me-n3 absolute bottom-0 right-3">
|
||||||
<div class="me-n3 absolute top-0 right-3">
|
<IconBtn>
|
||||||
<IconBtn>
|
<VIcon icon="mdi-dots-vertical" />
|
||||||
<VIcon icon="mdi-dots-vertical" class="text-white" />
|
<VMenu activator="parent" close-on-content-click>
|
||||||
<VMenu activator="parent" close-on-content-click>
|
<VList>
|
||||||
<VList>
|
<VListItem
|
||||||
<VListItem
|
v-for="(item, i) in dropdownItems"
|
||||||
v-for="(item, i) in dropdownItems"
|
v-show="item.show"
|
||||||
v-show="item.show"
|
:key="i"
|
||||||
:key="i"
|
variant="plain"
|
||||||
variant="plain"
|
@click="item.props.click"
|
||||||
@click="item.props.click"
|
>
|
||||||
>
|
<template #prepend>
|
||||||
<template #prepend>
|
<VIcon :icon="item.props.prependIcon" />
|
||||||
<VIcon :icon="item.props.prependIcon" />
|
</template>
|
||||||
</template>
|
<VListItemTitle v-text="item.title" />
|
||||||
<VListItemTitle v-text="item.title" />
|
</VListItem>
|
||||||
</VListItem>
|
</VList>
|
||||||
</VList>
|
</VMenu>
|
||||||
</VMenu>
|
</IconBtn>
|
||||||
</IconBtn>
|
|
||||||
</div>
|
|
||||||
<VAvatar size="6rem">
|
|
||||||
<VImg
|
|
||||||
ref="imageRef"
|
|
||||||
:src="iconPath"
|
|
||||||
aspect-ratio="4/3"
|
|
||||||
cover
|
|
||||||
:class="{ shadow: isImageLoaded }"
|
|
||||||
@load="imageLoaded"
|
|
||||||
@error="imageLoadError = true"
|
|
||||||
/>
|
|
||||||
</VAvatar>
|
|
||||||
</div>
|
</div>
|
||||||
<VCardTitle>
|
<div
|
||||||
{{ props.plugin?.plugin_name }}
|
class="relative flex flex-row items-start pa-3 justify-between grow"
|
||||||
<span class="text-sm text-gray-500">v{{ props.plugin?.plugin_version }}</span>
|
:style="{ background: `${backgroundColor}` }"
|
||||||
</VCardTitle>
|
>
|
||||||
<VCardText class="pb-2">
|
<div
|
||||||
<div>{{ props.plugin?.plugin_desc }}</div>
|
class="absolute inset-0 bg-cover bg-center"
|
||||||
<div>
|
:style="{ background: `${backgroundColor}`, filter: 'brightness(0.7)' }"
|
||||||
<VChip v-for="label in pluginLabels" variant="tonal" size="small" class="me-1 my-1" color="info" label>
|
></div>
|
||||||
{{ label }}
|
<div class="relative flex-1 min-w-0">
|
||||||
</VChip>
|
<VCardTitle class="text-white px-2 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>
|
||||||
|
</VCardTitle>
|
||||||
|
<VCardText class="text-white px-2 py-1 text-shadow line-clamp-3">
|
||||||
|
{{ props.plugin?.plugin_desc }}
|
||||||
|
</VCardText>
|
||||||
</div>
|
</div>
|
||||||
</VCardText>
|
<div class="relative flex-shrink-0 self-center">
|
||||||
<VCardText class="flex align-self-baseline pb-2 w-full align-end">
|
<VAvatar size="64">
|
||||||
|
<VImg
|
||||||
|
ref="imageRef"
|
||||||
|
:src="iconPath"
|
||||||
|
aspect-ratio="4/3"
|
||||||
|
cover
|
||||||
|
:class="{ shadow: isImageLoaded }"
|
||||||
|
@load="imageLoaded"
|
||||||
|
@error="imageLoadError = true"
|
||||||
|
/>
|
||||||
|
</VAvatar>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<VCardText class="flex flex-none align-self-baseline py-3 w-full align-end">
|
||||||
<span>
|
<span>
|
||||||
<VIcon icon="mdi-account" class="me-1" />
|
<VIcon icon="mdi-github" class="me-1" />
|
||||||
<a :href="props.plugin?.author_url" target="_blank" @click.stop>
|
<a :href="props.plugin?.author_url" target="_blank" @click.stop>
|
||||||
{{ props.plugin?.plugin_author }}
|
{{ props.plugin?.plugin_author }}
|
||||||
</a>
|
</a>
|
||||||
@@ -220,15 +226,3 @@ const dropdownItems = ref([
|
|||||||
</VCard>
|
</VCard>
|
||||||
</VDialog>
|
</VDialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.card-cover-blurred::before {
|
|
||||||
position: absolute;
|
|
||||||
/* stylelint-disable-next-line property-no-vendor-prefix */
|
|
||||||
-webkit-backdrop-filter: blur(2px);
|
|
||||||
backdrop-filter: blur(2px);
|
|
||||||
background: rgba(29, 39, 59, 48%);
|
|
||||||
content: '';
|
|
||||||
inset: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -383,60 +383,75 @@ watch(
|
|||||||
<template>
|
<template>
|
||||||
<!-- 插件卡片 -->
|
<!-- 插件卡片 -->
|
||||||
<VCard v-if="isVisible" :width="props.width" :height="props.height" @click="openPluginDetail" class="flex flex-col">
|
<VCard v-if="isVisible" :width="props.width" :height="props.height" @click="openPluginDetail" class="flex flex-col">
|
||||||
<div class="relative pa-3 text-center card-cover-blurred" :style="{ background: `${backgroundColor}` }">
|
<div class="me-n3 absolute bottom-0 right-3">
|
||||||
<div v-if="props.plugin?.has_update" class="me-n3 absolute top-0 left-1">
|
<IconBtn>
|
||||||
<VIcon icon="mdi-new-box" class="text-white" />
|
<VIcon icon="mdi-dots-vertical" />
|
||||||
</div>
|
<VMenu activator="parent" close-on-content-click>
|
||||||
<div class="me-n3 absolute top-0 right-3">
|
<VList>
|
||||||
<IconBtn>
|
<VListItem
|
||||||
<VIcon icon="mdi-dots-vertical" class="text-white" />
|
v-for="(item, i) in dropdownItems"
|
||||||
<VMenu activator="parent" close-on-content-click>
|
v-show="item.show"
|
||||||
<VList>
|
:key="i"
|
||||||
<VListItem
|
variant="plain"
|
||||||
v-for="(item, i) in dropdownItems"
|
:base-color="item.props.color"
|
||||||
v-show="item.show"
|
@click="item.props.click"
|
||||||
:key="i"
|
>
|
||||||
variant="plain"
|
<template #prepend>
|
||||||
:base-color="item.props.color"
|
<VIcon :icon="item.props.prependIcon" />
|
||||||
@click="item.props.click"
|
</template>
|
||||||
>
|
<VListItemTitle v-text="item.title" />
|
||||||
<template #prepend>
|
</VListItem>
|
||||||
<VIcon :icon="item.props.prependIcon" />
|
</VList>
|
||||||
</template>
|
</VMenu>
|
||||||
<VListItemTitle v-text="item.title" />
|
</IconBtn>
|
||||||
</VListItem>
|
|
||||||
</VList>
|
|
||||||
</VMenu>
|
|
||||||
</IconBtn>
|
|
||||||
</div>
|
|
||||||
<VAvatar size="6rem">
|
|
||||||
<VImg
|
|
||||||
ref="imageRef"
|
|
||||||
:src="iconPath"
|
|
||||||
aspect-ratio="4/3"
|
|
||||||
cover
|
|
||||||
:class="{ shadow: isImageLoaded }"
|
|
||||||
@load="imageLoaded"
|
|
||||||
@error="imageLoadError = true"
|
|
||||||
/>
|
|
||||||
</VAvatar>
|
|
||||||
</div>
|
</div>
|
||||||
<VCardItem class="py-2">
|
<div
|
||||||
<VCardTitle class="flex items-center flex-row">
|
class="relative flex flex-row items-start pa-3 justify-between grow"
|
||||||
<VBadge v-if="props.plugin?.state" dot inline color="success" class="me-1 mb-1" />
|
:style="{ background: `${backgroundColor}` }"
|
||||||
{{ props.plugin?.plugin_name }}
|
>
|
||||||
<span class="text-sm ms-2 mt-1 text-gray-500">v{{ props.plugin?.plugin_version }}</span>
|
<div
|
||||||
</VCardTitle>
|
class="absolute inset-0 bg-cover bg-center"
|
||||||
</VCardItem>
|
:style="{ background: `${backgroundColor}`, filter: 'brightness(0.7)' }"
|
||||||
<VCardText class="pb-1">
|
/>
|
||||||
{{ props.plugin?.plugin_desc }}
|
<div class="relative flex-1 min-w-0">
|
||||||
</VCardText>
|
<VCardTitle class="text-white px-2 text-shadow whitespace-nowrap overflow-hidden text-ellipsis">
|
||||||
<VCardText class="flex justify-end align-self-baseline p-1 w-full align-end">
|
<VBadge v-if="props.plugin?.state" dot inline color="success" />
|
||||||
|
{{ 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-1 text-white text-shadow 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
|
||||||
|
:class="{ shadow: isImageLoaded }"
|
||||||
|
@load="imageLoaded"
|
||||||
|
@error="imageLoadError = true"
|
||||||
|
/>
|
||||||
|
</VAvatar>
|
||||||
|
</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">
|
<span v-if="props.count" class="ms-3">
|
||||||
<VIcon icon="mdi-fire" />
|
<VIcon icon="mdi-download" />
|
||||||
<span class="text-sm ms-1">{{ props.count?.toLocaleString() }}</span>
|
<span class="text-sm ms-1 mt-1">{{ props.count?.toLocaleString() }}</span>
|
||||||
</span>
|
</span>
|
||||||
</VCardText>
|
</VCardText>
|
||||||
|
<div v-if="props.plugin?.has_update" class="me-n3 absolute top-0 right-5">
|
||||||
|
<VIcon icon="mdi-new-box" class="text-white" />
|
||||||
|
</div>
|
||||||
</VCard>
|
</VCard>
|
||||||
|
|
||||||
<!-- 插件配置页面 -->
|
<!-- 插件配置页面 -->
|
||||||
|
|||||||
@@ -182,6 +182,7 @@ watch(
|
|||||||
'outline-dashed outline-1': props.media?.best_version && imageLoaded,
|
'outline-dashed outline-1': props.media?.best_version && imageLoaded,
|
||||||
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
|
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
|
||||||
}"
|
}"
|
||||||
|
min-height="170"
|
||||||
@click="editSubscribeDialog"
|
@click="editSubscribeDialog"
|
||||||
>
|
>
|
||||||
<div class="me-n3 absolute top-1 right-2">
|
<div class="me-n3 absolute top-1 right-2">
|
||||||
@@ -222,9 +223,9 @@ watch(
|
|||||||
<div class="absolute inset-0 subscribe-card-background"></div>
|
<div class="absolute inset-0 subscribe-card-background"></div>
|
||||||
</VImg>
|
</VImg>
|
||||||
</template>
|
</template>
|
||||||
<div v-if="imageLoaded">
|
<div>
|
||||||
<VCardText class="flex items-center">
|
<VCardText class="flex items-center">
|
||||||
<div class="h-auto w-12 flex-shrink-0 overflow-hidden rounded-md shadow-lg">
|
<div class="h-auto w-12 flex-shrink-0 overflow-hidden rounded-md shadow-lg" v-if="imageLoaded">
|
||||||
<VImg :src="props.media?.poster" aspect-ratio="2/3" cover @click.stop="viewMediaDetail">
|
<VImg :src="props.media?.poster" aspect-ratio="2/3" cover @click.stop="viewMediaDetail">
|
||||||
<template #placeholder>
|
<template #placeholder>
|
||||||
<div class="w-full h-full">
|
<div class="w-full h-full">
|
||||||
|
|||||||
@@ -92,9 +92,9 @@ onUnmounted(() => {
|
|||||||
<VDialog width="40rem" scrollable max-height="85vh">
|
<VDialog width="40rem" scrollable max-height="85vh">
|
||||||
<VCard title="阿里云盘登录" class="rounded-t">
|
<VCard title="阿里云盘登录" class="rounded-t">
|
||||||
<DialogCloseBtn @click="emit('close')" />
|
<DialogCloseBtn @click="emit('close')" />
|
||||||
<VCardText class="pt-2">
|
<VCardText class="pt-2 flex flex-col items-center">
|
||||||
<div class="my-6">
|
<div class="my-6 shadow-lg rounded text-center p-3 border">
|
||||||
<QrcodeVue class="mx-auto" :value="qrCodeContent" :size="200" max-width="25rem" />
|
<QrcodeVue class="mx-auto" :value="qrCodeContent" :size="200" />
|
||||||
</div>
|
</div>
|
||||||
<VAlert variant="tonal" :type="alertType" class="my-4 text-center" :text="text">
|
<VAlert variant="tonal" :type="alertType" class="my-4 text-center" :text="text">
|
||||||
<template #prepend />
|
<template #prepend />
|
||||||
|
|||||||
@@ -6,16 +6,20 @@ import api from '@/api'
|
|||||||
import { numberValidator } from '@/@validators'
|
import { numberValidator } from '@/@validators'
|
||||||
import { useDisplay } from 'vuetify'
|
import { useDisplay } from 'vuetify'
|
||||||
import ProgressDialog from './ProgressDialog.vue'
|
import ProgressDialog from './ProgressDialog.vue'
|
||||||
import { MediaDirectory } from '@/api/types'
|
import { FileItem, MediaDirectory } from '@/api/types'
|
||||||
|
|
||||||
// 显示器宽度
|
// 显示器宽度
|
||||||
const display = useDisplay()
|
const display = useDisplay()
|
||||||
|
|
||||||
// 输入参数
|
// 输入参数
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
path: String,
|
storage: {
|
||||||
target: String,
|
type: String,
|
||||||
|
default: () => 'local',
|
||||||
|
},
|
||||||
logids: Array<number>,
|
logids: Array<number>,
|
||||||
|
items: Array<FileItem>,
|
||||||
|
target: String,
|
||||||
})
|
})
|
||||||
|
|
||||||
// 定义事件
|
// 定义事件
|
||||||
@@ -50,10 +54,25 @@ const progressText = ref('请稍候 ...')
|
|||||||
// 整理进度
|
// 整理进度
|
||||||
const progressValue = ref(0)
|
const progressValue = ref(0)
|
||||||
|
|
||||||
// 文件转移表单
|
// 标题
|
||||||
|
const dialogTitle = computed(() => {
|
||||||
|
if (props.items) {
|
||||||
|
if (props.items.length > 1) return `整理 - 共 ${props.items.length} 项`
|
||||||
|
return `整理 - ${props.items[0].path}`
|
||||||
|
} else if (props.logids) {
|
||||||
|
return `整理 - 共 ${props.logids.length} 项`
|
||||||
|
}
|
||||||
|
return '手动整理'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 表单
|
||||||
const transferForm = reactive({
|
const transferForm = reactive({
|
||||||
|
storage: props.storage,
|
||||||
logid: 0,
|
logid: 0,
|
||||||
path: '',
|
path: '',
|
||||||
|
drive_id: '',
|
||||||
|
fileid: '',
|
||||||
|
filetype: '',
|
||||||
target: props.target ?? null,
|
target: props.target ?? null,
|
||||||
tmdbid: null,
|
tmdbid: null,
|
||||||
doubanid: null,
|
doubanid: null,
|
||||||
@@ -77,12 +96,6 @@ const targetDirectories = computed(() => {
|
|||||||
return [...new Set(directories)]
|
return [...new Set(directories)]
|
||||||
})
|
})
|
||||||
|
|
||||||
// 监听输入变化
|
|
||||||
watchEffect(() => {
|
|
||||||
transferForm.path = props.path ?? ''
|
|
||||||
transferForm.target = props.target ?? null
|
|
||||||
})
|
|
||||||
|
|
||||||
// 监听目的路径变化,自动查询目录的刮削配置
|
// 监听目的路径变化,自动查询目录的刮削配置
|
||||||
watch(transferForm, async () => {
|
watch(transferForm, async () => {
|
||||||
if (transferForm.target) {
|
if (transferForm.target) {
|
||||||
@@ -117,47 +130,25 @@ function stopLoadingProgress() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 整理文件
|
// 整理文件
|
||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
|
||||||
async function transfer() {
|
async function transfer() {
|
||||||
if (!props.logids && !props.path) return
|
if (!props.logids && !props.items) return
|
||||||
|
|
||||||
// 显示进度条
|
// 显示进度条
|
||||||
progressDialog.value = true
|
progressDialog.value = true
|
||||||
// 开始监听进度
|
// 开始监听进度
|
||||||
startLoadingProgress()
|
startLoadingProgress()
|
||||||
|
|
||||||
if (props.path) {
|
// 文件整理
|
||||||
// 文件整理
|
if (props.items) {
|
||||||
try {
|
for (const item of props.items) {
|
||||||
const result: { [key: string]: any } = await api.post(
|
await handleTransfer(item)
|
||||||
'transfer/manual',
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
params: transferForm,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
// 显示结果
|
|
||||||
if (result.success) $toast.success(`${props.path} 整理完成!`)
|
|
||||||
else $toast.error(`${props.path} 整理失败:${result.message}!`)
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e)
|
|
||||||
}
|
}
|
||||||
} else if (props.logids) {
|
}
|
||||||
// 日志整理
|
|
||||||
|
// 日志整理
|
||||||
|
if (props.logids) {
|
||||||
for (const logid of props.logids) {
|
for (const logid of props.logids) {
|
||||||
transferForm.logid = logid
|
await handleTransferLog(logid)
|
||||||
try {
|
|
||||||
const result: { [key: string]: any } = await api.post(
|
|
||||||
'transfer/manual',
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
params: transferForm,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if (!result.success) $toast.error(`历史记录 ${logid} 重新整理失败:${result.message}!`)
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,6 +160,32 @@ async function transfer() {
|
|||||||
emit('done')
|
emit('done')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 整理文件
|
||||||
|
async function handleTransfer(item: FileItem) {
|
||||||
|
transferForm.path = item.path
|
||||||
|
transferForm.fileid = item.fileid || ''
|
||||||
|
transferForm.drive_id = item.drive_id || ''
|
||||||
|
transferForm.filetype = item.type || 'dir'
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result: { [key: string]: any } = await api.post('transfer/manual', {}, { params: transferForm })
|
||||||
|
if (!result.success) $toast.error(`文件 ${item.path} 整理失败:${result.message}!`)
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 整理日志
|
||||||
|
async function handleTransferLog(logid: number) {
|
||||||
|
transferForm.logid = logid
|
||||||
|
try {
|
||||||
|
const result: { [key: string]: any } = await api.post('transfer/manual', {}, { params: transferForm })
|
||||||
|
if (!result.success) $toast.error(`历史记录 ${logid} 重新整理失败:${result.message}!`)
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 调用API,加载当前系统环境设置
|
// 调用API,加载当前系统环境设置
|
||||||
async function loadSystemSettings() {
|
async function loadSystemSettings() {
|
||||||
try {
|
try {
|
||||||
@@ -199,16 +216,13 @@ onMounted(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<VDialog scrollable max-width="50rem" :fullscreen="!display.mdAndUp.value">
|
<VDialog scrollable max-width="50rem" :fullscreen="!display.mdAndUp.value">
|
||||||
<VCard
|
<VCard :title="dialogTitle" class="rounded-t">
|
||||||
:title="`${props.path ? `整理 - ${props.path}` : `整理 - 共 ${props.logids?.length} 条记录`}`"
|
|
||||||
class="rounded-t"
|
|
||||||
>
|
|
||||||
<DialogCloseBtn @click="emit('close')" />
|
<DialogCloseBtn @click="emit('close')" />
|
||||||
<VDivider />
|
<VDivider />
|
||||||
<VCardText>
|
<VCardText>
|
||||||
<VForm @submit.prevent="() => {}">
|
<VForm @submit.prevent="() => {}">
|
||||||
<VRow>
|
<VRow>
|
||||||
<VCol cols="12" md="8">
|
<VCol v-if="props.storage == 'local'" cols="12" md="8">
|
||||||
<VCombobox
|
<VCombobox
|
||||||
v-model="transferForm.target"
|
v-model="transferForm.target"
|
||||||
:items="targetDirectories"
|
:items="targetDirectories"
|
||||||
@@ -218,7 +232,7 @@ onMounted(() => {
|
|||||||
persistent-hint
|
persistent-hint
|
||||||
/>
|
/>
|
||||||
</VCol>
|
</VCol>
|
||||||
<VCol cols="12" md="4">
|
<VCol v-if="props.storage == 'local'" cols="12" md="4">
|
||||||
<VSelect
|
<VSelect
|
||||||
v-model="transferForm.transfer_type"
|
v-model="transferForm.transfer_type"
|
||||||
label="整理方式"
|
label="整理方式"
|
||||||
|
|||||||
@@ -79,9 +79,11 @@ onUnmounted(() => {
|
|||||||
<VDialog width="40rem" scrollable max-height="85vh">
|
<VDialog width="40rem" scrollable max-height="85vh">
|
||||||
<VCard title="115网盘登录" class="rounded-t">
|
<VCard title="115网盘登录" class="rounded-t">
|
||||||
<DialogCloseBtn @click="emit('close')" />
|
<DialogCloseBtn @click="emit('close')" />
|
||||||
<VCardText class="pt-2">
|
<VCardText class="pt-2 flex flex-col items-center">
|
||||||
<div class="my-6">
|
<div class="my-6 shadow-lg rounded border">
|
||||||
<VImg class="mx-auto" :src="qrCodeContent" :size="200" max-width="25rem" />
|
<VImg class="mx-auto" :src="qrCodeContent" style="block-size: 200px; inline-size: 200px">
|
||||||
|
<VSkeletonLoader v-if="!qrCodeContent" class="w-full h-full" />
|
||||||
|
</VImg>
|
||||||
</div>
|
</div>
|
||||||
<VAlert variant="tonal" :type="alertType" class="my-4 text-center" :text="text">
|
<VAlert variant="tonal" :type="alertType" class="my-4 text-center" :text="text">
|
||||||
<template #prepend />
|
<template #prepend />
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { Axios } from 'axios'
|
import type { Axios, AxiosRequestConfig } from 'axios'
|
||||||
import type { PropType } from 'vue'
|
import type { PropType } from 'vue'
|
||||||
import { useConfirm } from 'vuetify-use-dialog'
|
import { useConfirm } from 'vuetify-use-dialog'
|
||||||
import { useToast } from 'vue-toast-notification'
|
import { useToast } from 'vue-toast-notification'
|
||||||
@@ -24,24 +24,31 @@ const appMode = computed(() => {
|
|||||||
const inProps = defineProps({
|
const inProps = defineProps({
|
||||||
icons: Object,
|
icons: Object,
|
||||||
storage: String,
|
storage: String,
|
||||||
path: String,
|
|
||||||
fileid: String,
|
|
||||||
pickcode: String,
|
|
||||||
endpoints: Object as PropType<EndPoints>,
|
endpoints: Object as PropType<EndPoints>,
|
||||||
axios: {
|
axios: {
|
||||||
type: Object as PropType<Axios>,
|
type: Object as PropType<Axios>,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
refreshpending: Boolean,
|
refreshpending: Boolean,
|
||||||
|
item: {
|
||||||
|
type: Object as PropType<FileItem>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
sort: String,
|
sort: String,
|
||||||
})
|
})
|
||||||
|
|
||||||
// 对外事件
|
// 对外事件
|
||||||
const emit = defineEmits(['loading', 'pathchanged', 'refreshed', 'filedeleted', 'renamed'])
|
const emit = defineEmits(['loading', 'pathchanged', 'refreshed', 'filedeleted', 'renamed'])
|
||||||
|
|
||||||
|
// 确认框
|
||||||
|
const createConfirm = useConfirm()
|
||||||
|
|
||||||
// 提示框
|
// 提示框
|
||||||
const $toast = useToast()
|
const $toast = useToast()
|
||||||
|
|
||||||
|
// 是否选择模式
|
||||||
|
const selectMode = ref(false)
|
||||||
|
|
||||||
// 是否正在加载
|
// 是否正在加载
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
|
|
||||||
@@ -57,9 +64,6 @@ const progressText = ref('请稍候 ...')
|
|||||||
// 识别进度
|
// 识别进度
|
||||||
const progressValue = ref(0)
|
const progressValue = ref(0)
|
||||||
|
|
||||||
// 确认框
|
|
||||||
const createConfirm = useConfirm()
|
|
||||||
|
|
||||||
// 内容列表
|
// 内容列表
|
||||||
const items = ref<FileItem[]>([])
|
const items = ref<FileItem[]>([])
|
||||||
|
|
||||||
@@ -78,9 +82,12 @@ const newName = ref('')
|
|||||||
// 处理目录内所有文件
|
// 处理目录内所有文件
|
||||||
const renameAll = ref(false)
|
const renameAll = ref(false)
|
||||||
|
|
||||||
// 当前名称
|
// 当前操作项
|
||||||
const currentItem = ref<FileItem>()
|
const currentItem = ref<FileItem>()
|
||||||
|
|
||||||
|
// 选中的项目
|
||||||
|
const selected = ref<FileItem[]>([])
|
||||||
|
|
||||||
// 识别结果
|
// 识别结果
|
||||||
const nameTestResult = ref<Context>()
|
const nameTestResult = ref<Context>()
|
||||||
|
|
||||||
@@ -90,40 +97,58 @@ const nameTestDialog = ref(false)
|
|||||||
// 弹出菜单
|
// 弹出菜单
|
||||||
const dropdownItems = ref<{ [key: string]: any }[]>([])
|
const dropdownItems = ref<{ [key: string]: any }[]>([])
|
||||||
|
|
||||||
|
// 加载进度SSE
|
||||||
|
const progressEventSource = ref<EventSource>()
|
||||||
|
|
||||||
// 目录过滤
|
// 目录过滤
|
||||||
const dirs = computed(() => items.value.filter(item => item.type === 'dir' && item.name.includes(filter.value)))
|
const dirs = computed(() => items.value.filter(item => item.type === 'dir' && item.name.includes(filter.value)))
|
||||||
|
|
||||||
// 文件过滤
|
// 文件过滤
|
||||||
const files = computed(() => items.value.filter(item => item.type === 'file' && item.name.includes(filter.value)))
|
const files = computed(() => items.value.filter(item => item.type === 'file' && item.name.includes(filter.value)))
|
||||||
|
|
||||||
// 是否目录
|
// 是否目录
|
||||||
const isDir = computed(() => inProps.path?.endsWith('/'))
|
const isDir = computed(() => inProps.item.path?.endsWith('/'))
|
||||||
|
|
||||||
// 是否文件
|
// 是否文件
|
||||||
const isFile = computed(() => !isDir.value)
|
const isFile = computed(() => !isDir.value)
|
||||||
|
|
||||||
|
// 需要整理的文件项
|
||||||
|
const transferItems = ref<FileItem[]>([])
|
||||||
|
|
||||||
|
// 大小控制
|
||||||
|
const scrollStyle = computed(() => {
|
||||||
|
return appMode.value
|
||||||
|
? 'height: calc(100vh - 15.5rem - env(safe-area-inset-bottom) - 3.5rem)'
|
||||||
|
: 'height: calc(100vh - 14.5rem - env(safe-area-inset-bottom)'
|
||||||
|
})
|
||||||
|
|
||||||
// 是否为图片文件
|
// 是否为图片文件
|
||||||
const isImage = computed(() => {
|
const isImage = computed(() => {
|
||||||
const ext = inProps.path?.split('.').pop()?.toLowerCase()
|
const ext = inProps.item.path?.split('.').pop()?.toLowerCase()
|
||||||
return ['png', 'jpg', 'jpeg', 'gif', 'bmp'].includes(ext ?? '')
|
return ['png', 'jpg', 'jpeg', 'gif', 'bmp'].includes(ext ?? '')
|
||||||
})
|
})
|
||||||
|
|
||||||
// 调API加载内容
|
// 调整选择模式
|
||||||
async function load() {
|
function changeSelectMode() {
|
||||||
|
selectMode.value = !selectMode.value
|
||||||
|
if (!selectMode.value) selected.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调API加载文件夹内的内容
|
||||||
|
async function list_files() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
emit('loading', true)
|
emit('loading', true)
|
||||||
|
|
||||||
// 参数
|
// 参数
|
||||||
const url = inProps.endpoints?.list.url
|
const url = inProps.endpoints?.list.url
|
||||||
.replace(/{storage}/g, inProps.storage)
|
.replace(/{storage}/g, inProps.storage)
|
||||||
.replace(/{path}/g, encodeURIComponent(inProps.path || ''))
|
|
||||||
.replace(/{sort}/g, inProps.sort || 'name')
|
.replace(/{sort}/g, inProps.sort || 'name')
|
||||||
.replace(/{fileid}/g, inProps.fileid || '')
|
|
||||||
.replace(/{filetype}/g, isDir.value ? 'dir' : 'file')
|
const config: AxiosRequestConfig<FileItem> = {
|
||||||
.replace(/{pickcode}/g, inProps.pickcode || '')
|
|
||||||
const config = {
|
|
||||||
url,
|
url,
|
||||||
method: inProps.endpoints?.list.method || 'get',
|
method: inProps.endpoints?.list.method || 'get',
|
||||||
|
data: inProps.item,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载数据
|
// 加载数据
|
||||||
items.value = (await inProps.axios.request(config)) ?? []
|
items.value = (await inProps.axios.request(config)) ?? []
|
||||||
emit('loading', false)
|
emit('loading', false)
|
||||||
@@ -131,60 +156,99 @@ async function load() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 删除项目
|
// 删除项目
|
||||||
async function deleteItem(item: FileItem) {
|
async function deleteItem(item: FileItem, confirm: boolean = true) {
|
||||||
|
if (confirm) {
|
||||||
|
const confirmed = await createConfirm({
|
||||||
|
title: '确认',
|
||||||
|
content: `是否确认删除${item.type === 'dir' ? '目录' : '文件'} ${item.name}?`,
|
||||||
|
})
|
||||||
|
if (!confirmed) return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载中
|
||||||
|
emit('loading', true)
|
||||||
|
|
||||||
|
// 请求API
|
||||||
|
const url = inProps.endpoints?.delete.url.replace(/{storage}/g, inProps.storage)
|
||||||
|
const config: AxiosRequestConfig<FileItem> = {
|
||||||
|
url,
|
||||||
|
method: inProps.endpoints?.delete.method || 'post',
|
||||||
|
data: item,
|
||||||
|
}
|
||||||
|
await inProps.axios.request(config)
|
||||||
|
|
||||||
|
// 删除完成
|
||||||
|
emit('loading', false)
|
||||||
|
emit('filedeleted')
|
||||||
|
|
||||||
|
// 重新加载
|
||||||
|
list_files()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量删除
|
||||||
|
async function batchDelete() {
|
||||||
const confirmed = await createConfirm({
|
const confirmed = await createConfirm({
|
||||||
title: '确认',
|
title: '确认',
|
||||||
content: `是否确认删除${item.type === 'dir' ? '目录' : '文件'} ${item.name}?`,
|
content: `是否确认删除选中的 ${selected.value.length} 个项目?`,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (confirmed) {
|
if (!confirmed) return
|
||||||
emit('loading', true)
|
|
||||||
const url = inProps.endpoints?.delete.url
|
|
||||||
.replace(/{storage}/g, inProps.storage)
|
|
||||||
.replace(/{path}/g, encodeURIComponent(item.path))
|
|
||||||
.replace(/{fileid}/g, item.fileid || '')
|
|
||||||
|
|
||||||
const config = {
|
// 显示进度条
|
||||||
url,
|
progressDialog.value = true
|
||||||
method: inProps.endpoints?.delete.method || 'post',
|
progressValue.value = 0
|
||||||
}
|
|
||||||
|
|
||||||
await inProps.axios.request(config)
|
// 删除选中的项目
|
||||||
emit('filedeleted')
|
selected.value.every(async item => {
|
||||||
emit('loading', false)
|
progressText.value = `正在删除 ${item.name} ...`
|
||||||
// 重新加载
|
await deleteItem(item, false)
|
||||||
load()
|
})
|
||||||
}
|
|
||||||
|
// 关闭进度条
|
||||||
|
progressDialog.value = false
|
||||||
|
|
||||||
|
// 重新加载
|
||||||
|
list_files()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 切换路径
|
// 切换路径
|
||||||
function changePath(item: FileItem) {
|
function changePath(item: FileItem) {
|
||||||
item.path = inProps.path + item.name + (item.type === 'dir' ? '/' : '')
|
item.path = inProps.item.path + item.name + (item.type === 'dir' ? '/' : '')
|
||||||
emit('pathchanged', item)
|
emit('pathchanged', item)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 新窗口中下载文件
|
// 点击列表项
|
||||||
function download(item: FileItem) {
|
function listItemClick(item: FileItem) {
|
||||||
const token = store.state.auth.token
|
if (selectMode.value) {
|
||||||
const url_path = inProps.endpoints?.download.url
|
if (selected.value.includes(item)) {
|
||||||
.replace(/{storage}/g, inProps.storage)
|
selected.value = selected.value.filter(i => i !== item)
|
||||||
.replace(/{path}/g, encodeURIComponent(item.path))
|
} else {
|
||||||
.replace(/{fileid}/g, item.fileid || '')
|
selected.value.push(item)
|
||||||
.replace(/{pickcode}/g, item.pickcode || '')
|
}
|
||||||
const url = `${import.meta.env.VITE_API_BASE_URL}${url_path.slice(1)}&token=${token}`
|
// 去重
|
||||||
// 下载文件
|
selected.value = Array.from(new Set(selected.value))
|
||||||
window.open(url, '_blank')
|
return false
|
||||||
|
}
|
||||||
|
changePath(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示图片
|
// 新窗口中下载文件
|
||||||
|
async function download(item: FileItem) {
|
||||||
|
const url = inProps.endpoints?.download.url.replace(/{storage}/g, inProps.storage)
|
||||||
|
const filterEntries = Object.entries(item).filter(([key, value]) => !['children', 'thumbnail'].includes(key) && value)
|
||||||
|
const queryParams = filterEntries.map(([key, value]) => `${key}=${encodeURIComponent(value)}`).join('&')
|
||||||
|
window.open(
|
||||||
|
`${import.meta.env.VITE_API_BASE_URL}${url.slice(1)}?${queryParams}&token=${store.state.auth.token}`,
|
||||||
|
'_blank',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取图片地址
|
||||||
function getImgLink(item: FileItem) {
|
function getImgLink(item: FileItem) {
|
||||||
const token = store.state.auth.token
|
let url = inProps.endpoints?.image.url.replace(/{storage}/g, inProps.storage)
|
||||||
const url_path = inProps.endpoints?.image.url
|
const filterEntries = Object.entries(item).filter(([key, value]) => !['children', 'thumbnail'].includes(key) && value)
|
||||||
.replace(/{storage}/g, inProps.storage)
|
const queryParams = filterEntries.map(([key, value]) => `${key}=${encodeURIComponent(value)}`).join('&')
|
||||||
.replace(/{path}/g, encodeURIComponent(item.path))
|
return `${import.meta.env.VITE_API_BASE_URL}${url.slice(1)}?${queryParams}&token=${store.state.auth.token}`
|
||||||
.replace(/{fileid}/g, item.fileid || '')
|
|
||||||
.replace(/{pickcode}/g, item.pickcode || '')
|
|
||||||
return `${import.meta.env.VITE_API_BASE_URL}${url_path.slice(1)}&token=${token}`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示重命名弹窗
|
// 显示重命名弹窗
|
||||||
@@ -201,7 +265,7 @@ async function get_recommend_name() {
|
|||||||
try {
|
try {
|
||||||
const result: { [key: string]: any } = await api.get('transfer/name', {
|
const result: { [key: string]: any } = await api.get('transfer/name', {
|
||||||
params: {
|
params: {
|
||||||
path: `${inProps.path}${currentItem.value?.name}`,
|
path: `${inProps.item.path}${currentItem.value?.name}`,
|
||||||
filetype: currentItem.value?.type ?? 'file',
|
filetype: currentItem.value?.type ?? 'file',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -220,60 +284,89 @@ async function get_recommend_name() {
|
|||||||
async function rename() {
|
async function rename() {
|
||||||
emit('loading', true)
|
emit('loading', true)
|
||||||
|
|
||||||
|
// 关闭弹窗
|
||||||
|
renamePopper.value = false
|
||||||
|
|
||||||
|
// 显示进度条
|
||||||
|
progressDialog.value = true
|
||||||
|
progressValue.value = 0
|
||||||
|
if (renameAll.value) {
|
||||||
|
progressText.value = `正在重命名 ${currentItem.value?.path} 及目录内所有文件 ...`
|
||||||
|
} else {
|
||||||
|
progressText.value = `正在重命名 ${currentItem.value?.name} ...`
|
||||||
|
}
|
||||||
|
if (renameAll.value) {
|
||||||
|
startLoadingProgress()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调API
|
||||||
let url = inProps.endpoints?.rename.url
|
let url = inProps.endpoints?.rename.url
|
||||||
.replace(/{storage}/g, inProps.storage)
|
.replace(/{storage}/g, inProps.storage)
|
||||||
.replace(/{path}/g, encodeURIComponent(currentItem.value?.path || ''))
|
|
||||||
.replace(/{fileid}/g, currentItem.value?.fileid || '')
|
|
||||||
.replace(/{newname}/g, encodeURIComponent(newName.value))
|
.replace(/{newname}/g, encodeURIComponent(newName.value))
|
||||||
.replace(/{filetype}/g, currentItem.value?.type || 'file')
|
|
||||||
|
|
||||||
if (renameAll.value) {
|
if (renameAll.value) {
|
||||||
url += '&recursive=true'
|
url += '&recursive=true'
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = {
|
const config: AxiosRequestConfig<FileItem> = {
|
||||||
url,
|
url,
|
||||||
method: inProps.endpoints?.mkdir.method || 'post',
|
method: inProps.endpoints?.rename.method || 'post',
|
||||||
|
data: currentItem.value,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 关闭弹窗
|
|
||||||
renamePopper.value = false
|
|
||||||
newName.value = ''
|
|
||||||
renameAll.value = false
|
|
||||||
|
|
||||||
// 显示进度条
|
|
||||||
progressDialog.value = true
|
|
||||||
progressText.value = `正在重命名 ${currentItem.value?.path} ...`
|
|
||||||
progressValue.value = 0
|
|
||||||
|
|
||||||
// 调API
|
|
||||||
const result: { [key: string]: any } = await inProps.axios?.request(config)
|
const result: { [key: string]: any } = await inProps.axios?.request(config)
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
$toast.error(result.message)
|
$toast.error(result.message)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 关闭进度条
|
// 关闭进度条
|
||||||
|
if (renameAll.value) {
|
||||||
|
stopLoadingProgress()
|
||||||
|
}
|
||||||
progressDialog.value = false
|
progressDialog.value = false
|
||||||
|
|
||||||
// 通知重新加载
|
// 通知重新加载
|
||||||
|
newName.value = ''
|
||||||
|
renameAll.value = false
|
||||||
emit('loading', false)
|
emit('loading', false)
|
||||||
emit('renamed')
|
emit('renamed')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示整理对话框
|
// 显示整理对话框
|
||||||
function showTransfer(item: FileItem) {
|
function showTransfer(item: FileItem) {
|
||||||
currentItem.value = item
|
transferItems.value = [item]
|
||||||
transferPopper.value = true
|
transferPopper.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 显示批量整理对话框
|
||||||
|
function showBatchTransfer() {
|
||||||
|
transferItems.value = selected.value
|
||||||
|
transferPopper.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 整理完成
|
||||||
|
function transferDone() {
|
||||||
|
transferPopper.value = false
|
||||||
|
list_files()
|
||||||
|
}
|
||||||
|
|
||||||
// 将文件修改时间(timestape)转换为本地时间
|
// 将文件修改时间(timestape)转换为本地时间
|
||||||
function formatTime(timestape: number) {
|
function formatTime(timestape: number) {
|
||||||
return new Date(timestape * 1000).toLocaleString()
|
return new Date(timestape * 1000).toLocaleString()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 监听path变化或者storage变化
|
// 监听refreshPending变化
|
||||||
watch(
|
watch(
|
||||||
[() => inProps.path, () => inProps.fileid, () => inProps.storage],
|
() => inProps.refreshpending,
|
||||||
|
async () => {
|
||||||
|
if (inProps.refreshpending) {
|
||||||
|
await list_files()
|
||||||
|
emit('refreshed')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// 监听item变化或者storage变化
|
||||||
|
watch(
|
||||||
|
[() => inProps.item, () => inProps.storage],
|
||||||
async () => {
|
async () => {
|
||||||
// 清空列表
|
// 清空列表
|
||||||
items.value = []
|
items.value = []
|
||||||
@@ -296,11 +389,11 @@ watch(
|
|||||||
{
|
{
|
||||||
title: '刮削',
|
title: '刮削',
|
||||||
value: 2,
|
value: 2,
|
||||||
show: inProps.storage == 'local',
|
show: true,
|
||||||
props: {
|
props: {
|
||||||
prependIcon: 'mdi-auto-fix',
|
prependIcon: 'mdi-auto-fix',
|
||||||
click: (_item: FileItem) => {
|
click: (_item: FileItem) => {
|
||||||
scrape(_item.path || '')
|
scrape(_item)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -316,7 +409,7 @@ watch(
|
|||||||
{
|
{
|
||||||
title: '整理',
|
title: '整理',
|
||||||
value: 4,
|
value: 4,
|
||||||
show: inProps.storage == 'local',
|
show: true,
|
||||||
props: {
|
props: {
|
||||||
prependIcon: 'mdi-folder-arrow-right',
|
prependIcon: 'mdi-folder-arrow-right',
|
||||||
click: showTransfer,
|
click: showTransfer,
|
||||||
@@ -333,22 +426,11 @@ watch(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
await load()
|
await list_files()
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
// 监听refreshPending变化
|
|
||||||
watch(
|
|
||||||
() => inProps.refreshpending,
|
|
||||||
async () => {
|
|
||||||
if (inProps.refreshpending) {
|
|
||||||
await load()
|
|
||||||
emit('refreshed')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// 调用API识别
|
// 调用API识别
|
||||||
async function recognize(path: string) {
|
async function recognize(path: string) {
|
||||||
try {
|
try {
|
||||||
@@ -371,27 +453,71 @@ async function recognize(path: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 调用API刮削
|
// 调用API刮削
|
||||||
async function scrape(path: string) {
|
async function scrape(item: FileItem, confirm: boolean = true) {
|
||||||
try {
|
try {
|
||||||
|
if (confirm) {
|
||||||
|
// 确认
|
||||||
|
const confirmed = await createConfirm({
|
||||||
|
title: '确认',
|
||||||
|
content: `是否确认刮削 ${item.path}?`,
|
||||||
|
})
|
||||||
|
if (!confirmed) return
|
||||||
|
}
|
||||||
|
|
||||||
// 显示进度条
|
// 显示进度条
|
||||||
progressDialog.value = true
|
progressDialog.value = true
|
||||||
progressText.value = `正在刮削 ${path} ...`
|
progressText.value = `正在刮削 ${item.path} ...`
|
||||||
const result: { [key: string]: any } = await api.get('media/scrape', {
|
|
||||||
params: {
|
const result: { [key: string]: any } = await api.post(`media/scrape/${inProps.storage}`, item)
|
||||||
path,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
// 关闭进度条
|
// 关闭进度条
|
||||||
progressDialog.value = false
|
progressDialog.value = false
|
||||||
if (!result.success) $toast.error(result.message)
|
if (!result.success) $toast.error(result.message)
|
||||||
else $toast.success(`${path}削刮完成!`)
|
else $toast.success(`${item.path} 削刮完成!`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 批量刮削
|
||||||
|
async function batchScrape() {
|
||||||
|
// 确认
|
||||||
|
const confirmed = await createConfirm({
|
||||||
|
title: '确认',
|
||||||
|
content: `是否确认刮削选中的 ${selected.value.length} 项?`,
|
||||||
|
})
|
||||||
|
if (!confirmed) return
|
||||||
|
|
||||||
|
selected.value.map(item => {
|
||||||
|
scrape(item, false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用SSE监听加载进度
|
||||||
|
function startLoadingProgress() {
|
||||||
|
progressText.value = '请稍候 ...'
|
||||||
|
|
||||||
|
const token = store.state.auth.token
|
||||||
|
|
||||||
|
progressEventSource.value = new EventSource(
|
||||||
|
`${import.meta.env.VITE_API_BASE_URL}system/progress/batchrename?token=${token}`,
|
||||||
|
)
|
||||||
|
progressEventSource.value.onmessage = event => {
|
||||||
|
const progress = JSON.parse(event.data)
|
||||||
|
if (progress) {
|
||||||
|
progressText.value = progress.text
|
||||||
|
progressValue.value = progress.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 停止监听加载进度
|
||||||
|
function stopLoadingProgress() {
|
||||||
|
progressEventSource.value?.close()
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
load()
|
list_files()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -411,15 +537,31 @@ onMounted(() => {
|
|||||||
rounded="0"
|
rounded="0"
|
||||||
/>
|
/>
|
||||||
<VSpacer v-if="isFile" />
|
<VSpacer v-if="isFile" />
|
||||||
<IconBtn v-if="isFile" @click="recognize(inProps.path || '')">
|
<IconBtn v-if="!isFile" @click="changeSelectMode">
|
||||||
|
<VIcon color="primary" v-if="selectMode"> mdi-selection-remove </VIcon>
|
||||||
|
<VIcon color="primary" v-else>mdi-select</VIcon>
|
||||||
|
</IconBtn>
|
||||||
|
<IconBtn v-if="isFile" @click="recognize(inProps.item.path || '')">
|
||||||
<VIcon color="primary"> mdi-text-recognition </VIcon>
|
<VIcon color="primary"> mdi-text-recognition </VIcon>
|
||||||
</IconBtn>
|
</IconBtn>
|
||||||
<IconBtn v-if="isFile && items.length > 0" @click="download(items[0])">
|
<IconBtn v-if="isFile && items.length > 0" @click="download(items[0])">
|
||||||
<VIcon color="primary"> mdi-download </VIcon>
|
<VIcon color="primary"> mdi-download </VIcon>
|
||||||
</IconBtn>
|
</IconBtn>
|
||||||
<IconBtn v-if="!isFile" @click="load">
|
<IconBtn v-if="!isFile" @click="list_files">
|
||||||
<VIcon color="primary"> mdi-refresh </VIcon>
|
<VIcon color="primary"> mdi-refresh </VIcon>
|
||||||
</IconBtn>
|
</IconBtn>
|
||||||
|
<!-- 批量操作按钮 -->
|
||||||
|
<span v-if="selected.length > 0">
|
||||||
|
<IconBtn @click.stop="batchScrape">
|
||||||
|
<VIcon color="primary" icon="mdi-auto-fix" />
|
||||||
|
</IconBtn>
|
||||||
|
<IconBtn @click.stop="showBatchTransfer">
|
||||||
|
<VIcon color="primary" icon="mdi-folder-arrow-right" />
|
||||||
|
</IconBtn>
|
||||||
|
<IconBtn @click.stop="batchDelete">
|
||||||
|
<VIcon icon="mdi-delete-outline" color="error" />
|
||||||
|
</IconBtn>
|
||||||
|
</span>
|
||||||
</VToolbar>
|
</VToolbar>
|
||||||
<VCardText v-if="loading" class="text-center flex flex-col items-center">
|
<VCardText v-if="loading" class="text-center flex flex-col items-center">
|
||||||
<VProgressCircular size="48" indeterminate color="primary" />
|
<VProgressCircular size="48" indeterminate color="primary" />
|
||||||
@@ -446,31 +588,29 @@ onMounted(() => {
|
|||||||
<!-- 目录和文件列表 -->
|
<!-- 目录和文件列表 -->
|
||||||
<VCardText v-else-if="dirs.length || files.length" class="p-0">
|
<VCardText v-else-if="dirs.length || files.length" class="p-0">
|
||||||
<VList subheader>
|
<VList subheader>
|
||||||
<VVirtualScroll
|
<VVirtualScroll :items="[...dirs, ...files]" :style="scrollStyle">
|
||||||
:items="[...dirs, ...files]"
|
|
||||||
:style="
|
|
||||||
appMode
|
|
||||||
? 'height: calc(100vh - 15.5rem - env(safe-area-inset-bottom) - 3.5rem)'
|
|
||||||
: 'height: calc(100vh - 14.5rem - env(safe-area-inset-bottom)'
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<template #default="{ item }">
|
<template #default="{ item }">
|
||||||
<VHover>
|
<VHover>
|
||||||
<template #default="hover">
|
<template #default="hover">
|
||||||
<VListItem v-bind="hover.props" class="px-3 pe-1" @click="changePath(item)">
|
<VListItem v-bind="hover.props" class="px-3 pe-1" @click="listItemClick(item)">
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
<VIcon
|
<VListItemAction v-if="selectMode">
|
||||||
v-if="inProps.icons && item.extension"
|
<VCheckbox v-model="selected" :value="item" />
|
||||||
:icon="inProps.icons[item.extension.toLowerCase()] || inProps.icons?.other"
|
</VListItemAction>
|
||||||
/>
|
<template v-else>
|
||||||
<VIcon v-else icon="mdi-folder-outline" />
|
<VIcon
|
||||||
|
v-if="inProps.icons && item.extension"
|
||||||
|
:icon="inProps.icons[item.extension.toLowerCase()] || inProps.icons?.other"
|
||||||
|
/>
|
||||||
|
<VIcon v-else icon="mdi-folder-outline" />
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
<VListItemTitle v-text="item.name" />
|
<VListItemTitle v-text="item.name" />
|
||||||
<VListItemSubtitle v-if="item.size">
|
<VListItemSubtitle v-if="item.size">
|
||||||
{{ formatBytes(item.size) }}
|
{{ formatBytes(item.size) }}
|
||||||
</VListItemSubtitle>
|
</VListItemSubtitle>
|
||||||
<template #append>
|
<template #append>
|
||||||
<IconBtn class="d-sm-none">
|
<IconBtn v-if="display.smAndDown.value && !selectMode">
|
||||||
<VIcon icon="mdi-dots-vertical" />
|
<VIcon icon="mdi-dots-vertical" />
|
||||||
<VMenu activator="parent" close-on-content-click>
|
<VMenu activator="parent" close-on-content-click>
|
||||||
<VList>
|
<VList>
|
||||||
@@ -490,38 +630,38 @@ onMounted(() => {
|
|||||||
</VList>
|
</VList>
|
||||||
</VMenu>
|
</VMenu>
|
||||||
</IconBtn>
|
</IconBtn>
|
||||||
<span v-if="hover.isHovering" class="flex">
|
<span v-if="hover.isHovering && display.mdAndUp.value && !selectMode" class="flex">
|
||||||
<VTooltip text="识别">
|
<VTooltip text="识别">
|
||||||
<template #activator="{ props }">
|
<template #activator="{ props }">
|
||||||
<IconBtn v-bind="props" class="d-none d-sm-block" @click.stop="recognize(item.path)">
|
<IconBtn v-bind="props" @click.stop="recognize(item.path)">
|
||||||
<VIcon icon="mdi-text-recognition" />
|
<VIcon icon="mdi-text-recognition" />
|
||||||
</IconBtn>
|
</IconBtn>
|
||||||
</template>
|
</template>
|
||||||
</VTooltip>
|
</VTooltip>
|
||||||
<VTooltip text="刮削" v-if="storage == 'local'">
|
<VTooltip text="刮削">
|
||||||
<template #activator="{ props }">
|
<template #activator="{ props }">
|
||||||
<IconBtn v-bind="props" class="d-none d-sm-block" @click.stop="scrape(item.path)">
|
<IconBtn v-bind="props" @click.stop="scrape(item)">
|
||||||
<VIcon icon="mdi-auto-fix" />
|
<VIcon icon="mdi-auto-fix" />
|
||||||
</IconBtn>
|
</IconBtn>
|
||||||
</template>
|
</template>
|
||||||
</VTooltip>
|
</VTooltip>
|
||||||
<VTooltip text="重命名">
|
<VTooltip text="重命名">
|
||||||
<template #activator="{ props }">
|
<template #activator="{ props }">
|
||||||
<IconBtn v-bind="props" class="d-none d-sm-block" @click.stop="showRenmae(item)">
|
<IconBtn v-bind="props" @click.stop="showRenmae(item)">
|
||||||
<VIcon icon="mdi-rename" />
|
<VIcon icon="mdi-rename" />
|
||||||
</IconBtn>
|
</IconBtn>
|
||||||
</template>
|
</template>
|
||||||
</VTooltip>
|
</VTooltip>
|
||||||
<VTooltip text="整理" v-if="storage == 'local'">
|
<VTooltip text="整理">
|
||||||
<template #activator="{ props }">
|
<template #activator="{ props }">
|
||||||
<IconBtn v-bind="props" class="d-none d-sm-block" @click.stop="showTransfer(item)">
|
<IconBtn v-bind="props" @click.stop="showTransfer(item)">
|
||||||
<VIcon icon="mdi-folder-arrow-right" />
|
<VIcon icon="mdi-folder-arrow-right" />
|
||||||
</IconBtn>
|
</IconBtn>
|
||||||
</template>
|
</template>
|
||||||
</VTooltip>
|
</VTooltip>
|
||||||
<VTooltip text="删除">
|
<VTooltip text="删除">
|
||||||
<template #activator="{ props }">
|
<template #activator="{ props }">
|
||||||
<IconBtn v-bind="props" class="d-none d-sm-block" @click.stop="deleteItem(item)">
|
<IconBtn v-bind="props" @click.stop="deleteItem(item)">
|
||||||
<VIcon icon="mdi-delete-outline" color="error" />
|
<VIcon icon="mdi-delete-outline" color="error" />
|
||||||
</IconBtn>
|
</IconBtn>
|
||||||
</template>
|
</template>
|
||||||
@@ -550,7 +690,7 @@ onMounted(() => {
|
|||||||
<VCol cols="12">
|
<VCol cols="12">
|
||||||
<VTextField v-model="newName" label="新名称" :loading="renameLoading" />
|
<VTextField v-model="newName" label="新名称" :loading="renameLoading" />
|
||||||
</VCol>
|
</VCol>
|
||||||
<VCol cols="6" v-if="currentItem && currentItem.type == 'dir'">
|
<VCol cols="12" md="6" v-if="currentItem && currentItem.type == 'dir'">
|
||||||
<VSwitch v-model="renameAll" label="自动重命名目录内所有媒体文件" />
|
<VSwitch v-model="renameAll" label="自动重命名目录内所有媒体文件" />
|
||||||
</VCol>
|
</VCol>
|
||||||
</VRow>
|
</VRow>
|
||||||
@@ -569,13 +709,9 @@ onMounted(() => {
|
|||||||
<ReorganizeDialog
|
<ReorganizeDialog
|
||||||
v-if="transferPopper"
|
v-if="transferPopper"
|
||||||
v-model="transferPopper"
|
v-model="transferPopper"
|
||||||
:path="currentItem?.path"
|
:storage="inProps.storage"
|
||||||
@done="
|
:items="transferItems"
|
||||||
() => {
|
@done="transferDone"
|
||||||
transferPopper = false
|
|
||||||
load()
|
|
||||||
}
|
|
||||||
"
|
|
||||||
@close="transferPopper = false"
|
@close="transferPopper = false"
|
||||||
/>
|
/>
|
||||||
<!-- 进度框 -->
|
<!-- 进度框 -->
|
||||||
|
|||||||
@@ -1,16 +1,22 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { Axios } from 'axios'
|
import type { Axios, AxiosRequestConfig } from 'axios'
|
||||||
import type { EndPoints } from '@/api/types'
|
import type { EndPoints, FileItem } from '@/api/types'
|
||||||
|
import { useDisplay } from 'vuetify'
|
||||||
|
|
||||||
|
// 显示器宽度
|
||||||
|
const display = useDisplay()
|
||||||
|
|
||||||
// 输入参数
|
// 输入参数
|
||||||
const inProps = defineProps({
|
const inProps = defineProps({
|
||||||
storages: Array as PropType<any[]>,
|
storages: Array as PropType<any[]>,
|
||||||
storage: String,
|
storage: String,
|
||||||
path: String,
|
item: {
|
||||||
fileid: String,
|
type: Object as PropType<FileItem>,
|
||||||
fileidstack: {
|
required: true,
|
||||||
type: Array as PropType<string[]>,
|
},
|
||||||
default: () => [],
|
itemstack: {
|
||||||
|
type: Array as PropType<FileItem[]>,
|
||||||
|
required: true,
|
||||||
},
|
},
|
||||||
endpoints: Object as PropType<EndPoints>,
|
endpoints: Object as PropType<EndPoints>,
|
||||||
axios: {
|
axios: {
|
||||||
@@ -42,21 +48,20 @@ function changeSort() {
|
|||||||
// 计算PATH面包屑
|
// 计算PATH面包屑
|
||||||
const pathSegments = computed(() => {
|
const pathSegments = computed(() => {
|
||||||
let path_str = ''
|
let path_str = ''
|
||||||
const isFolder = inProps.path?.endsWith('/')
|
const isFolder = inProps.item.path?.endsWith('/')
|
||||||
const segments = inProps.path?.split('/').filter(item => item)
|
const segments = inProps.item.path?.split('/').filter(item => item)
|
||||||
const fileids = inProps.fileidstack ?? []
|
|
||||||
return (
|
return (
|
||||||
segments?.map((item, index) => {
|
segments?.map((item, index) => {
|
||||||
path_str += item + (index < segments.length - 1 || isFolder ? '/' : '')
|
path_str += item + (index < segments.length - 1 || isFolder ? '/' : '')
|
||||||
return {
|
return {
|
||||||
name: item,
|
name: item,
|
||||||
path: path_str,
|
path: path_str,
|
||||||
fileid: fileids[index],
|
|
||||||
}
|
}
|
||||||
}) ?? []
|
}) ?? []
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 当前存储
|
||||||
const storageObject = computed(() => {
|
const storageObject = computed(() => {
|
||||||
return inProps.storages?.find(item => item.code === inProps.storage)
|
return inProps.storages?.find(item => item.code === inProps.storage)
|
||||||
})
|
})
|
||||||
@@ -69,19 +74,15 @@ function changeStorage(code: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 路径变化
|
// 路径变化
|
||||||
function changePath(_path: string, _fileid: string) {
|
function changePath(item: FileItem) {
|
||||||
emit('pathchanged', {
|
emit('pathchanged', item)
|
||||||
path: _path,
|
|
||||||
fileid: _fileid,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 返回上一级
|
// 返回上一级
|
||||||
function goUp() {
|
function goUp() {
|
||||||
const segments = pathSegments.value ?? []
|
const segments = pathSegments.value ?? []
|
||||||
const path = segments?.length === 1 ? '/' : segments[segments.length - 2].path
|
const fileitem = inProps.itemstack[segments.length - 1]
|
||||||
const fileid = segments?.length === 1 ? 'root' : segments[segments.length - 1].fileid
|
changePath(fileitem)
|
||||||
changePath(path, fileid)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建目录
|
// 创建目录
|
||||||
@@ -89,12 +90,12 @@ async function mkdir() {
|
|||||||
emit('loading', true)
|
emit('loading', true)
|
||||||
const url = inProps.endpoints?.mkdir.url
|
const url = inProps.endpoints?.mkdir.url
|
||||||
.replace(/{storage}/g, inProps.storage)
|
.replace(/{storage}/g, inProps.storage)
|
||||||
.replace(/{path}/g, encodeURIComponent(inProps.path + newFolderName.value))
|
.replace(/{name}/g, newFolderName.value)
|
||||||
.replace(/{fileid}/g, inProps.fileid || '')
|
|
||||||
|
|
||||||
const config = {
|
const config: AxiosRequestConfig<FileItem> = {
|
||||||
url,
|
url,
|
||||||
method: inProps.endpoints?.mkdir.method || 'post',
|
method: inProps.endpoints?.mkdir.method || 'post',
|
||||||
|
data: inProps.item,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 调API
|
// 调API
|
||||||
@@ -138,16 +139,17 @@ const sortIcon = computed(() => {
|
|||||||
</VListItem>
|
</VListItem>
|
||||||
</VList>
|
</VList>
|
||||||
</VMenu>
|
</VMenu>
|
||||||
<VBtn variant="text" :input-value="path === '/'" class="px-1" @click="changePath('/', 'root')">
|
<VBtn variant="text" :input-value="item.path === '/'" class="px-1" @click="changePath(inProps.itemstack[0])">
|
||||||
<VIcon :icon="storageObject?.icon" class="mr-2" />
|
<VIcon :icon="storageObject?.icon" class="mr-2" />
|
||||||
{{ storageObject?.name }}
|
{{ storageObject?.name }}
|
||||||
</VBtn>
|
</VBtn>
|
||||||
<template v-for="(segment, index) in pathSegments" :key="index">
|
<template v-for="(segment, index) in pathSegments" :key="index">
|
||||||
<VBtn
|
<VBtn
|
||||||
|
v-if="display.mdAndUp.value"
|
||||||
variant="text"
|
variant="text"
|
||||||
:input-value="index === pathSegments.length - 1"
|
:input-value="index === pathSegments.length - 1"
|
||||||
class="px-1 d-none d-md-block"
|
class="px-1"
|
||||||
@click="changePath(segment.path, inProps.fileidstack[index + 1])"
|
@click="changePath(inProps.itemstack[index + 1])"
|
||||||
>
|
>
|
||||||
<VIcon icon=" mdi-chevron-right" />
|
<VIcon icon=" mdi-chevron-right" />
|
||||||
{{ segment.name }}
|
{{ segment.name }}
|
||||||
@@ -180,13 +182,16 @@ const sortIcon = computed(() => {
|
|||||||
</IconBtn>
|
</IconBtn>
|
||||||
</template>
|
</template>
|
||||||
<VCard title="新建文件夹">
|
<VCard title="新建文件夹">
|
||||||
|
<DialogCloseBtn @click="newFolderPopper = false" />
|
||||||
|
<VDivider />
|
||||||
<VCardText>
|
<VCardText>
|
||||||
<VTextField v-model="newFolderName" label="名称" />
|
<VTextField v-model="newFolderName" label="名称" />
|
||||||
</VCardText>
|
</VCardText>
|
||||||
<VCardActions>
|
<VCardActions>
|
||||||
<div class="flex-grow-1" />
|
<div class="flex-grow-1" />
|
||||||
<VBtn depressed @click="newFolderPopper = false"> 取消 </VBtn>
|
<VBtn :disabled="!newFolderName" variant="elevated" @click="mkdir" prepend-icon="mdi-check" class="px-5 me-3">
|
||||||
<VBtn :disabled="!newFolderName" depressed variant="tonal" @click="mkdir"> 新建 </VBtn>
|
新建
|
||||||
|
</VBtn>
|
||||||
</VCardActions>
|
</VCardActions>
|
||||||
</VCard>
|
</VCard>
|
||||||
</VDialog>
|
</VDialog>
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ const props = defineProps({
|
|||||||
root: {
|
root: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '/',
|
default: '/',
|
||||||
required: true,
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -12,12 +12,18 @@ import MediaServerLibrary from '@/views/dashboard/MediaServerLibrary.vue'
|
|||||||
import MediaServerPlaying from '@/views/dashboard/MediaServerPlaying.vue'
|
import MediaServerPlaying from '@/views/dashboard/MediaServerPlaying.vue'
|
||||||
import DashboardRender from '@/components/render/DashboardRender.vue'
|
import DashboardRender from '@/components/render/DashboardRender.vue'
|
||||||
import { isNullOrEmptyObject } from '@/@core/utils'
|
import { isNullOrEmptyObject } from '@/@core/utils'
|
||||||
|
|
||||||
// 输入参数
|
// 输入参数
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
// 仪表板配置
|
// 仪表板配置
|
||||||
config: Object as PropType<DashboardItem>,
|
config: Object as PropType<DashboardItem>,
|
||||||
// 刷新状态
|
// 刷新状态
|
||||||
refreshStatus: Boolean,
|
refreshStatus: Boolean,
|
||||||
|
// 是否允许刷新数据
|
||||||
|
allowRefresh: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['update:refreshStatus'])
|
const emit = defineEmits(['update:refreshStatus'])
|
||||||
@@ -32,10 +38,10 @@ onUnmounted(() => {
|
|||||||
<AnalyticsStorage v-if="config?.id === 'storage'" />
|
<AnalyticsStorage v-if="config?.id === 'storage'" />
|
||||||
<AnalyticsMediaStatistic v-else-if="config?.id === 'mediaStatistic'" />
|
<AnalyticsMediaStatistic v-else-if="config?.id === 'mediaStatistic'" />
|
||||||
<AnalyticsWeeklyOverview v-else-if="config?.id === 'weeklyOverview'" />
|
<AnalyticsWeeklyOverview v-else-if="config?.id === 'weeklyOverview'" />
|
||||||
<AnalyticsSpeed v-else-if="config?.id === 'speed'" />
|
<AnalyticsSpeed v-else-if="config?.id === 'speed'" :allowRefresh="props.allowRefresh" />
|
||||||
<AnalyticsScheduler v-else-if="config?.id === 'scheduler'" />
|
<AnalyticsScheduler v-else-if="config?.id === 'scheduler'" :allowRefresh="props.allowRefresh" />
|
||||||
<AnalyticsCpu v-else-if="config?.id === 'cpu'" />
|
<AnalyticsCpu v-else-if="config?.id === 'cpu'" :allowRefresh="props.allowRefresh" />
|
||||||
<AnalyticsMemory v-else-if="config?.id === 'memory'" />
|
<AnalyticsMemory v-else-if="config?.id === 'memory'" :allowRefresh="props.allowRefresh" />
|
||||||
<MediaServerLibrary v-else-if="config?.id === 'library'" />
|
<MediaServerLibrary v-else-if="config?.id === 'library'" />
|
||||||
<MediaServerPlaying v-else-if="config?.id === 'playing'" />
|
<MediaServerPlaying v-else-if="config?.id === 'playing'" />
|
||||||
<MediaServerLatest v-else-if="config?.id === 'latest'" />
|
<MediaServerLatest v-else-if="config?.id === 'latest'" />
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import SlideViewTitle from '@/components/slide/SlideViewTitle.vue'
|
import SlideViewTitle from '@/components/slide/SlideViewTitle.vue'
|
||||||
|
import { useDisplay } from 'vuetify'
|
||||||
|
|
||||||
|
// 显示器宽度
|
||||||
|
const display = useDisplay()
|
||||||
|
|
||||||
// 元素
|
// 元素
|
||||||
const slideview_content = ref()
|
const slideview_content = ref()
|
||||||
@@ -91,7 +95,7 @@ onActivated(() => {
|
|||||||
<slot name="title">
|
<slot name="title">
|
||||||
<SlideViewTitle />
|
<SlideViewTitle />
|
||||||
</slot>
|
</slot>
|
||||||
<div v-if="disabled !== 3" class="me-1 d-none d-md-flex">
|
<div v-if="disabled !== 3 && display.mdAndUp.value" class="me-1 d-flex">
|
||||||
<VBtn
|
<VBtn
|
||||||
class="rounded-circle"
|
class="rounded-circle"
|
||||||
variant="text"
|
variant="text"
|
||||||
@@ -122,8 +126,8 @@ onActivated(() => {
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.slideview_content {
|
.slideview_content {
|
||||||
-ms-overflow-style: none !important;
|
|
||||||
overflow: scroll hidden !important;
|
overflow: scroll hidden !important;
|
||||||
|
-ms-overflow-style: none !important;
|
||||||
overscroll-behavior-x: contain !important;
|
overscroll-behavior-x: contain !important;
|
||||||
scrollbar-width: none !important;
|
scrollbar-width: none !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import * as Mousetrap from 'mousetrap'
|
import * as Mousetrap from 'mousetrap'
|
||||||
import SearchBarView from '@/views/system/SearchBarView.vue'
|
import SearchBarView from '@/views/system/SearchBarView.vue'
|
||||||
import { useDisplay } from 'vuetify'
|
import { useDisplay } from 'vuetify'
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
const display = useDisplay()
|
const display = useDisplay()
|
||||||
|
|
||||||
@@ -15,6 +16,14 @@ function openSearchDialog() {
|
|||||||
searchDialog.value = true
|
searchDialog.value = true
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检测操作系统是否是Mac
|
||||||
|
function isMac() {
|
||||||
|
return navigator.platform.toUpperCase().indexOf('MAC') >= 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算属性:根据操作系统显示不同的按键提示
|
||||||
|
const metaKey = computed(() => (isMac() ? '⌘+K' : 'Ctrl+K'))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -25,12 +34,13 @@ function openSearchDialog() {
|
|||||||
</IconBtn>
|
</IconBtn>
|
||||||
<span v-if="display.lgAndUp.value" class="flex align-center text-disabled ms-2" @click="openSearchDialog">
|
<span v-if="display.lgAndUp.value" class="flex align-center text-disabled ms-2" @click="openSearchDialog">
|
||||||
<span class="me-3">搜索</span>
|
<span class="me-3">搜索</span>
|
||||||
<span class="meta-key">⌘K</span>
|
<span class="meta-key">{{ metaKey }}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- 搜索弹窗 -->
|
<!-- 搜索弹窗 -->
|
||||||
<SearchBarView v-model="searchDialog" v-if="searchDialog" @close="searchDialog = false" />
|
<SearchBarView v-model="searchDialog" v-if="searchDialog" @close="searchDialog = false" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style type="scss" scoped>
|
<style type="scss" scoped>
|
||||||
.meta-key {
|
.meta-key {
|
||||||
border: thin solid rgba(var(--v-border-color), var(--v-border-opacity));
|
border: thin solid rgba(var(--v-border-color), var(--v-border-opacity));
|
||||||
@@ -39,4 +49,4 @@ function openSearchDialog() {
|
|||||||
padding-block: 0.1rem;
|
padding-block: 0.1rem;
|
||||||
padding-inline: 0.25rem;
|
padding-inline: 0.25rem;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -25,9 +25,7 @@ const progressDialog = ref(false)
|
|||||||
// 执行注销操作
|
// 执行注销操作
|
||||||
function logout() {
|
function logout() {
|
||||||
// 清除登录状态信息
|
// 清除登录状态信息
|
||||||
store.dispatch('auth/clearToken')
|
store.dispatch('auth/logout')
|
||||||
// 主动登出时清除路由标记
|
|
||||||
store.state.auth.originalPath = null
|
|
||||||
// 重定向到登录页面或其他适当的页面
|
// 重定向到登录页面或其他适当的页面
|
||||||
router.push('/login')
|
router.push('/login')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ const superUser = store.state.auth.superUser
|
|||||||
// 是否拉升高度
|
// 是否拉升高度
|
||||||
const isElevated = ref(true)
|
const isElevated = ref(true)
|
||||||
|
|
||||||
|
// 是否发送请求的总开关
|
||||||
|
const isRequest = ref(true)
|
||||||
|
|
||||||
// 计算属性,控制是否拉升高度
|
// 计算属性,控制是否拉升高度
|
||||||
const elevatedConf = controlledComputed(
|
const elevatedConf = controlledComputed(
|
||||||
() => isElevated.value,
|
() => isElevated.value,
|
||||||
@@ -269,7 +272,8 @@ async function getPluginDashboard(id: string, key: string) {
|
|||||||
if (
|
if (
|
||||||
res.attrs?.refresh &&
|
res.attrs?.refresh &&
|
||||||
pluginDashboardRefreshStatus.value[pluginDashboardId] &&
|
pluginDashboardRefreshStatus.value[pluginDashboardId] &&
|
||||||
enableConfig.value[pluginDashboardId]
|
enableConfig.value[pluginDashboardId] &&
|
||||||
|
isRequest.value
|
||||||
) {
|
) {
|
||||||
// 清除之前的定时器
|
// 清除之前的定时器
|
||||||
if (refreshTimers.value[pluginDashboardId]) {
|
if (refreshTimers.value[pluginDashboardId]) {
|
||||||
@@ -298,6 +302,14 @@ onBeforeMount(async () => {
|
|||||||
await loadDashboardConfig()
|
await loadDashboardConfig()
|
||||||
getPluginDashboardMeta()
|
getPluginDashboardMeta()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onActivated(async () => {
|
||||||
|
isRequest.value = true
|
||||||
|
})
|
||||||
|
|
||||||
|
onDeactivated(() => {
|
||||||
|
isRequest.value = false
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -314,6 +326,7 @@ onBeforeMount(async () => {
|
|||||||
<VCol v-if="enableConfig[buildPluginDashboardId(element.id, element.key)] && element.cols" v-bind:="element.cols">
|
<VCol v-if="enableConfig[buildPluginDashboardId(element.id, element.key)] && element.cols" v-bind:="element.cols">
|
||||||
<DashboardElement
|
<DashboardElement
|
||||||
:config="element"
|
:config="element"
|
||||||
|
:allow-refresh="isRequest"
|
||||||
v-model:refreshStatus="pluginDashboardRefreshStatus[buildPluginDashboardId(element.id, element.key)]"
|
v-model:refreshStatus="pluginDashboardRefreshStatus[buildPluginDashboardId(element.id, element.key)]"
|
||||||
/>
|
/>
|
||||||
</VCol>
|
</VCol>
|
||||||
|
|||||||
@@ -158,19 +158,17 @@ function login() {
|
|||||||
.then((response: any) => {
|
.then((response: any) => {
|
||||||
// 获取token
|
// 获取token
|
||||||
const token = response.access_token
|
const token = response.access_token
|
||||||
const superuser = response.super_user
|
const superUser = response.super_user
|
||||||
const username = response.user_name
|
const userName = response.user_name
|
||||||
const avatar = response.avatar
|
const avatar = response.avatar
|
||||||
|
const level = response.level
|
||||||
|
const remember = form.value.remember
|
||||||
|
|
||||||
// 更新token和remember状态到Vuex Store
|
// 更新token和remember状态到Vuex Store
|
||||||
store.dispatch('auth/updateToken', token)
|
store.dispatch('auth/login', { token, remember, superUser, userName, avatar, level })
|
||||||
store.dispatch('auth/updateRemember', form.value.remember)
|
|
||||||
store.dispatch('auth/updateSuperUser', superuser)
|
|
||||||
store.dispatch('auth/updateUserName', username)
|
|
||||||
store.dispatch('auth/updateAvatar', avatar)
|
|
||||||
|
|
||||||
// 登录后处理
|
// 登录后处理
|
||||||
afterLogin(superuser)
|
afterLogin(superUser)
|
||||||
})
|
})
|
||||||
.catch((error: any) => {
|
.catch((error: any) => {
|
||||||
// 登录失败,显示错误提示
|
// 登录失败,显示错误提示
|
||||||
|
|||||||
@@ -32,8 +32,10 @@ function jumpTab(tab: string) {
|
|||||||
@click="jumpTab(item.tab)"
|
@click="jumpTab(item.tab)"
|
||||||
selected-class="v-slide-group-item--active v-tab--selected"
|
selected-class="v-slide-group-item--active v-tab--selected"
|
||||||
>
|
>
|
||||||
<VIcon size="20" start :icon="item.icon" />
|
<div>
|
||||||
{{ item.title }}
|
<VIcon size="20" start :icon="item.icon" />
|
||||||
|
{{ item.title }}
|
||||||
|
</div>
|
||||||
</VTab>
|
</VTab>
|
||||||
</VTabs>
|
</VTabs>
|
||||||
|
|
||||||
@@ -41,70 +43,90 @@ function jumpTab(tab: string) {
|
|||||||
<!-- 用户 -->
|
<!-- 用户 -->
|
||||||
<VWindowItem value="account">
|
<VWindowItem value="account">
|
||||||
<transition name="fade-slide" appear>
|
<transition name="fade-slide" appear>
|
||||||
<AccountSettingAccount />
|
<div>
|
||||||
|
<AccountSettingAccount />
|
||||||
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
</VWindowItem>
|
</VWindowItem>
|
||||||
|
|
||||||
<!-- 连接 -->
|
<!-- 连接 -->
|
||||||
<VWindowItem value="system">
|
<VWindowItem value="system">
|
||||||
<transition name="fade-slide" appear>
|
<transition name="fade-slide" appear>
|
||||||
<AccountSettingSystem />
|
<div>
|
||||||
|
<AccountSettingSystem />
|
||||||
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
</VWindowItem>
|
</VWindowItem>
|
||||||
|
|
||||||
<!-- 目录 -->
|
<!-- 目录 -->
|
||||||
<VWindowItem value="directory">
|
<VWindowItem value="directory">
|
||||||
<transition name="fade-slide" appear>
|
<transition name="fade-slide" appear>
|
||||||
<AccountSettingDirectory />
|
<div>
|
||||||
|
<AccountSettingDirectory />
|
||||||
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
</VWindowItem>
|
</VWindowItem>
|
||||||
|
|
||||||
<!-- 站点 -->
|
<!-- 站点 -->
|
||||||
<VWindowItem value="site">
|
<VWindowItem value="site">
|
||||||
<transition name="fade-slide" appear>
|
<transition name="fade-slide" appear>
|
||||||
<AccountSettingSite />
|
<div>
|
||||||
|
<AccountSettingSite />
|
||||||
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
</VWindowItem>
|
</VWindowItem>
|
||||||
|
|
||||||
<!-- 搜索 -->
|
<!-- 搜索 -->
|
||||||
<VWindowItem value="search">
|
<VWindowItem value="search">
|
||||||
<transition name="fade-slide" appear>
|
<transition name="fade-slide" appear>
|
||||||
<AccountSettingSearch />
|
<div>
|
||||||
|
<AccountSettingSearch />
|
||||||
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
</VWindowItem>
|
</VWindowItem>
|
||||||
|
|
||||||
<!-- 订阅 -->
|
<!-- 订阅 -->
|
||||||
<VWindowItem value="subscribe">
|
<VWindowItem value="subscribe">
|
||||||
<transition name="fade-slide" appear>
|
<transition name="fade-slide" appear>
|
||||||
<AccountSettingSubscribe />
|
<div>
|
||||||
|
<AccountSettingSubscribe />
|
||||||
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
</VWindowItem>
|
</VWindowItem>
|
||||||
|
|
||||||
<!-- 服务 -->
|
<!-- 服务 -->
|
||||||
<VWindowItem value="service">
|
<VWindowItem value="service">
|
||||||
<transition name="fade-slide" appear>
|
<transition name="fade-slide" appear>
|
||||||
<AccountSettingService />
|
<div>
|
||||||
|
<AccountSettingService />
|
||||||
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
</VWindowItem>
|
</VWindowItem>
|
||||||
|
|
||||||
<!-- 通知 -->
|
<!-- 通知 -->
|
||||||
<VWindowItem value="notification">
|
<VWindowItem value="notification">
|
||||||
<transition name="fade-slide" appear>
|
<transition name="fade-slide" appear>
|
||||||
<AccountSettingNotification />
|
<div>
|
||||||
|
<AccountSettingNotification />
|
||||||
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
</VWindowItem>
|
</VWindowItem>
|
||||||
|
|
||||||
<!-- 词表 -->
|
<!-- 词表 -->
|
||||||
<VWindowItem value="words">
|
<VWindowItem value="words">
|
||||||
<transition name="fade-slide" appear>
|
<transition name="fade-slide" appear>
|
||||||
<AccountSettingWords />
|
<div>
|
||||||
|
<AccountSettingWords />
|
||||||
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
</VWindowItem>
|
</VWindowItem>
|
||||||
|
|
||||||
<!-- 关于 -->
|
<!-- 关于 -->
|
||||||
<VWindowItem value="about">
|
<VWindowItem value="about">
|
||||||
<transition name="fade-slide" appear>
|
<transition name="fade-slide" appear>
|
||||||
<AccountSettingAbout />
|
<div>
|
||||||
|
<AccountSettingAbout />
|
||||||
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
</VWindowItem>
|
</VWindowItem>
|
||||||
</VWindow>
|
</VWindow>
|
||||||
|
|||||||
@@ -144,6 +144,7 @@ const router = createRouter({
|
|||||||
path: '/filemanager',
|
path: '/filemanager',
|
||||||
component: () => import('../pages/filemanager.vue'),
|
component: () => import('../pages/filemanager.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
|
keepAlive: true,
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ interface AuthState {
|
|||||||
userName: string
|
userName: string
|
||||||
avatar: string
|
avatar: string
|
||||||
originalPath: string | null
|
originalPath: string | null
|
||||||
|
level: number
|
||||||
}
|
}
|
||||||
|
|
||||||
// 定义根状态类型
|
// 定义根状态类型
|
||||||
@@ -25,6 +26,7 @@ const authModule: Module<AuthState, RootState> = {
|
|||||||
userName: '',
|
userName: '',
|
||||||
avatar: '',
|
avatar: '',
|
||||||
originalPath: null,
|
originalPath: null,
|
||||||
|
level: 1,
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
setToken(state, token: string) {
|
setToken(state, token: string) {
|
||||||
@@ -45,25 +47,25 @@ const authModule: Module<AuthState, RootState> = {
|
|||||||
setAvatar(state, avatar: string) {
|
setAvatar(state, avatar: string) {
|
||||||
state.avatar = avatar
|
state.avatar = avatar
|
||||||
},
|
},
|
||||||
|
setOriginalPath(state, originalPath: string) {
|
||||||
|
state.originalPath = originalPath
|
||||||
|
},
|
||||||
|
setLevel(state, level: number) {
|
||||||
|
state.level = level
|
||||||
|
},
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
updateToken({ commit }, token: string) {
|
login({ commit }, { token, remember, superUser, userName, avatar, level }) {
|
||||||
commit('setToken', token)
|
commit('setToken', token)
|
||||||
},
|
|
||||||
clearToken({ commit }) {
|
|
||||||
commit('clearToken')
|
|
||||||
},
|
|
||||||
updateRemember({ commit }, remember: boolean) {
|
|
||||||
commit('setRemember', remember)
|
commit('setRemember', remember)
|
||||||
},
|
|
||||||
updateSuperUser({ commit }, superUser: boolean) {
|
|
||||||
commit('setSuperUser', superUser)
|
commit('setSuperUser', superUser)
|
||||||
},
|
|
||||||
updateUserName({ commit }, userName: string) {
|
|
||||||
commit('setUserName', userName)
|
commit('setUserName', userName)
|
||||||
},
|
|
||||||
updateAvatar({ commit }, avatar: string) {
|
|
||||||
commit('setAvatar', avatar)
|
commit('setAvatar', avatar)
|
||||||
|
commit('setLevel', level)
|
||||||
|
},
|
||||||
|
logout({ commit }) {
|
||||||
|
commit('clearToken')
|
||||||
|
commit('setOriginalPath', null)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
@@ -72,6 +74,8 @@ const authModule: Module<AuthState, RootState> = {
|
|||||||
getSuperUser: state => state.superUser,
|
getSuperUser: state => state.superUser,
|
||||||
getUserName: state => state.userName,
|
getUserName: state => state.userName,
|
||||||
getAvatar: state => state.avatar,
|
getAvatar: state => state.avatar,
|
||||||
|
getOriginalPath: state => state.originalPath,
|
||||||
|
getLevel: state => state.level,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -156,7 +156,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.grid-plugin-card {
|
.grid-plugin-card {
|
||||||
grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
|
||||||
padding-block-end: 1rem;
|
padding-block-end: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,15 @@ import { useTheme } from 'vuetify'
|
|||||||
import { hexToRgb } from '@layouts/utils'
|
import { hexToRgb } from '@layouts/utils'
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
|
|
||||||
|
// 输入参数
|
||||||
|
const props = defineProps({
|
||||||
|
// 是否允许刷新数据
|
||||||
|
allowRefresh: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const vuetifyTheme = useTheme()
|
const vuetifyTheme = useTheme()
|
||||||
|
|
||||||
const currentTheme = controlledComputed(
|
const currentTheme = controlledComputed(
|
||||||
@@ -94,6 +103,7 @@ const chartOptions = controlledComputed(
|
|||||||
|
|
||||||
// 调用API接口获取最新CPU使用率
|
// 调用API接口获取最新CPU使用率
|
||||||
async function getCpuUsage() {
|
async function getCpuUsage() {
|
||||||
|
if (!props.allowRefresh) return
|
||||||
try {
|
try {
|
||||||
// 请求数据
|
// 请求数据
|
||||||
current.value = (await api.get('dashboard/cpu')) ?? 0
|
current.value = (await api.get('dashboard/cpu')) ?? 0
|
||||||
|
|||||||
@@ -5,6 +5,15 @@ import { hexToRgb } from '@layouts/utils'
|
|||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import { formatBytes } from '@/@core/utils/formatters'
|
import { formatBytes } from '@/@core/utils/formatters'
|
||||||
|
|
||||||
|
// 输入参数
|
||||||
|
const props = defineProps({
|
||||||
|
// 是否允许刷新数据
|
||||||
|
allowRefresh: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const vuetifyTheme = useTheme()
|
const vuetifyTheme = useTheme()
|
||||||
|
|
||||||
const currentTheme = controlledComputed(
|
const currentTheme = controlledComputed(
|
||||||
@@ -100,6 +109,7 @@ const chartOptions = controlledComputed(
|
|||||||
|
|
||||||
// 调用API接口获取最新内存使用量
|
// 调用API接口获取最新内存使用量
|
||||||
async function getMemorgUsage() {
|
async function getMemorgUsage() {
|
||||||
|
if (!props.allowRefresh) return
|
||||||
try {
|
try {
|
||||||
// 请求数据
|
// 请求数据
|
||||||
;[usedMemory.value, memoryUsage.value] = await api.get('dashboard/memory')
|
;[usedMemory.value, memoryUsage.value] = await api.get('dashboard/memory')
|
||||||
|
|||||||
@@ -2,6 +2,15 @@
|
|||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import type { ScheduleInfo } from '@/api/types'
|
import type { ScheduleInfo } from '@/api/types'
|
||||||
|
|
||||||
|
// 输入参数
|
||||||
|
const props = defineProps({
|
||||||
|
// 是否允许刷新数据
|
||||||
|
allowRefresh: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
// 定时服务列表
|
// 定时服务列表
|
||||||
const schedulerList = ref<ScheduleInfo[]>([])
|
const schedulerList = ref<ScheduleInfo[]>([])
|
||||||
|
|
||||||
@@ -10,6 +19,9 @@ let refreshTimer: NodeJS.Timeout | null = null
|
|||||||
|
|
||||||
// 调用API加载定时服务列表
|
// 调用API加载定时服务列表
|
||||||
async function loadSchedulerList() {
|
async function loadSchedulerList() {
|
||||||
|
if (!props.allowRefresh) {
|
||||||
|
return
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const res: ScheduleInfo[] = await api.get('dashboard/schedule')
|
const res: ScheduleInfo[] = await api.get('dashboard/schedule')
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,15 @@ import { formatFileSize } from '@/@core/utils/formatters'
|
|||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import type { DownloaderInfo } from '@/api/types'
|
import type { DownloaderInfo } from '@/api/types'
|
||||||
|
|
||||||
|
// 输入参数
|
||||||
|
const props = defineProps({
|
||||||
|
// 是否允许刷新数据
|
||||||
|
allowRefresh: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
// 定时器
|
// 定时器
|
||||||
let refreshTimer: NodeJS.Timeout | null = null
|
let refreshTimer: NodeJS.Timeout | null = null
|
||||||
|
|
||||||
@@ -35,6 +44,10 @@ const infoItems = ref([
|
|||||||
|
|
||||||
// 调用API查询下载器数据
|
// 调用API查询下载器数据
|
||||||
async function loadDownloaderInfo() {
|
async function loadDownloaderInfo() {
|
||||||
|
if (!props.allowRefresh) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res: DownloaderInfo = await api.get('dashboard/downloader')
|
const res: DownloaderInfo = await api.get('dashboard/downloader')
|
||||||
|
|
||||||
|
|||||||
@@ -80,7 +80,13 @@ const options = controlledComputed(
|
|||||||
fontSize: '12px',
|
fontSize: '12px',
|
||||||
},
|
},
|
||||||
|
|
||||||
formatter: (value: number) => (value > 999 ? (value / 1000).toFixed(0) : value),
|
formatter: (value: number) => {
|
||||||
|
if (value > 999) {
|
||||||
|
return (value / 1000).toFixed(1) + 'k'
|
||||||
|
} else {
|
||||||
|
return value.toString()
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { Context } from '@/api/types'
|
import type { Context } from '@/api/types'
|
||||||
import TorrentItem from '@/components/cards/TorrentItem.vue'
|
import TorrentItem from '@/components/cards/TorrentItem.vue'
|
||||||
|
import { list } from 'postcss'
|
||||||
import { useDisplay } from 'vuetify'
|
import { useDisplay } from 'vuetify'
|
||||||
|
|
||||||
// 显示器宽度
|
// 显示器宽度
|
||||||
@@ -35,6 +36,13 @@ const filterForm = reactive({
|
|||||||
resolution: [] as string[],
|
resolution: [] as string[],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 列表样式
|
||||||
|
const listStyle = computed(() => {
|
||||||
|
return appMode.value
|
||||||
|
? 'height: calc(100vh - 7.5rem - env(safe-area-inset-bottom) - 3.5rem)'
|
||||||
|
: 'height: calc(100vh - 6.5rem - env(safe-area-inset-bottom)'
|
||||||
|
})
|
||||||
|
|
||||||
// 排序字段
|
// 排序字段
|
||||||
const sortField = ref('default')
|
const sortField = ref('default')
|
||||||
|
|
||||||
@@ -71,7 +79,6 @@ function initOptions(data: Context) {
|
|||||||
optionValue(resolutionFilterOptions.value, meta_info?.resource_pix)
|
optionValue(resolutionFilterOptions.value, meta_info?.resource_pix)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 对季过滤选项进行排序
|
// 对季过滤选项进行排序
|
||||||
const sortSeasonFilterOptions = computed(() => {
|
const sortSeasonFilterOptions = computed(() => {
|
||||||
return seasonFilterOptions.value.sort((a, b) => {
|
return seasonFilterOptions.value.sort((a, b) => {
|
||||||
@@ -168,26 +175,15 @@ onMounted(() => {
|
|||||||
</VListItem>
|
</VListItem>
|
||||||
</VList>
|
</VList>
|
||||||
<VList v-if="dataList.length !== 0" lines="three" class="rounded p-0 torrent-list-vscroll shadow-lg">
|
<VList v-if="dataList.length !== 0" lines="three" class="rounded p-0 torrent-list-vscroll shadow-lg">
|
||||||
<VVirtualScroll
|
<VVirtualScroll :items="dataList" :style="listStyle">
|
||||||
:items="dataList"
|
|
||||||
:style="
|
|
||||||
appMode
|
|
||||||
? 'height: calc(100vh - 7.5rem - env(safe-area-inset-bottom) - 3.5rem)'
|
|
||||||
: 'height: calc(100vh - 6.5rem - env(safe-area-inset-bottom)'
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<template #default="{ item }">
|
<template #default="{ item }">
|
||||||
<TorrentItem :torrent="item" :key="`${item.torrent_info.page_url}`" />
|
<TorrentItem :torrent="item" :key="`${item.torrent_info.page_url}`" />
|
||||||
</template>
|
</template>
|
||||||
</VVirtualScroll>
|
</VVirtualScroll>
|
||||||
</VList>
|
</VList>
|
||||||
</VCol>
|
</VCol>
|
||||||
<VCol xl="2" md="3" class="d-none d-md-block">
|
<VCol xl="2" md="3" v-if="display.mdAndUp.value">
|
||||||
<VList
|
<VList lines="one" class="rounded shadow-lg" :style="listStyle">
|
||||||
lines="one"
|
|
||||||
class="rounded shadow-lg"
|
|
||||||
style="block-size: calc(100vh - 6.5rem - env(safe-area-inset-bottom))"
|
|
||||||
>
|
|
||||||
<VListSubheader> 排序 </VListSubheader>
|
<VListSubheader> 排序 </VListSubheader>
|
||||||
<VListItem>
|
<VListItem>
|
||||||
<VChipGroup column v-model="sortField">
|
<VChipGroup column v-model="sortField">
|
||||||
|
|||||||
@@ -347,7 +347,7 @@ onBeforeMount(async () => {
|
|||||||
<transition name="fade-slide" appear>
|
<transition name="fade-slide" appear>
|
||||||
<div>
|
<div>
|
||||||
<LoadingBanner v-if="!isRefreshed" class="mt-12" />
|
<LoadingBanner v-if="!isRefreshed" class="mt-12" />
|
||||||
<div v-if="dataList.length > 0" class="grid gap-4 grid-plugin-card items-start">
|
<div v-if="dataList.length > 0" class="grid gap-4 grid-plugin-card">
|
||||||
<template v-for="(data, index) in dataList" :key="`${data.id}_v${data.plugin_version}`">
|
<template v-for="(data, index) in dataList" :key="`${data.id}_v${data.plugin_version}`">
|
||||||
<PluginCard
|
<PluginCard
|
||||||
:count="PluginStatistics[data.id || '0']"
|
:count="PluginStatistics[data.id || '0']"
|
||||||
@@ -417,7 +417,7 @@ onBeforeMount(async () => {
|
|||||||
</VCol>
|
</VCol>
|
||||||
</VRow>
|
</VRow>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="isAppMarketLoaded" class="grid gap-4 grid-plugin-card items-start">
|
<div v-if="isAppMarketLoaded" class="grid gap-4 grid-plugin-card">
|
||||||
<template v-for="(data, index) in sortedUninstalledList" :key="`${data.id}_v${data.plugin_version}`">
|
<template v-for="(data, index) in sortedUninstalledList" :key="`${data.id}_v${data.plugin_version}`">
|
||||||
<PluginAppCard :plugin="data" :count="PluginStatistics[data.id || '0']" @install="pluginInstalled" />
|
<PluginAppCard :plugin="data" :count="PluginStatistics[data.id || '0']" @install="pluginInstalled" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -2,45 +2,57 @@
|
|||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import { FileItem, MediaDirectory } from '@/api/types'
|
import { FileItem, MediaDirectory } from '@/api/types'
|
||||||
import FileBrowser from '@/components/FileBrowser.vue'
|
import FileBrowser from '@/components/FileBrowser.vue'
|
||||||
|
import store from '@/store'
|
||||||
|
|
||||||
const endpoints = {
|
const endpoints = {
|
||||||
list: {
|
list: {
|
||||||
url: '/{storage}/list?path={path}&sort={sort}&fileid={fileid}&filetype={filetype}&pickcode={pickcode}',
|
url: '/{storage}/list?sort={sort}',
|
||||||
method: 'get',
|
method: 'post',
|
||||||
},
|
},
|
||||||
mkdir: {
|
mkdir: {
|
||||||
url: '/{storage}/mkdir?path={path}&fileid={fileid}',
|
url: '/{storage}/mkdir?name={name}',
|
||||||
method: 'get',
|
method: 'post',
|
||||||
},
|
},
|
||||||
delete: {
|
delete: {
|
||||||
url: '/{storage}/delete?path={path}&fileid={fileid}',
|
url: '/{storage}/delete',
|
||||||
method: 'get',
|
method: 'post',
|
||||||
},
|
},
|
||||||
download: {
|
download: {
|
||||||
url: '/{storage}/download?path={path}&fileid={fileid}&pickcode={pickcode}',
|
url: '/{storage}/download',
|
||||||
method: 'get',
|
method: 'get',
|
||||||
},
|
},
|
||||||
image: {
|
image: {
|
||||||
url: '/{storage}/image?path={path}&fileid={fileid}&pickcode={pickcode}',
|
url: '/{storage}/image',
|
||||||
method: 'get',
|
method: 'get',
|
||||||
},
|
},
|
||||||
rename: {
|
rename: {
|
||||||
url: '/{storage}/rename?path={path}&new_name={newname}&fileid={fileid}&filetype={filetype}',
|
url: '/{storage}/rename?new_name={newname}',
|
||||||
method: 'get',
|
method: 'post',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// 当前目录
|
const user_level = store.state.auth.level
|
||||||
const path = ref<string>('')
|
|
||||||
|
|
||||||
// 当前fileid
|
// 用户存储
|
||||||
const fileid = ref<string>('root')
|
const userStorage = user_level > 1 ? 'local,aliyun,u115' : 'local'
|
||||||
|
|
||||||
// 当前pickcode
|
// 当前文件项
|
||||||
const pickcode = ref<string>('')
|
const operItem = ref<FileItem>({
|
||||||
|
type: 'dir',
|
||||||
|
name: '/',
|
||||||
|
path: '/',
|
||||||
|
fileid: 'root',
|
||||||
|
})
|
||||||
|
|
||||||
// fileid的堆栈
|
// fileid的堆栈
|
||||||
const fileidstack = ref<string[]>(['root'])
|
const itemstack = ref<FileItem[]>([
|
||||||
|
{
|
||||||
|
type: 'dir',
|
||||||
|
name: '/',
|
||||||
|
path: '/',
|
||||||
|
fileid: 'root',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
// 下载目录列表
|
// 下载目录列表
|
||||||
const downloadDirectories = ref<MediaDirectory[]>([])
|
const downloadDirectories = ref<MediaDirectory[]>([])
|
||||||
@@ -73,7 +85,7 @@ function findCommonPath(paths: string[]): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (commonPath.includes(':')) {
|
if (commonPath.includes(':')) {
|
||||||
commonPath = commonPath.replace('/', '\\')
|
commonPath = commonPath.replace('\\', '/')
|
||||||
}
|
}
|
||||||
|
|
||||||
return commonPath
|
return commonPath
|
||||||
@@ -85,7 +97,23 @@ async function loadDownloadDirectories() {
|
|||||||
const result: { [key: string]: any } = await api.get('system/setting/DownloadDirectories')
|
const result: { [key: string]: any } = await api.get('system/setting/DownloadDirectories')
|
||||||
if (result.success && result.data?.value) {
|
if (result.success && result.data?.value) {
|
||||||
downloadDirectories.value = result.data.value
|
downloadDirectories.value = result.data.value
|
||||||
path.value = findCommonPath(downloadDirectories.value.map(item => item.path) as string[])
|
const path = findCommonPath(downloadDirectories.value.map(item => item.path) as string[])
|
||||||
|
const name = path.split('/').filter(Boolean).pop() ?? ''
|
||||||
|
operItem.value = {
|
||||||
|
type: 'dir',
|
||||||
|
name: name,
|
||||||
|
path: path,
|
||||||
|
}
|
||||||
|
// 将初始数据拆分到堆栈中
|
||||||
|
const paths = path.split('/').filter(Boolean)
|
||||||
|
paths.map((name, index) => {
|
||||||
|
const path = '/' + paths.slice(0, index + 1).join('/') + '/'
|
||||||
|
itemstack.value.push({
|
||||||
|
type: 'dir',
|
||||||
|
name: name,
|
||||||
|
path: path,
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
@@ -94,32 +122,28 @@ async function loadDownloadDirectories() {
|
|||||||
|
|
||||||
// 目录变化
|
// 目录变化
|
||||||
function pathChanged(item: FileItem) {
|
function pathChanged(item: FileItem) {
|
||||||
path.value = item.path
|
operItem.value = item
|
||||||
pickcode.value = item.pickcode || ''
|
const index = itemstack.value.findIndex(i => i.path === item.path)
|
||||||
if (item.fileid) {
|
if (index >= 0) {
|
||||||
fileid.value = item.fileid
|
itemstack.value = itemstack.value.slice(0, index + 1)
|
||||||
if (fileidstack.value.includes(item.fileid)) {
|
} else {
|
||||||
fileidstack.value = fileidstack.value.slice(0, fileidstack.value.indexOf(item.fileid) + 1)
|
itemstack.value.push(item)
|
||||||
} else {
|
|
||||||
fileidstack.value.push(item.fileid)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 加载初始目录
|
||||||
onBeforeMount(loadDownloadDirectories)
|
onBeforeMount(loadDownloadDirectories)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<FileBrowser
|
<FileBrowser
|
||||||
storages="local,aliyun,u115"
|
:storages="userStorage"
|
||||||
:tree="false"
|
:tree="false"
|
||||||
:path="path"
|
:itemstack="itemstack"
|
||||||
:fileid="fileid"
|
|
||||||
:pickcode="pickcode"
|
|
||||||
:fileidstack="fileidstack"
|
|
||||||
:endpoints="endpoints"
|
:endpoints="endpoints"
|
||||||
:axios="api"
|
:axios="api"
|
||||||
|
:item="operItem"
|
||||||
@pathchanged="pathChanged"
|
@pathchanged="pathChanged"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -124,6 +124,12 @@ const TransferDict: { [key: string]: string } = {
|
|||||||
rclone_move: 'Rclone移动',
|
rclone_move: 'Rclone移动',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tableStyle = computed(() => {
|
||||||
|
return appMode.value
|
||||||
|
? 'height: calc(100vh - 15.5rem - env(safe-area-inset-bottom) - 3.5rem)'
|
||||||
|
: 'height: calc(100vh - 14.5rem - env(safe-area-inset-bottom)'
|
||||||
|
})
|
||||||
|
|
||||||
// 分页提示
|
// 分页提示
|
||||||
const pageTip = computed(() => {
|
const pageTip = computed(() => {
|
||||||
const begin = itemsPerPage.value * (currentPage.value - 1) + 1
|
const begin = itemsPerPage.value * (currentPage.value - 1) + 1
|
||||||
@@ -142,12 +148,19 @@ const totalPage = computed(() => {
|
|||||||
|
|
||||||
// 切换页签和搜索词
|
// 切换页签和搜索词
|
||||||
watch(
|
watch(
|
||||||
[() => currentPage.value, () => itemsPerPage.value, () => search.value],
|
[() => currentPage.value, () => itemsPerPage.value],
|
||||||
debounce(async () => {
|
debounce(async () => {
|
||||||
reloadPage()
|
reloadPage()
|
||||||
}, 1000),
|
}, 1000),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
[() => search.value],
|
||||||
|
debounce(async () => {
|
||||||
|
reloadPage(true)
|
||||||
|
}, 1000),
|
||||||
|
)
|
||||||
|
|
||||||
// 获取订阅列表数据
|
// 获取订阅列表数据
|
||||||
async function fetchData(page = currentPage.value, count = itemsPerPage.value) {
|
async function fetchData(page = currentPage.value, count = itemsPerPage.value) {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
@@ -329,7 +342,7 @@ function addUrlQuery(url: string, name: string, value: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 重载页面
|
// 重载页面
|
||||||
function reloadPage() {
|
function reloadPage(resetPage = false) {
|
||||||
let url = '/history'
|
let url = '/history'
|
||||||
if (search.value) {
|
if (search.value) {
|
||||||
url = addUrlQuery(url, 'search', search.value)
|
url = addUrlQuery(url, 'search', search.value)
|
||||||
@@ -338,7 +351,7 @@ function reloadPage() {
|
|||||||
url = addUrlQuery(url, 'itemsPerPage', itemsPerPage.value)
|
url = addUrlQuery(url, 'itemsPerPage', itemsPerPage.value)
|
||||||
}
|
}
|
||||||
if (currentPage.value) {
|
if (currentPage.value) {
|
||||||
url = addUrlQuery(url, 'currentPage', currentPage.value)
|
url = addUrlQuery(url, 'currentPage', resetPage ? 1 : currentPage.value)
|
||||||
}
|
}
|
||||||
router.push(url)
|
router.push(url)
|
||||||
}
|
}
|
||||||
@@ -347,7 +360,7 @@ function reloadPage() {
|
|||||||
function ensureNumber(value: any, defaultValue: number = 0) {
|
function ensureNumber(value: any, defaultValue: number = 0) {
|
||||||
value = Number(value)
|
value = Number(value)
|
||||||
// 如果不是数字
|
// 如果不是数字
|
||||||
if (value !== value) {
|
if (Number.isNaN(value)) {
|
||||||
value = defaultValue
|
value = defaultValue
|
||||||
}
|
}
|
||||||
return value
|
return value
|
||||||
@@ -394,11 +407,7 @@ onMounted(fetchData)
|
|||||||
show-select
|
show-select
|
||||||
loading-text="加载中..."
|
loading-text="加载中..."
|
||||||
hover
|
hover
|
||||||
:style="
|
:style="tableStyle"
|
||||||
appMode
|
|
||||||
? 'height: calc(100vh - 15.5rem - env(safe-area-inset-bottom) - 3.5rem)'
|
|
||||||
: 'height: calc(100vh - 14.5rem - env(safe-area-inset-bottom)'
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<template #item.title="{ item }">
|
<template #item.title="{ item }">
|
||||||
<div class="d-flex align-center">
|
<div class="d-flex align-center">
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ import { requiredValidator } from '@/@validators'
|
|||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import type { User } from '@/api/types'
|
import type { User } from '@/api/types'
|
||||||
import avatar1 from '@images/avatars/avatar-1.png'
|
import avatar1 from '@images/avatars/avatar-1.png'
|
||||||
|
import { useDisplay } from 'vuetify'
|
||||||
|
|
||||||
|
// 显示器宽度
|
||||||
|
const display = useDisplay()
|
||||||
|
|
||||||
const isNewPasswordVisible = ref(false)
|
const isNewPasswordVisible = ref(false)
|
||||||
const isConfirmPasswordVisible = ref(false)
|
const isConfirmPasswordVisible = ref(false)
|
||||||
@@ -250,7 +254,7 @@ onMounted(() => {
|
|||||||
<div class="d-flex flex-wrap gap-2">
|
<div class="d-flex flex-wrap gap-2">
|
||||||
<VBtn color="primary" @click="refInputEl?.click()">
|
<VBtn color="primary" @click="refInputEl?.click()">
|
||||||
<VIcon icon="mdi-cloud-upload-outline" />
|
<VIcon icon="mdi-cloud-upload-outline" />
|
||||||
<span class="d-none d-sm-block ms-2">上传头像</span>
|
<span v-if="display.mdAndUp.value" class="ms-2">上传头像</span>
|
||||||
</VBtn>
|
</VBtn>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
@@ -264,7 +268,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
<VBtn type="reset" color="error" variant="tonal" @click="resetAvatar">
|
<VBtn type="reset" color="error" variant="tonal" @click="resetAvatar">
|
||||||
<VIcon icon="mdi-refresh" />
|
<VIcon icon="mdi-refresh" />
|
||||||
<span class="d-none d-sm-block ms-2">重置</span>
|
<span v-if="display.mdAndUp.value" class="ms-2">重置</span>
|
||||||
</VBtn>
|
</VBtn>
|
||||||
|
|
||||||
<VBtn
|
<VBtn
|
||||||
@@ -273,7 +277,9 @@ onMounted(() => {
|
|||||||
@click.stop="accountInfo.is_otp ? disableOtp() : getOtpUri()"
|
@click.stop="accountInfo.is_otp ? disableOtp() : getOtpUri()"
|
||||||
>
|
>
|
||||||
<VIcon icon="mdi-account-key" />
|
<VIcon icon="mdi-account-key" />
|
||||||
<span class="d-none d-sm-block ms-2">{{ accountInfo.is_otp ? '关闭验证' : '双重验证' }}</span>
|
<span v-if="display.mdAndUp.value" class="ms-2">{{
|
||||||
|
accountInfo.is_otp ? '关闭验证' : '双重验证'
|
||||||
|
}}</span>
|
||||||
</VBtn>
|
</VBtn>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ function addDownloadDirectory() {
|
|||||||
downloadDirectories.value.push({
|
downloadDirectories.value.push({
|
||||||
name: `下载目录${downloadDirectories.value.length + 1}`,
|
name: `下载目录${downloadDirectories.value.length + 1}`,
|
||||||
path: '',
|
path: '',
|
||||||
media_type: '全部',
|
media_type: '',
|
||||||
category: '',
|
category: '',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -181,7 +181,7 @@ function addLibraryDirectory() {
|
|||||||
libraryDirectories.value.push({
|
libraryDirectories.value.push({
|
||||||
name: `媒体库目录${libraryDirectories.value.length + 1}`,
|
name: `媒体库目录${libraryDirectories.value.length + 1}`,
|
||||||
path: '',
|
path: '',
|
||||||
media_type: '全部',
|
media_type: '',
|
||||||
category: '',
|
category: '',
|
||||||
scrape: true,
|
scrape: true,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import type { Plugin, Subscribe } from '@/api/types'
|
import type { Plugin, Subscribe } from '@/api/types'
|
||||||
import { SystemNavMenus, UserfulMenus, PluginTabs, SettingTabs } from '@/router/menu'
|
import { SystemNavMenus, UserfulMenus, SettingTabs } from '@/router/menu'
|
||||||
import { NavMenu } from '@/@layouts/types'
|
import { NavMenu } from '@/@layouts/types'
|
||||||
|
import store from '@/store'
|
||||||
|
|
||||||
// 路由
|
// 路由
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
|
// 超级用户
|
||||||
|
const superUser = store.state.auth.superUser
|
||||||
|
|
||||||
|
// 当前用户名
|
||||||
|
const userName = store.state.auth.userName
|
||||||
|
|
||||||
// 定义事件
|
// 定义事件
|
||||||
const emit = defineEmits(['close'])
|
const emit = defineEmits(['close'])
|
||||||
|
|
||||||
@@ -74,6 +81,7 @@ function getMenus(): NavMenu[] {
|
|||||||
// 匹配的菜单列表
|
// 匹配的菜单列表
|
||||||
const matchedMenuItems = computed(() => {
|
const matchedMenuItems = computed(() => {
|
||||||
if (!searchWord.value) return []
|
if (!searchWord.value) return []
|
||||||
|
if (!superUser) return []
|
||||||
const lowerWord = (searchWord.value as string).toLowerCase()
|
const lowerWord = (searchWord.value as string).toLowerCase()
|
||||||
const menuItems = getMenus()
|
const menuItems = getMenus()
|
||||||
if (menuItems)
|
if (menuItems)
|
||||||
@@ -104,6 +112,7 @@ async function fetchInstalledPlugins() {
|
|||||||
// 区配的插件列表
|
// 区配的插件列表
|
||||||
const matchedPluginItems = computed(() => {
|
const matchedPluginItems = computed(() => {
|
||||||
if (!searchWord.value) return []
|
if (!searchWord.value) return []
|
||||||
|
if (!superUser) return []
|
||||||
const lowerWord = (searchWord.value as string).toLowerCase()
|
const lowerWord = (searchWord.value as string).toLowerCase()
|
||||||
return pluginItems.value.filter((item: Plugin) => {
|
return pluginItems.value.filter((item: Plugin) => {
|
||||||
if (!item.plugin_name && !item.plugin_desc) return false
|
if (!item.plugin_name && !item.plugin_desc) return false
|
||||||
@@ -114,7 +123,7 @@ const matchedPluginItems = computed(() => {
|
|||||||
// 所有订阅数据
|
// 所有订阅数据
|
||||||
const SubscribeItems = ref<Subscribe[]>([])
|
const SubscribeItems = ref<Subscribe[]>([])
|
||||||
|
|
||||||
// 获取电影订阅列表数据
|
// 获取订阅列表数据
|
||||||
async function fetchSubscribes() {
|
async function fetchSubscribes() {
|
||||||
try {
|
try {
|
||||||
SubscribeItems.value = await api.get('subscribe/')
|
SubscribeItems.value = await api.get('subscribe/')
|
||||||
@@ -128,7 +137,7 @@ const matchedSubscribeItems = computed(() => {
|
|||||||
if (!searchWord.value) return []
|
if (!searchWord.value) return []
|
||||||
const lowerWord = (searchWord.value as string).toLowerCase()
|
const lowerWord = (searchWord.value as string).toLowerCase()
|
||||||
return SubscribeItems.value.filter((item: Subscribe) => {
|
return SubscribeItems.value.filter((item: Subscribe) => {
|
||||||
return item.name.toLowerCase().includes(lowerWord)
|
return (item.name.toLowerCase().includes(lowerWord) && (superUser || userName === item.username)) || false
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -287,7 +296,7 @@ onMounted(() => {
|
|||||||
</VListItem>
|
</VListItem>
|
||||||
</template>
|
</template>
|
||||||
</VHover>
|
</VHover>
|
||||||
<VHover>
|
<VHover v-if="superUser">
|
||||||
<template #default="hover">
|
<template #default="hover">
|
||||||
<VListItem prepend-icon="mdi-history" link v-bind="hover.props" @click="searchHistory">
|
<VListItem prepend-icon="mdi-history" link v-bind="hover.props" @click="searchHistory">
|
||||||
<VListItemTitle class="break-words whitespace-break-spaces">
|
<VListItemTitle class="break-words whitespace-break-spaces">
|
||||||
@@ -382,7 +391,7 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</VCol>
|
</VCol>
|
||||||
</VRow>
|
</VRow>
|
||||||
<VRow>
|
<VRow v-if="superUser">
|
||||||
<VCol cols="12" md="6">
|
<VCol cols="12" md="6">
|
||||||
<p class="custom-letter-spacing text-sm text-disabled text-uppercase py-2 px-4 mb-0">常用功能</p>
|
<p class="custom-letter-spacing text-sm text-disabled text-uppercase py-2 px-4 mb-0">常用功能</p>
|
||||||
<VList lines="one">
|
<VList lines="one">
|
||||||
|
|||||||
Reference in New Issue
Block a user