Compare commits

...

53 Commits
v1.9.7 ... main

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",
"version": "1.9.7",
"version": "1.9.17",
"private": true,
"bin": "dist/service.js",
"scripts": {
@@ -101,4 +101,4 @@
"resolutions": {
"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 modePythonUrl from 'ace-builds/src-noconflict/mode-python?url'
import themeGithubUrl from 'ace-builds/src-noconflict/theme-github?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 snippetsPythonUrl from 'ace-builds/src-noconflict/snippets/python?url'
import 'ace-builds/src-noconflict/ext-language_tools'
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/yaml', modeYamlUrl)
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/chrome', themeChromeUrl)
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/json', snippetsJsonUrl)
ace.config.setModuleUrl('ace/snippets/css', snippertsCssUrl)
ace.config.setModuleUrl('ace/snippets/python', snippetsPythonUrl)
ace.require('ace/ext/language_tools')

View File

@@ -27,13 +27,12 @@ api.interceptors.response.use(
return Promise.reject(new Error(error))
} else if (error.response.status === 403) {
// 清除登录状态信息
store.dispatch('auth/clearToken')
store.dispatch('auth/logout')
// token验证失败跳转到登录页面
router.push('/login')
}
return Promise.reject(new Error(error))
return Promise.reject(error)
},
)

View File

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

View File

@@ -11,10 +11,6 @@ import { isNullOrEmptyObject } from '@/@core/utils'
// 输入参数
const props = defineProps({
storages: String,
path: String,
fileid: String,
pickcode: String,
fileidstack: Array as PropType<string[]>,
tree: Boolean,
endpoints: Object as PropType<EndPoints>,
axios: {
@@ -22,6 +18,11 @@ const props = defineProps({
required: true,
},
axiosconfig: Object,
item: {
type: Object as PropType<FileItem>,
required: true,
},
itemstack: Array as PropType<FileItem[]>,
})
// 对外事件
@@ -165,11 +166,10 @@ function u115AuthDone() {
<template>
<VCard class="mx-auto" :loading="loading > 0">
<div v-if="activeStorage && (path || fileid)">
<div v-if="activeStorage && item">
<FileToolbar
:path="path"
:fileid="fileid"
:fileidstack="fileidstack"
:item="item"
:itemstack="itemstack"
:storages="storagesArray"
:storage="activeStorage"
:endpoints="endpoints"
@@ -180,9 +180,7 @@ function u115AuthDone() {
@sortchanged="sortChanged"
/>
<FileList
:path="path"
:fileid="fileid"
:pickcode="pickcode"
:item="item"
:storage="activeStorage"
:icons="fileIcons"
:endpoints="endpoints"

View File

@@ -21,6 +21,14 @@ function filtersChanged(value: string[]) {
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 }[]>([
{ title: '特效字幕', value: ' SPECSUB ' },
@@ -77,7 +85,7 @@ const selectFilterOptions = ref<{ [key: string]: string }[]>([
<VRow>
<VCol>
<VSelect
v-model="props.rules"
v-model="cleanedRules"
variant="underlined"
:items="selectFilterOptions"
chips

View File

@@ -150,55 +150,61 @@ const dropdownItems = ref([
<template>
<VCard :width="props.width" :height="props.height" @click="installPlugin" class="flex flex-col">
<div class="relative pa-3 text-center card-cover-blurred" :style="{ background: `${backgroundColor}` }">
<div class="me-n3 absolute top-0 right-3">
<IconBtn>
<VIcon icon="mdi-dots-vertical" class="text-white" />
<VMenu activator="parent" close-on-content-click>
<VList>
<VListItem
v-for="(item, i) in dropdownItems"
v-show="item.show"
:key="i"
variant="plain"
@click="item.props.click"
>
<template #prepend>
<VIcon :icon="item.props.prependIcon" />
</template>
<VListItemTitle v-text="item.title" />
</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 class="me-n3 absolute bottom-0 right-3">
<IconBtn>
<VIcon icon="mdi-dots-vertical" />
<VMenu activator="parent" close-on-content-click>
<VList>
<VListItem
v-for="(item, i) in dropdownItems"
v-show="item.show"
:key="i"
variant="plain"
@click="item.props.click"
>
<template #prepend>
<VIcon :icon="item.props.prependIcon" />
</template>
<VListItemTitle v-text="item.title" />
</VListItem>
</VList>
</VMenu>
</IconBtn>
</div>
<VCardTitle>
{{ props.plugin?.plugin_name }}
<span class="text-sm text-gray-500">v{{ props.plugin?.plugin_version }}</span>
</VCardTitle>
<VCardText class="pb-2">
<div>{{ props.plugin?.plugin_desc }}</div>
<div>
<VChip v-for="label in pluginLabels" variant="tonal" size="small" class="me-1 my-1" color="info" label>
{{ label }}
</VChip>
<div
class="relative flex flex-row items-start pa-3 justify-between grow"
:style="{ background: `${backgroundColor}` }"
>
<div
class="absolute inset-0 bg-cover bg-center"
:style="{ background: `${backgroundColor}`, filter: 'brightness(0.7)' }"
></div>
<div class="relative flex-1 min-w-0">
<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>
</VCardText>
<VCardText class="flex align-self-baseline pb-2 w-full align-end">
<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-account" class="me-1" />
<VIcon icon="mdi-github" class="me-1" />
<a :href="props.plugin?.author_url" target="_blank" @click.stop>
{{ props.plugin?.plugin_author }}
</a>
@@ -220,15 +226,3 @@ const dropdownItems = ref([
</VCard>
</VDialog>
</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>
<!-- 插件卡片 -->
<VCard v-if="isVisible" :width="props.width" :height="props.height" @click="openPluginDetail" class="flex flex-col">
<div class="relative pa-3 text-center card-cover-blurred" :style="{ background: `${backgroundColor}` }">
<div v-if="props.plugin?.has_update" class="me-n3 absolute top-0 left-1">
<VIcon icon="mdi-new-box" class="text-white" />
</div>
<div class="me-n3 absolute top-0 right-3">
<IconBtn>
<VIcon icon="mdi-dots-vertical" class="text-white" />
<VMenu activator="parent" close-on-content-click>
<VList>
<VListItem
v-for="(item, i) in dropdownItems"
v-show="item.show"
:key="i"
variant="plain"
:base-color="item.props.color"
@click="item.props.click"
>
<template #prepend>
<VIcon :icon="item.props.prependIcon" />
</template>
<VListItemTitle v-text="item.title" />
</VListItem>
</VList>
</VMenu>
</IconBtn>
</div>
<VAvatar size="6rem">
<VImg
ref="imageRef"
:src="iconPath"
aspect-ratio="4/3"
cover
:class="{ shadow: isImageLoaded }"
@load="imageLoaded"
@error="imageLoadError = true"
/>
</VAvatar>
<div class="me-n3 absolute bottom-0 right-3">
<IconBtn>
<VIcon icon="mdi-dots-vertical" />
<VMenu activator="parent" close-on-content-click>
<VList>
<VListItem
v-for="(item, i) in dropdownItems"
v-show="item.show"
:key="i"
variant="plain"
:base-color="item.props.color"
@click="item.props.click"
>
<template #prepend>
<VIcon :icon="item.props.prependIcon" />
</template>
<VListItemTitle v-text="item.title" />
</VListItem>
</VList>
</VMenu>
</IconBtn>
</div>
<VCardItem class="py-2">
<VCardTitle class="flex items-center flex-row">
<VBadge v-if="props.plugin?.state" dot inline color="success" class="me-1 mb-1" />
{{ props.plugin?.plugin_name }}
<span class="text-sm ms-2 mt-1 text-gray-500">v{{ props.plugin?.plugin_version }}</span>
</VCardTitle>
</VCardItem>
<VCardText class="pb-1">
{{ props.plugin?.plugin_desc }}
</VCardText>
<VCardText class="flex justify-end align-self-baseline p-1 w-full align-end">
<div
class="relative flex flex-row items-start pa-3 justify-between grow"
:style="{ background: `${backgroundColor}` }"
>
<div
class="absolute inset-0 bg-cover bg-center"
:style="{ background: `${backgroundColor}`, filter: 'brightness(0.7)' }"
/>
<div class="relative flex-1 min-w-0">
<VCardTitle class="text-white px-2 text-shadow whitespace-nowrap overflow-hidden text-ellipsis">
<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">
<VIcon icon="mdi-fire" />
<span class="text-sm ms-1">{{ props.count?.toLocaleString() }}</span>
<VIcon icon="mdi-download" />
<span class="text-sm ms-1 mt-1">{{ props.count?.toLocaleString() }}</span>
</span>
</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>
<!-- 插件配置页面 -->

View File

@@ -182,6 +182,7 @@ watch(
'outline-dashed outline-1': props.media?.best_version && imageLoaded,
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
}"
min-height="170"
@click="editSubscribeDialog"
>
<div class="me-n3 absolute top-1 right-2">
@@ -222,9 +223,9 @@ watch(
<div class="absolute inset-0 subscribe-card-background"></div>
</VImg>
</template>
<div v-if="imageLoaded">
<div>
<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">
<template #placeholder>
<div class="w-full h-full">

View File

@@ -92,9 +92,9 @@ onUnmounted(() => {
<VDialog width="40rem" scrollable max-height="85vh">
<VCard title="阿里云盘登录" class="rounded-t">
<DialogCloseBtn @click="emit('close')" />
<VCardText class="pt-2">
<div class="my-6">
<QrcodeVue class="mx-auto" :value="qrCodeContent" :size="200" max-width="25rem" />
<VCardText class="pt-2 flex flex-col items-center">
<div class="my-6 shadow-lg rounded text-center p-3 border">
<QrcodeVue class="mx-auto" :value="qrCodeContent" :size="200" />
</div>
<VAlert variant="tonal" :type="alertType" class="my-4 text-center" :text="text">
<template #prepend />

View File

@@ -6,16 +6,20 @@ import api from '@/api'
import { numberValidator } from '@/@validators'
import { useDisplay } from 'vuetify'
import ProgressDialog from './ProgressDialog.vue'
import { MediaDirectory } from '@/api/types'
import { FileItem, MediaDirectory } from '@/api/types'
// 显示器宽度
const display = useDisplay()
// 输入参数
const props = defineProps({
path: String,
target: String,
storage: {
type: String,
default: () => 'local',
},
logids: Array<number>,
items: Array<FileItem>,
target: String,
})
// 定义事件
@@ -50,10 +54,25 @@ const progressText = ref('请稍候 ...')
// 整理进度
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({
storage: props.storage,
logid: 0,
path: '',
drive_id: '',
fileid: '',
filetype: '',
target: props.target ?? null,
tmdbid: null,
doubanid: null,
@@ -77,12 +96,6 @@ const targetDirectories = computed(() => {
return [...new Set(directories)]
})
// 监听输入变化
watchEffect(() => {
transferForm.path = props.path ?? ''
transferForm.target = props.target ?? null
})
// 监听目的路径变化,自动查询目录的刮削配置
watch(transferForm, async () => {
if (transferForm.target) {
@@ -117,47 +130,25 @@ function stopLoadingProgress() {
}
// 整理文件
// eslint-disable-next-line sonarjs/cognitive-complexity
async function transfer() {
if (!props.logids && !props.path) return
if (!props.logids && !props.items) return
// 显示进度条
progressDialog.value = true
// 开始监听进度
startLoadingProgress()
if (props.path) {
// 文件整理
try {
const result: { [key: string]: any } = await api.post(
'transfer/manual',
{},
{
params: transferForm,
},
)
// 显示结果
if (result.success) $toast.success(`${props.path} 整理完成!`)
else $toast.error(`${props.path} 整理失败:${result.message}`)
} catch (e) {
console.log(e)
// 文件整理
if (props.items) {
for (const item of props.items) {
await handleTransfer(item)
}
} else if (props.logids) {
// 日志整理
}
// 日志整理
if (props.logids) {
for (const logid of props.logids) {
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)
}
await handleTransferLog(logid)
}
}
@@ -169,6 +160,32 @@ async function transfer() {
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加载当前系统环境设置
async function loadSystemSettings() {
try {
@@ -199,16 +216,13 @@ onMounted(() => {
<template>
<VDialog scrollable max-width="50rem" :fullscreen="!display.mdAndUp.value">
<VCard
:title="`${props.path ? `整理 - ${props.path}` : `整理 - 共 ${props.logids?.length} 条记录`}`"
class="rounded-t"
>
<VCard :title="dialogTitle" class="rounded-t">
<DialogCloseBtn @click="emit('close')" />
<VDivider />
<VCardText>
<VForm @submit.prevent="() => {}">
<VRow>
<VCol cols="12" md="8">
<VCol v-if="props.storage == 'local'" cols="12" md="8">
<VCombobox
v-model="transferForm.target"
:items="targetDirectories"
@@ -218,7 +232,7 @@ onMounted(() => {
persistent-hint
/>
</VCol>
<VCol cols="12" md="4">
<VCol v-if="props.storage == 'local'" cols="12" md="4">
<VSelect
v-model="transferForm.transfer_type"
label="整理方式"

View File

@@ -79,9 +79,11 @@ onUnmounted(() => {
<VDialog width="40rem" scrollable max-height="85vh">
<VCard title="115网盘登录" class="rounded-t">
<DialogCloseBtn @click="emit('close')" />
<VCardText class="pt-2">
<div class="my-6">
<VImg class="mx-auto" :src="qrCodeContent" :size="200" max-width="25rem" />
<VCardText class="pt-2 flex flex-col items-center">
<div class="my-6 shadow-lg rounded border">
<VImg class="mx-auto" :src="qrCodeContent" style="block-size: 200px; inline-size: 200px">
<VSkeletonLoader v-if="!qrCodeContent" class="w-full h-full" />
</VImg>
</div>
<VAlert variant="tonal" :type="alertType" class="my-4 text-center" :text="text">
<template #prepend />

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup>
import type { Axios } from 'axios'
import type { Axios, AxiosRequestConfig } from 'axios'
import type { PropType } from 'vue'
import { useConfirm } from 'vuetify-use-dialog'
import { useToast } from 'vue-toast-notification'
@@ -24,24 +24,31 @@ const appMode = computed(() => {
const inProps = defineProps({
icons: Object,
storage: String,
path: String,
fileid: String,
pickcode: String,
endpoints: Object as PropType<EndPoints>,
axios: {
type: Object as PropType<Axios>,
required: true,
},
refreshpending: Boolean,
item: {
type: Object as PropType<FileItem>,
required: true,
},
sort: String,
})
// 对外事件
const emit = defineEmits(['loading', 'pathchanged', 'refreshed', 'filedeleted', 'renamed'])
// 确认框
const createConfirm = useConfirm()
// 提示框
const $toast = useToast()
// 是否选择模式
const selectMode = ref(false)
// 是否正在加载
const loading = ref(true)
@@ -57,9 +64,6 @@ const progressText = ref('请稍候 ...')
// 识别进度
const progressValue = ref(0)
// 确认框
const createConfirm = useConfirm()
// 内容列表
const items = ref<FileItem[]>([])
@@ -78,9 +82,12 @@ const newName = ref('')
// 处理目录内所有文件
const renameAll = ref(false)
// 当前名称
// 当前操作项
const currentItem = ref<FileItem>()
// 选中的项目
const selected = ref<FileItem[]>([])
// 识别结果
const nameTestResult = ref<Context>()
@@ -90,40 +97,58 @@ const nameTestDialog = ref(false)
// 弹出菜单
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 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 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 ext = inProps.path?.split('.').pop()?.toLowerCase()
const ext = inProps.item.path?.split('.').pop()?.toLowerCase()
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
emit('loading', true)
// 参数
const url = inProps.endpoints?.list.url
.replace(/{storage}/g, inProps.storage)
.replace(/{path}/g, encodeURIComponent(inProps.path || ''))
.replace(/{sort}/g, inProps.sort || 'name')
.replace(/{fileid}/g, inProps.fileid || '')
.replace(/{filetype}/g, isDir.value ? 'dir' : 'file')
.replace(/{pickcode}/g, inProps.pickcode || '')
const config = {
const config: AxiosRequestConfig<FileItem> = {
url,
method: inProps.endpoints?.list.method || 'get',
data: inProps.item,
}
// 加载数据
items.value = (await inProps.axios.request(config)) ?? []
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({
title: '确认',
content: `是否确认删除${item.type === 'dir' ? '目录' : '文件'} ${item.name}`,
content: `是否确认删除选中的 ${selected.value.length} 个项目`,
})
if (confirmed) {
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 || '')
if (!confirmed) return
const config = {
url,
method: inProps.endpoints?.delete.method || 'post',
}
// 显示进度条
progressDialog.value = true
progressValue.value = 0
await inProps.axios.request(config)
emit('filedeleted')
emit('loading', false)
// 重新加载
load()
}
// 删除选中的项目
selected.value.every(async item => {
progressText.value = `正在删除 ${item.name} ...`
await deleteItem(item, false)
})
// 关闭进度条
progressDialog.value = false
// 重新加载
list_files()
}
// 切换路径
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)
}
// 新窗口中下载文件
function download(item: FileItem) {
const token = store.state.auth.token
const url_path = inProps.endpoints?.download.url
.replace(/{storage}/g, inProps.storage)
.replace(/{path}/g, encodeURIComponent(item.path))
.replace(/{fileid}/g, item.fileid || '')
.replace(/{pickcode}/g, item.pickcode || '')
const url = `${import.meta.env.VITE_API_BASE_URL}${url_path.slice(1)}&token=${token}`
// 下载文件
window.open(url, '_blank')
// 点击列表项
function listItemClick(item: FileItem) {
if (selectMode.value) {
if (selected.value.includes(item)) {
selected.value = selected.value.filter(i => i !== item)
} else {
selected.value.push(item)
}
// 去重
selected.value = Array.from(new Set(selected.value))
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) {
const token = store.state.auth.token
const url_path = inProps.endpoints?.image.url
.replace(/{storage}/g, inProps.storage)
.replace(/{path}/g, encodeURIComponent(item.path))
.replace(/{fileid}/g, item.fileid || '')
.replace(/{pickcode}/g, item.pickcode || '')
return `${import.meta.env.VITE_API_BASE_URL}${url_path.slice(1)}&token=${token}`
let url = inProps.endpoints?.image.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('&')
return `${import.meta.env.VITE_API_BASE_URL}${url.slice(1)}?${queryParams}&token=${store.state.auth.token}`
}
// 显示重命名弹窗
@@ -201,7 +265,7 @@ async function get_recommend_name() {
try {
const result: { [key: string]: any } = await api.get('transfer/name', {
params: {
path: `${inProps.path}${currentItem.value?.name}`,
path: `${inProps.item.path}${currentItem.value?.name}`,
filetype: currentItem.value?.type ?? 'file',
},
})
@@ -220,60 +284,89 @@ async function get_recommend_name() {
async function rename() {
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
.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(/{filetype}/g, currentItem.value?.type || 'file')
if (renameAll.value) {
url += '&recursive=true'
}
const config = {
const config: AxiosRequestConfig<FileItem> = {
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)
if (!result.success) {
$toast.error(result.message)
}
// 关闭进度条
if (renameAll.value) {
stopLoadingProgress()
}
progressDialog.value = false
// 通知重新加载
newName.value = ''
renameAll.value = false
emit('loading', false)
emit('renamed')
}
// 显示整理对话框
function showTransfer(item: FileItem) {
currentItem.value = item
transferItems.value = [item]
transferPopper.value = true
}
// 显示批量整理对话框
function showBatchTransfer() {
transferItems.value = selected.value
transferPopper.value = true
}
// 整理完成
function transferDone() {
transferPopper.value = false
list_files()
}
// 将文件修改时间timestape转换为本地时间
function formatTime(timestape: number) {
return new Date(timestape * 1000).toLocaleString()
}
// 监听path变化或者storage变化
// 监听refreshPending变化
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 () => {
// 清空列表
items.value = []
@@ -296,11 +389,11 @@ watch(
{
title: '刮削',
value: 2,
show: inProps.storage == 'local',
show: true,
props: {
prependIcon: 'mdi-auto-fix',
click: (_item: FileItem) => {
scrape(_item.path || '')
scrape(_item)
},
},
},
@@ -316,7 +409,7 @@ watch(
{
title: '整理',
value: 4,
show: inProps.storage == 'local',
show: true,
props: {
prependIcon: 'mdi-folder-arrow-right',
click: showTransfer,
@@ -333,22 +426,11 @@ watch(
},
},
]
await load()
await list_files()
},
{ immediate: true },
)
// 监听refreshPending变化
watch(
() => inProps.refreshpending,
async () => {
if (inProps.refreshpending) {
await load()
emit('refreshed')
}
},
)
// 调用API识别
async function recognize(path: string) {
try {
@@ -371,27 +453,71 @@ async function recognize(path: string) {
}
// 调用API刮削
async function scrape(path: string) {
async function scrape(item: FileItem, confirm: boolean = true) {
try {
if (confirm) {
// 确认
const confirmed = await createConfirm({
title: '确认',
content: `是否确认刮削 ${item.path}`,
})
if (!confirmed) return
}
// 显示进度条
progressDialog.value = true
progressText.value = `正在刮削 ${path} ...`
const result: { [key: string]: any } = await api.get('media/scrape', {
params: {
path,
},
})
progressText.value = `正在刮削 ${item.path} ...`
const result: { [key: string]: any } = await api.post(`media/scrape/${inProps.storage}`, item)
// 关闭进度条
progressDialog.value = false
if (!result.success) $toast.error(result.message)
else $toast.success(`${path}削刮完成!`)
else $toast.success(`${item.path} 削刮完成!`)
} catch (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(() => {
load()
list_files()
})
</script>
@@ -411,15 +537,31 @@ onMounted(() => {
rounded="0"
/>
<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>
</IconBtn>
<IconBtn v-if="isFile && items.length > 0" @click="download(items[0])">
<VIcon color="primary"> mdi-download </VIcon>
</IconBtn>
<IconBtn v-if="!isFile" @click="load">
<IconBtn v-if="!isFile" @click="list_files">
<VIcon color="primary"> mdi-refresh </VIcon>
</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>
<VCardText v-if="loading" class="text-center flex flex-col items-center">
<VProgressCircular size="48" indeterminate color="primary" />
@@ -446,31 +588,29 @@ onMounted(() => {
<!-- 目录和文件列表 -->
<VCardText v-else-if="dirs.length || files.length" class="p-0">
<VList subheader>
<VVirtualScroll
: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)'
"
>
<VVirtualScroll :items="[...dirs, ...files]" :style="scrollStyle">
<template #default="{ item }">
<VHover>
<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>
<VIcon
v-if="inProps.icons && item.extension"
:icon="inProps.icons[item.extension.toLowerCase()] || inProps.icons?.other"
/>
<VIcon v-else icon="mdi-folder-outline" />
<VListItemAction v-if="selectMode">
<VCheckbox v-model="selected" :value="item" />
</VListItemAction>
<template v-else>
<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>
<VListItemTitle v-text="item.name" />
<VListItemSubtitle v-if="item.size">
{{ formatBytes(item.size) }}
</VListItemSubtitle>
<template #append>
<IconBtn class="d-sm-none">
<IconBtn v-if="display.smAndDown.value && !selectMode">
<VIcon icon="mdi-dots-vertical" />
<VMenu activator="parent" close-on-content-click>
<VList>
@@ -490,38 +630,38 @@ onMounted(() => {
</VList>
</VMenu>
</IconBtn>
<span v-if="hover.isHovering" class="flex">
<span v-if="hover.isHovering && display.mdAndUp.value && !selectMode" class="flex">
<VTooltip text="识别">
<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" />
</IconBtn>
</template>
</VTooltip>
<VTooltip text="刮削" v-if="storage == 'local'">
<VTooltip text="刮削">
<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" />
</IconBtn>
</template>
</VTooltip>
<VTooltip text="重命名">
<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" />
</IconBtn>
</template>
</VTooltip>
<VTooltip text="整理" v-if="storage == 'local'">
<VTooltip text="整理">
<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" />
</IconBtn>
</template>
</VTooltip>
<VTooltip text="删除">
<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" />
</IconBtn>
</template>
@@ -550,7 +690,7 @@ onMounted(() => {
<VCol cols="12">
<VTextField v-model="newName" label="新名称" :loading="renameLoading" />
</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="自动重命名目录内所有媒体文件" />
</VCol>
</VRow>
@@ -569,13 +709,9 @@ onMounted(() => {
<ReorganizeDialog
v-if="transferPopper"
v-model="transferPopper"
:path="currentItem?.path"
@done="
() => {
transferPopper = false
load()
}
"
:storage="inProps.storage"
:items="transferItems"
@done="transferDone"
@close="transferPopper = false"
/>
<!-- 进度框 -->

View File

@@ -1,16 +1,22 @@
<script lang="ts" setup>
import type { Axios } from 'axios'
import type { EndPoints } from '@/api/types'
import type { Axios, AxiosRequestConfig } from 'axios'
import type { EndPoints, FileItem } from '@/api/types'
import { useDisplay } from 'vuetify'
// 显示器宽度
const display = useDisplay()
// 输入参数
const inProps = defineProps({
storages: Array as PropType<any[]>,
storage: String,
path: String,
fileid: String,
fileidstack: {
type: Array as PropType<string[]>,
default: () => [],
item: {
type: Object as PropType<FileItem>,
required: true,
},
itemstack: {
type: Array as PropType<FileItem[]>,
required: true,
},
endpoints: Object as PropType<EndPoints>,
axios: {
@@ -42,21 +48,20 @@ function changeSort() {
// 计算PATH面包屑
const pathSegments = computed(() => {
let path_str = ''
const isFolder = inProps.path?.endsWith('/')
const segments = inProps.path?.split('/').filter(item => item)
const fileids = inProps.fileidstack ?? []
const isFolder = inProps.item.path?.endsWith('/')
const segments = inProps.item.path?.split('/').filter(item => item)
return (
segments?.map((item, index) => {
path_str += item + (index < segments.length - 1 || isFolder ? '/' : '')
return {
name: item,
path: path_str,
fileid: fileids[index],
}
}) ?? []
)
})
// 当前存储
const storageObject = computed(() => {
return inProps.storages?.find(item => item.code === inProps.storage)
})
@@ -69,19 +74,15 @@ function changeStorage(code: string) {
}
// 路径变化
function changePath(_path: string, _fileid: string) {
emit('pathchanged', {
path: _path,
fileid: _fileid,
})
function changePath(item: FileItem) {
emit('pathchanged', item)
}
// 返回上一级
function goUp() {
const segments = pathSegments.value ?? []
const path = segments?.length === 1 ? '/' : segments[segments.length - 2].path
const fileid = segments?.length === 1 ? 'root' : segments[segments.length - 1].fileid
changePath(path, fileid)
const fileitem = inProps.itemstack[segments.length - 1]
changePath(fileitem)
}
// 创建目录
@@ -89,12 +90,12 @@ async function mkdir() {
emit('loading', true)
const url = inProps.endpoints?.mkdir.url
.replace(/{storage}/g, inProps.storage)
.replace(/{path}/g, encodeURIComponent(inProps.path + newFolderName.value))
.replace(/{fileid}/g, inProps.fileid || '')
.replace(/{name}/g, newFolderName.value)
const config = {
const config: AxiosRequestConfig<FileItem> = {
url,
method: inProps.endpoints?.mkdir.method || 'post',
data: inProps.item,
}
// 调API
@@ -138,16 +139,17 @@ const sortIcon = computed(() => {
</VListItem>
</VList>
</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" />
{{ storageObject?.name }}
</VBtn>
<template v-for="(segment, index) in pathSegments" :key="index">
<VBtn
v-if="display.mdAndUp.value"
variant="text"
:input-value="index === pathSegments.length - 1"
class="px-1 d-none d-md-block"
@click="changePath(segment.path, inProps.fileidstack[index + 1])"
class="px-1"
@click="changePath(inProps.itemstack[index + 1])"
>
<VIcon icon=" mdi-chevron-right" />
{{ segment.name }}
@@ -180,13 +182,16 @@ const sortIcon = computed(() => {
</IconBtn>
</template>
<VCard title="新建文件夹">
<DialogCloseBtn @click="newFolderPopper = false" />
<VDivider />
<VCardText>
<VTextField v-model="newFolderName" label="名称" />
</VCardText>
<VCardActions>
<div class="flex-grow-1" />
<VBtn depressed @click="newFolderPopper = false"> 取消 </VBtn>
<VBtn :disabled="!newFolderName" depressed variant="tonal" @click="mkdir"> 新建 </VBtn>
<VBtn :disabled="!newFolderName" variant="elevated" @click="mkdir" prepend-icon="mdi-check" class="px-5 me-3">
新建
</VBtn>
</VCardActions>
</VCard>
</VDialog>

View File

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

View File

@@ -12,12 +12,18 @@ import MediaServerLibrary from '@/views/dashboard/MediaServerLibrary.vue'
import MediaServerPlaying from '@/views/dashboard/MediaServerPlaying.vue'
import DashboardRender from '@/components/render/DashboardRender.vue'
import { isNullOrEmptyObject } from '@/@core/utils'
// 输入参数
const props = defineProps({
// 仪表板配置
config: Object as PropType<DashboardItem>,
// 刷新状态
refreshStatus: Boolean,
// 是否允许刷新数据
allowRefresh: {
type: Boolean,
default: true,
},
})
const emit = defineEmits(['update:refreshStatus'])
@@ -32,10 +38,10 @@ onUnmounted(() => {
<AnalyticsStorage v-if="config?.id === 'storage'" />
<AnalyticsMediaStatistic v-else-if="config?.id === 'mediaStatistic'" />
<AnalyticsWeeklyOverview v-else-if="config?.id === 'weeklyOverview'" />
<AnalyticsSpeed v-else-if="config?.id === 'speed'" />
<AnalyticsScheduler v-else-if="config?.id === 'scheduler'" />
<AnalyticsCpu v-else-if="config?.id === 'cpu'" />
<AnalyticsMemory v-else-if="config?.id === 'memory'" />
<AnalyticsSpeed v-else-if="config?.id === 'speed'" :allowRefresh="props.allowRefresh" />
<AnalyticsScheduler v-else-if="config?.id === 'scheduler'" :allowRefresh="props.allowRefresh" />
<AnalyticsCpu v-else-if="config?.id === 'cpu'" :allowRefresh="props.allowRefresh" />
<AnalyticsMemory v-else-if="config?.id === 'memory'" :allowRefresh="props.allowRefresh" />
<MediaServerLibrary v-else-if="config?.id === 'library'" />
<MediaServerPlaying v-else-if="config?.id === 'playing'" />
<MediaServerLatest v-else-if="config?.id === 'latest'" />

View File

@@ -1,5 +1,9 @@
<script lang="ts" setup>
import SlideViewTitle from '@/components/slide/SlideViewTitle.vue'
import { useDisplay } from 'vuetify'
// 显示器宽度
const display = useDisplay()
// 元素
const slideview_content = ref()
@@ -91,7 +95,7 @@ onActivated(() => {
<slot name="title">
<SlideViewTitle />
</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
class="rounded-circle"
variant="text"
@@ -122,8 +126,8 @@ onActivated(() => {
<style lang="scss" scoped>
.slideview_content {
-ms-overflow-style: none !important;
overflow: scroll hidden !important;
-ms-overflow-style: none !important;
overscroll-behavior-x: contain !important;
scrollbar-width: none !important;
}

View File

@@ -2,6 +2,7 @@
import * as Mousetrap from 'mousetrap'
import SearchBarView from '@/views/system/SearchBarView.vue'
import { useDisplay } from 'vuetify'
import { ref, computed } from 'vue'
const display = useDisplay()
@@ -15,6 +16,14 @@ function openSearchDialog() {
searchDialog.value = true
return false
}
// 检测操作系统是否是Mac
function isMac() {
return navigator.platform.toUpperCase().indexOf('MAC') >= 0
}
// 计算属性:根据操作系统显示不同的按键提示
const metaKey = computed(() => (isMac() ? '⌘+K' : 'Ctrl+K'))
</script>
<template>
@@ -25,12 +34,13 @@ function openSearchDialog() {
</IconBtn>
<span v-if="display.lgAndUp.value" class="flex align-center text-disabled ms-2" @click="openSearchDialog">
<span class="me-3">搜索</span>
<span class="meta-key">K</span>
<span class="meta-key">{{ metaKey }}</span>
</span>
</div>
<!-- 搜索弹窗 -->
<SearchBarView v-model="searchDialog" v-if="searchDialog" @close="searchDialog = false" />
</template>
<style type="scss" scoped>
.meta-key {
border: thin solid rgba(var(--v-border-color), var(--v-border-opacity));
@@ -39,4 +49,4 @@ function openSearchDialog() {
padding-block: 0.1rem;
padding-inline: 0.25rem;
}
</style>
</style>

View File

@@ -25,9 +25,7 @@ const progressDialog = ref(false)
// 执行注销操作
function logout() {
// 清除登录状态信息
store.dispatch('auth/clearToken')
// 主动登出时清除路由标记
store.state.auth.originalPath = null
store.dispatch('auth/logout')
// 重定向到登录页面或其他适当的页面
router.push('/login')
}

View File

@@ -19,6 +19,9 @@ const superUser = store.state.auth.superUser
// 是否拉升高度
const isElevated = ref(true)
// 是否发送请求的总开关
const isRequest = ref(true)
// 计算属性,控制是否拉升高度
const elevatedConf = controlledComputed(
() => isElevated.value,
@@ -269,7 +272,8 @@ async function getPluginDashboard(id: string, key: string) {
if (
res.attrs?.refresh &&
pluginDashboardRefreshStatus.value[pluginDashboardId] &&
enableConfig.value[pluginDashboardId]
enableConfig.value[pluginDashboardId] &&
isRequest.value
) {
// 清除之前的定时器
if (refreshTimers.value[pluginDashboardId]) {
@@ -298,6 +302,14 @@ onBeforeMount(async () => {
await loadDashboardConfig()
getPluginDashboardMeta()
})
onActivated(async () => {
isRequest.value = true
})
onDeactivated(() => {
isRequest.value = false
})
</script>
<template>
@@ -314,6 +326,7 @@ onBeforeMount(async () => {
<VCol v-if="enableConfig[buildPluginDashboardId(element.id, element.key)] && element.cols" v-bind:="element.cols">
<DashboardElement
:config="element"
:allow-refresh="isRequest"
v-model:refreshStatus="pluginDashboardRefreshStatus[buildPluginDashboardId(element.id, element.key)]"
/>
</VCol>

View File

@@ -158,19 +158,17 @@ function login() {
.then((response: any) => {
// 获取token
const token = response.access_token
const superuser = response.super_user
const username = response.user_name
const superUser = response.super_user
const userName = response.user_name
const avatar = response.avatar
const level = response.level
const remember = form.value.remember
// 更新token和remember状态到Vuex Store
store.dispatch('auth/updateToken', token)
store.dispatch('auth/updateRemember', form.value.remember)
store.dispatch('auth/updateSuperUser', superuser)
store.dispatch('auth/updateUserName', username)
store.dispatch('auth/updateAvatar', avatar)
store.dispatch('auth/login', { token, remember, superUser, userName, avatar, level })
// 登录后处理
afterLogin(superuser)
afterLogin(superUser)
})
.catch((error: any) => {
// 登录失败,显示错误提示

View File

@@ -32,8 +32,10 @@ function jumpTab(tab: string) {
@click="jumpTab(item.tab)"
selected-class="v-slide-group-item--active v-tab--selected"
>
<VIcon size="20" start :icon="item.icon" />
{{ item.title }}
<div>
<VIcon size="20" start :icon="item.icon" />
{{ item.title }}
</div>
</VTab>
</VTabs>
@@ -41,70 +43,90 @@ function jumpTab(tab: string) {
<!-- 用户 -->
<VWindowItem value="account">
<transition name="fade-slide" appear>
<AccountSettingAccount />
<div>
<AccountSettingAccount />
</div>
</transition>
</VWindowItem>
<!-- 连接 -->
<VWindowItem value="system">
<transition name="fade-slide" appear>
<AccountSettingSystem />
<div>
<AccountSettingSystem />
</div>
</transition>
</VWindowItem>
<!-- 目录 -->
<VWindowItem value="directory">
<transition name="fade-slide" appear>
<AccountSettingDirectory />
<div>
<AccountSettingDirectory />
</div>
</transition>
</VWindowItem>
<!-- 站点 -->
<VWindowItem value="site">
<transition name="fade-slide" appear>
<AccountSettingSite />
<div>
<AccountSettingSite />
</div>
</transition>
</VWindowItem>
<!-- 搜索 -->
<VWindowItem value="search">
<transition name="fade-slide" appear>
<AccountSettingSearch />
<div>
<AccountSettingSearch />
</div>
</transition>
</VWindowItem>
<!-- 订阅 -->
<VWindowItem value="subscribe">
<transition name="fade-slide" appear>
<AccountSettingSubscribe />
<div>
<AccountSettingSubscribe />
</div>
</transition>
</VWindowItem>
<!-- 服务 -->
<VWindowItem value="service">
<transition name="fade-slide" appear>
<AccountSettingService />
<div>
<AccountSettingService />
</div>
</transition>
</VWindowItem>
<!-- 通知 -->
<VWindowItem value="notification">
<transition name="fade-slide" appear>
<AccountSettingNotification />
<div>
<AccountSettingNotification />
</div>
</transition>
</VWindowItem>
<!-- 词表 -->
<VWindowItem value="words">
<transition name="fade-slide" appear>
<AccountSettingWords />
<div>
<AccountSettingWords />
</div>
</transition>
</VWindowItem>
<!-- 关于 -->
<VWindowItem value="about">
<transition name="fade-slide" appear>
<AccountSettingAbout />
<div>
<AccountSettingAbout />
</div>
</transition>
</VWindowItem>
</VWindow>

View File

@@ -144,6 +144,7 @@ const router = createRouter({
path: '/filemanager',
component: () => import('../pages/filemanager.vue'),
meta: {
keepAlive: true,
requiresAuth: true,
},
},

View File

@@ -8,6 +8,7 @@ interface AuthState {
userName: string
avatar: string
originalPath: string | null
level: number
}
// 定义根状态类型
@@ -25,6 +26,7 @@ const authModule: Module<AuthState, RootState> = {
userName: '',
avatar: '',
originalPath: null,
level: 1,
},
mutations: {
setToken(state, token: string) {
@@ -45,25 +47,25 @@ const authModule: Module<AuthState, RootState> = {
setAvatar(state, avatar: string) {
state.avatar = avatar
},
setOriginalPath(state, originalPath: string) {
state.originalPath = originalPath
},
setLevel(state, level: number) {
state.level = level
},
},
actions: {
updateToken({ commit }, token: string) {
login({ commit }, { token, remember, superUser, userName, avatar, level }) {
commit('setToken', token)
},
clearToken({ commit }) {
commit('clearToken')
},
updateRemember({ commit }, remember: boolean) {
commit('setRemember', remember)
},
updateSuperUser({ commit }, superUser: boolean) {
commit('setSuperUser', superUser)
},
updateUserName({ commit }, userName: string) {
commit('setUserName', userName)
},
updateAvatar({ commit }, avatar: string) {
commit('setAvatar', avatar)
commit('setLevel', level)
},
logout({ commit }) {
commit('clearToken')
commit('setOriginalPath', null)
},
},
getters: {
@@ -72,6 +74,8 @@ const authModule: Module<AuthState, RootState> = {
getSuperUser: state => state.superUser,
getUserName: state => state.userName,
getAvatar: state => state.avatar,
getOriginalPath: state => state.originalPath,
getLevel: state => state.level,
},
}

View File

@@ -156,7 +156,7 @@
}
.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;
}

View File

@@ -4,6 +4,15 @@ import { useTheme } from 'vuetify'
import { hexToRgb } from '@layouts/utils'
import api from '@/api'
// 输入参数
const props = defineProps({
// 是否允许刷新数据
allowRefresh: {
type: Boolean,
default: true,
},
})
const vuetifyTheme = useTheme()
const currentTheme = controlledComputed(
@@ -94,6 +103,7 @@ const chartOptions = controlledComputed(
// 调用API接口获取最新CPU使用率
async function getCpuUsage() {
if (!props.allowRefresh) return
try {
// 请求数据
current.value = (await api.get('dashboard/cpu')) ?? 0

View File

@@ -5,6 +5,15 @@ import { hexToRgb } from '@layouts/utils'
import api from '@/api'
import { formatBytes } from '@/@core/utils/formatters'
// 输入参数
const props = defineProps({
// 是否允许刷新数据
allowRefresh: {
type: Boolean,
default: true,
},
})
const vuetifyTheme = useTheme()
const currentTheme = controlledComputed(
@@ -100,6 +109,7 @@ const chartOptions = controlledComputed(
// 调用API接口获取最新内存使用量
async function getMemorgUsage() {
if (!props.allowRefresh) return
try {
// 请求数据
;[usedMemory.value, memoryUsage.value] = await api.get('dashboard/memory')

View File

@@ -2,6 +2,15 @@
import api from '@/api'
import type { ScheduleInfo } from '@/api/types'
// 输入参数
const props = defineProps({
// 是否允许刷新数据
allowRefresh: {
type: Boolean,
default: true,
},
})
// 定时服务列表
const schedulerList = ref<ScheduleInfo[]>([])
@@ -10,6 +19,9 @@ let refreshTimer: NodeJS.Timeout | null = null
// 调用API加载定时服务列表
async function loadSchedulerList() {
if (!props.allowRefresh) {
return
}
try {
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 type { DownloaderInfo } from '@/api/types'
// 输入参数
const props = defineProps({
// 是否允许刷新数据
allowRefresh: {
type: Boolean,
default: true,
},
})
// 定时器
let refreshTimer: NodeJS.Timeout | null = null
@@ -35,6 +44,10 @@ const infoItems = ref([
// 调用API查询下载器数据
async function loadDownloaderInfo() {
if (!props.allowRefresh) {
return
}
try {
const res: DownloaderInfo = await api.get('dashboard/downloader')

View File

@@ -80,7 +80,13 @@ const options = controlledComputed(
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>
import type { Context } from '@/api/types'
import TorrentItem from '@/components/cards/TorrentItem.vue'
import { list } from 'postcss'
import { useDisplay } from 'vuetify'
// 显示器宽度
@@ -35,6 +36,13 @@ const filterForm = reactive({
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')
@@ -71,7 +79,6 @@ function initOptions(data: Context) {
optionValue(resolutionFilterOptions.value, meta_info?.resource_pix)
}
// 对季过滤选项进行排序
const sortSeasonFilterOptions = computed(() => {
return seasonFilterOptions.value.sort((a, b) => {
@@ -168,26 +175,15 @@ onMounted(() => {
</VListItem>
</VList>
<VList v-if="dataList.length !== 0" lines="three" class="rounded p-0 torrent-list-vscroll shadow-lg">
<VVirtualScroll
: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)'
"
>
<VVirtualScroll :items="dataList" :style="listStyle">
<template #default="{ item }">
<TorrentItem :torrent="item" :key="`${item.torrent_info.page_url}`" />
</template>
</VVirtualScroll>
</VList>
</VCol>
<VCol xl="2" md="3" class="d-none d-md-block">
<VList
lines="one"
class="rounded shadow-lg"
style="block-size: calc(100vh - 6.5rem - env(safe-area-inset-bottom))"
>
<VCol xl="2" md="3" v-if="display.mdAndUp.value">
<VList lines="one" class="rounded shadow-lg" :style="listStyle">
<VListSubheader> 排序 </VListSubheader>
<VListItem>
<VChipGroup column v-model="sortField">

View File

@@ -347,7 +347,7 @@ onBeforeMount(async () => {
<transition name="fade-slide" appear>
<div>
<LoadingBanner v-if="!isRefreshed" class="mt-12" />
<div v-if="dataList.length > 0" class="grid gap-4 grid-plugin-card 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}`">
<PluginCard
:count="PluginStatistics[data.id || '0']"
@@ -417,7 +417,7 @@ onBeforeMount(async () => {
</VCol>
</VRow>
</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}`">
<PluginAppCard :plugin="data" :count="PluginStatistics[data.id || '0']" @install="pluginInstalled" />
</template>

View File

@@ -2,45 +2,57 @@
import api from '@/api'
import { FileItem, MediaDirectory } from '@/api/types'
import FileBrowser from '@/components/FileBrowser.vue'
import store from '@/store'
const endpoints = {
list: {
url: '/{storage}/list?path={path}&sort={sort}&fileid={fileid}&filetype={filetype}&pickcode={pickcode}',
method: 'get',
url: '/{storage}/list?sort={sort}',
method: 'post',
},
mkdir: {
url: '/{storage}/mkdir?path={path}&fileid={fileid}',
method: 'get',
url: '/{storage}/mkdir?name={name}',
method: 'post',
},
delete: {
url: '/{storage}/delete?path={path}&fileid={fileid}',
method: 'get',
url: '/{storage}/delete',
method: 'post',
},
download: {
url: '/{storage}/download?path={path}&fileid={fileid}&pickcode={pickcode}',
url: '/{storage}/download',
method: 'get',
},
image: {
url: '/{storage}/image?path={path}&fileid={fileid}&pickcode={pickcode}',
url: '/{storage}/image',
method: 'get',
},
rename: {
url: '/{storage}/rename?path={path}&new_name={newname}&fileid={fileid}&filetype={filetype}',
method: 'get',
url: '/{storage}/rename?new_name={newname}',
method: 'post',
},
}
// 当前目录
const path = ref<string>('')
const user_level = store.state.auth.level
// 当前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的堆栈
const fileidstack = ref<string[]>(['root'])
const itemstack = ref<FileItem[]>([
{
type: 'dir',
name: '/',
path: '/',
fileid: 'root',
},
])
// 下载目录列表
const downloadDirectories = ref<MediaDirectory[]>([])
@@ -73,7 +85,7 @@ function findCommonPath(paths: string[]): string {
}
if (commonPath.includes(':')) {
commonPath = commonPath.replace('/', '\\')
commonPath = commonPath.replace('\\', '/')
}
return commonPath
@@ -85,7 +97,23 @@ async function loadDownloadDirectories() {
const result: { [key: string]: any } = await api.get('system/setting/DownloadDirectories')
if (result.success && result.data?.value) {
downloadDirectories.value = result.data.value
path.value = findCommonPath(downloadDirectories.value.map(item => item.path) as string[])
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) {
console.log(error)
@@ -94,32 +122,28 @@ async function loadDownloadDirectories() {
// 目录变化
function pathChanged(item: FileItem) {
path.value = item.path
pickcode.value = item.pickcode || ''
if (item.fileid) {
fileid.value = item.fileid
if (fileidstack.value.includes(item.fileid)) {
fileidstack.value = fileidstack.value.slice(0, fileidstack.value.indexOf(item.fileid) + 1)
} else {
fileidstack.value.push(item.fileid)
}
operItem.value = item
const index = itemstack.value.findIndex(i => i.path === item.path)
if (index >= 0) {
itemstack.value = itemstack.value.slice(0, index + 1)
} else {
itemstack.value.push(item)
}
}
// 加载初始目录
onBeforeMount(loadDownloadDirectories)
</script>
<template>
<div>
<FileBrowser
storages="local,aliyun,u115"
:storages="userStorage"
:tree="false"
:path="path"
:fileid="fileid"
:pickcode="pickcode"
:fileidstack="fileidstack"
:itemstack="itemstack"
:endpoints="endpoints"
:axios="api"
:item="operItem"
@pathchanged="pathChanged"
/>
</div>

View File

@@ -124,6 +124,12 @@ const TransferDict: { [key: string]: string } = {
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 begin = itemsPerPage.value * (currentPage.value - 1) + 1
@@ -142,12 +148,19 @@ const totalPage = computed(() => {
// 切换页签和搜索词
watch(
[() => currentPage.value, () => itemsPerPage.value, () => search.value],
[() => currentPage.value, () => itemsPerPage.value],
debounce(async () => {
reloadPage()
}, 1000),
)
watch(
[() => search.value],
debounce(async () => {
reloadPage(true)
}, 1000),
)
// 获取订阅列表数据
async function fetchData(page = currentPage.value, count = itemsPerPage.value) {
loading.value = true
@@ -329,7 +342,7 @@ function addUrlQuery(url: string, name: string, value: any) {
}
// 重载页面
function reloadPage() {
function reloadPage(resetPage = false) {
let url = '/history'
if (search.value) {
url = addUrlQuery(url, 'search', search.value)
@@ -338,7 +351,7 @@ function reloadPage() {
url = addUrlQuery(url, 'itemsPerPage', itemsPerPage.value)
}
if (currentPage.value) {
url = addUrlQuery(url, 'currentPage', currentPage.value)
url = addUrlQuery(url, 'currentPage', resetPage ? 1 : currentPage.value)
}
router.push(url)
}
@@ -347,7 +360,7 @@ function reloadPage() {
function ensureNumber(value: any, defaultValue: number = 0) {
value = Number(value)
// 如果不是数字
if (value !== value) {
if (Number.isNaN(value)) {
value = defaultValue
}
return value
@@ -394,11 +407,7 @@ onMounted(fetchData)
show-select
loading-text="加载中..."
hover
:style="
appMode
? 'height: calc(100vh - 15.5rem - env(safe-area-inset-bottom) - 3.5rem)'
: 'height: calc(100vh - 14.5rem - env(safe-area-inset-bottom)'
"
:style="tableStyle"
>
<template #item.title="{ item }">
<div class="d-flex align-center">

View File

@@ -6,6 +6,10 @@ import { requiredValidator } from '@/@validators'
import api from '@/api'
import type { User } from '@/api/types'
import avatar1 from '@images/avatars/avatar-1.png'
import { useDisplay } from 'vuetify'
// 显示器宽度
const display = useDisplay()
const isNewPasswordVisible = ref(false)
const isConfirmPasswordVisible = ref(false)
@@ -250,7 +254,7 @@ onMounted(() => {
<div class="d-flex flex-wrap gap-2">
<VBtn color="primary" @click="refInputEl?.click()">
<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>
<input
@@ -264,7 +268,7 @@ onMounted(() => {
<VBtn type="reset" color="error" variant="tonal" @click="resetAvatar">
<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
@@ -273,7 +277,9 @@ onMounted(() => {
@click.stop="accountInfo.is_otp ? disableOtp() : getOtpUri()"
>
<VIcon icon="mdi-account-key" />
<span class="d-none d-sm-block ms-2">{{ accountInfo.is_otp ? '关闭验证' : '双重验证' }}</span>
<span v-if="display.mdAndUp.value" class="ms-2">{{
accountInfo.is_otp ? '关闭验证' : '双重验证'
}}</span>
</VBtn>
</div>

View File

@@ -137,7 +137,7 @@ function addDownloadDirectory() {
downloadDirectories.value.push({
name: `下载目录${downloadDirectories.value.length + 1}`,
path: '',
media_type: '全部',
media_type: '',
category: '',
})
}
@@ -181,7 +181,7 @@ function addLibraryDirectory() {
libraryDirectories.value.push({
name: `媒体库目录${libraryDirectories.value.length + 1}`,
path: '',
media_type: '全部',
media_type: '',
category: '',
scrape: true,
})

View File

@@ -1,12 +1,19 @@
<script setup lang="ts">
import api from '@/api'
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 store from '@/store'
// 路由
const router = useRouter()
// 超级用户
const superUser = store.state.auth.superUser
// 当前用户名
const userName = store.state.auth.userName
// 定义事件
const emit = defineEmits(['close'])
@@ -74,6 +81,7 @@ function getMenus(): NavMenu[] {
// 匹配的菜单列表
const matchedMenuItems = computed(() => {
if (!searchWord.value) return []
if (!superUser) return []
const lowerWord = (searchWord.value as string).toLowerCase()
const menuItems = getMenus()
if (menuItems)
@@ -104,6 +112,7 @@ async function fetchInstalledPlugins() {
// 区配的插件列表
const matchedPluginItems = computed(() => {
if (!searchWord.value) return []
if (!superUser) return []
const lowerWord = (searchWord.value as string).toLowerCase()
return pluginItems.value.filter((item: Plugin) => {
if (!item.plugin_name && !item.plugin_desc) return false
@@ -114,7 +123,7 @@ const matchedPluginItems = computed(() => {
// 所有订阅数据
const SubscribeItems = ref<Subscribe[]>([])
// 获取电影订阅列表数据
// 获取订阅列表数据
async function fetchSubscribes() {
try {
SubscribeItems.value = await api.get('subscribe/')
@@ -128,7 +137,7 @@ const matchedSubscribeItems = computed(() => {
if (!searchWord.value) return []
const lowerWord = (searchWord.value as string).toLowerCase()
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>
</template>
</VHover>
<VHover>
<VHover v-if="superUser">
<template #default="hover">
<VListItem prepend-icon="mdi-history" link v-bind="hover.props" @click="searchHistory">
<VListItemTitle class="break-words whitespace-break-spaces">
@@ -382,7 +391,7 @@ onMounted(() => {
</div>
</VCol>
</VRow>
<VRow>
<VRow v-if="superUser">
<VCol cols="12" md="6">
<p class="custom-letter-spacing text-sm text-disabled text-uppercase py-2 px-4 mb-0">常用功能</p>
<VList lines="one">