Compare commits

..

53 Commits

Author SHA1 Message Date
jxxghp
6fdbc8104c v1.9.17 2024-09-18 17:37:57 +08:00
jxxghp
433c14679c 更新 package.json 2024-09-08 13:09:25 +08:00
jxxghp
fcaa4476f0 更新 package.json 2024-09-05 06:59:13 +08:00
jxxghp
85c5c3058c 更新 AccountSettingDirectory.vue 2024-09-05 06:57:32 +08:00
jxxghp
035122a08e Merge pull request #172 from flowclouds/main 2024-08-16 12:07:18 +08:00
jxxghp
0a76875f8e chore: update package.json version to 1.9.14-2 2024-08-16 10:10:41 +08:00
lj
218eac54ce fix: 优化代码移除相关警告 2024-08-15 15:52:32 +08:00
jxxghp
84deeff4f5 chore: update package.json version to 1.9.14-1 2024-08-12 18:13:47 +08:00
jxxghp
0c72d026f6 chore: update package.json version to 1.9.14 2024-08-08 14:37:40 +08:00
jxxghp
aec9ea83c5 更新 package.json 2024-07-30 06:33:53 +08:00
jxxghp
effd13aedd Merge pull request #171 from InfinityPacer/main 2024-07-22 22:12:19 +08:00
InfinityPacer
42b43d65d7 fix(FilterRuleCard): 移除规则中的空白字符并保留前后的空格 2024-07-22 21:47:42 +08:00
jxxghp
c501d824dd Merge pull request #170 from InfinityPacer/main 2024-07-17 06:53:43 +08:00
InfinityPacer
384ac2faf1 feat: ace-editor support python 2024-07-17 01:48:18 +08:00
jxxghp
dd2c4dd24b v1.9.12 2024-07-16 07:56:07 +08:00
jxxghp
356ffddb1c release 2024-07-09 08:00:11 +08:00
jxxghp
de69be7c4e Merge pull request #168 from s0urcelab/main 2024-07-09 06:28:30 +08:00
s0urce
e962f555ae fix: issue #167 2024-07-09 02:56:48 +08:00
jxxghp
1987246585 Merge pull request #166 from JavaZeroo/main 2024-07-08 19:05:23 +08:00
JavaZero
393264f66b 实现根据操作系统动态显示不同的搜索快捷键提示 2024-07-08 13:51:00 +08:00
jxxghp
9b50020b3b Merge pull request #165 from BrettDean/main 2024-07-05 15:26:31 +08:00
Dean
5e5545fe01 优化"最近入库"数量显示 2024-07-05 15:19:50 +08:00
jxxghp
0e8da35b0a fix bug 2024-07-01 10:55:46 +08:00
jxxghp
4d2cf73330 fix https://github.com/jxxghp/MoviePilot/issues/2471 2024-07-01 10:20:50 +08:00
jxxghp
5df89f2ce4 release 2024-06-28 10:48:20 +08:00
jxxghp
045c0b4c0c fix ui 2024-06-28 10:47:50 +08:00
jxxghp
8b4ffa0795 Update SubscribeCard.vue 2024-06-28 10:03:28 +08:00
jxxghp
14359a37ae Update package.json 2024-06-26 16:14:24 +08:00
jxxghp
a8e4a1c2e0 Merge pull request #162 from jxxghp/main
fix bugs
2024-06-24 12:59:49 +08:00
jxxghp
9048d181af Merge branch 'dev' into main 2024-06-24 12:59:22 +08:00
jxxghp
1cb02994bf fix 登录失败的提示信息 2024-06-24 11:51:39 +08:00
jxxghp
6fad85e957 feat:仪表盘不活跃时不刷新 && 网盘整理联动刮削 2024-06-24 09:13:22 +08:00
jxxghp
db9b2ee6b3 init 2024-06-23 09:35:00 +08:00
jxxghp
8efeb77102 v1.9.8 2024-06-23 09:08:19 +08:00
jxxghp
0215a800e2 fix scrape 2024-06-23 09:06:14 +08:00
jxxghp
87d282f98b fix bug 2024-06-21 21:22:42 +08:00
jxxghp
60c392d3d0 fix 2024-06-21 19:15:40 +08:00
jxxghp
34c3aa25da fix: 修复刮削功能中的路径错误 2024-06-21 12:17:57 +08:00
jxxghp
80690d4cc8 fix win 2024-06-21 11:02:42 +08:00
jxxghp
18f3dc2d44 fix buttons 2024-06-20 17:40:51 +08:00
jxxghp
e8256b4e1a fix bug 2024-06-20 15:34:30 +08:00
jxxghp
4f67bb0250 feat:文件管理批量选择 2024-06-20 15:32:17 +08:00
jxxghp
5dd071adf4 fix bug 2024-06-20 14:01:20 +08:00
jxxghp
aaf5e7f49d feat:阿里云盘支持备份盘 2024-06-20 13:16:05 +08:00
jxxghp
6a5958409a fix:优化文件管理 2024-06-20 11:39:25 +08:00
jxxghp
e0ff98b1d7 fix store 2024-06-20 08:11:47 +08:00
jxxghp
a815e07cdd fix store 2024-06-20 07:08:47 +08:00
jxxghp
aa2fe9740c fix 2024-06-19 18:02:47 +08:00
jxxghp
75a358a4d2 feat: improve QR code UI and add loading skeleton 2024-06-19 16:07:37 +08:00
jxxghp
d5646be6f8 fix qrcode ui 2024-06-19 15:58:26 +08:00
jxxghp
cb04ebcd95 批量重命名进度条 2024-06-19 15:20:50 +08:00
jxxghp
9889ccfc74 feat: add keepAlive meta property to filemanager route 2024-06-19 14:43:38 +08:00
jxxghp
f528bd861a chore: update file list layout for renaming feature 2024-06-19 13:45:08 +08:00
37 changed files with 789 additions and 459 deletions

View File

@@ -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"
} }
} }

View File

@@ -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')

View File

@@ -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)
}, },
) )

View File

@@ -765,6 +765,8 @@ export interface FileItem {
thumbnail?: string thumbnail?: string
// pickcode // pickcode
pickcode?: string pickcode?: string
// drive_id
drive_id?: string
} }
// 媒体服务器播放条目 // 媒体服务器播放条目

View File

@@ -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"

View File

@@ -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

View File

@@ -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>

View File

@@ -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>
<!-- 插件配置页面 --> <!-- 插件配置页面 -->

View File

@@ -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">

View File

@@ -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 />

View File

@@ -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="整理方式"

View File

@@ -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 />

View File

@@ -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"
/> />
<!-- 进度框 --> <!-- 进度框 -->

View File

@@ -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>

View File

@@ -8,7 +8,6 @@ const props = defineProps({
root: { root: {
type: String, type: String,
default: '/', default: '/',
required: true,
}, },
}) })

View File

@@ -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'" />

View File

@@ -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;
} }

View File

@@ -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>

View File

@@ -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')
} }

View File

@@ -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>

View File

@@ -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) => {
// 登录失败,显示错误提示 // 登录失败,显示错误提示

View File

@@ -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>

View File

@@ -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,
}, },
}, },

View File

@@ -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,
}, },
} }

View File

@@ -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;
} }

View File

@@ -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

View File

@@ -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')

View File

@@ -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')

View File

@@ -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')

View File

@@ -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()
}
},
}, },
}, },
} }

View File

@@ -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">

View File

@@ -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>

View File

@@ -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>

View File

@@ -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">

View File

@@ -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>

View File

@@ -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,
}) })

View File

@@ -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">