mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-05-08 01:03:27 +08:00
Compare commits
79 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6fdbc8104c | ||
|
|
433c14679c | ||
|
|
fcaa4476f0 | ||
|
|
85c5c3058c | ||
|
|
035122a08e | ||
|
|
0a76875f8e | ||
|
|
218eac54ce | ||
|
|
84deeff4f5 | ||
|
|
0c72d026f6 | ||
|
|
aec9ea83c5 | ||
|
|
effd13aedd | ||
|
|
42b43d65d7 | ||
|
|
c501d824dd | ||
|
|
384ac2faf1 | ||
|
|
dd2c4dd24b | ||
|
|
356ffddb1c | ||
|
|
de69be7c4e | ||
|
|
e962f555ae | ||
|
|
1987246585 | ||
|
|
393264f66b | ||
|
|
9b50020b3b | ||
|
|
5e5545fe01 | ||
|
|
0e8da35b0a | ||
|
|
4d2cf73330 | ||
|
|
5df89f2ce4 | ||
|
|
045c0b4c0c | ||
|
|
8b4ffa0795 | ||
|
|
14359a37ae | ||
|
|
a8e4a1c2e0 | ||
|
|
9048d181af | ||
|
|
1cb02994bf | ||
|
|
6fad85e957 | ||
|
|
db9b2ee6b3 | ||
|
|
8efeb77102 | ||
|
|
0215a800e2 | ||
|
|
87d282f98b | ||
|
|
60c392d3d0 | ||
|
|
34c3aa25da | ||
|
|
80690d4cc8 | ||
|
|
18f3dc2d44 | ||
|
|
e8256b4e1a | ||
|
|
4f67bb0250 | ||
|
|
5dd071adf4 | ||
|
|
aaf5e7f49d | ||
|
|
6a5958409a | ||
|
|
e0ff98b1d7 | ||
|
|
a815e07cdd | ||
|
|
aa2fe9740c | ||
|
|
75a358a4d2 | ||
|
|
d5646be6f8 | ||
|
|
cb04ebcd95 | ||
|
|
9889ccfc74 | ||
|
|
f528bd861a | ||
|
|
f793654bd8 | ||
|
|
8d064a2165 | ||
|
|
1240899b08 | ||
|
|
558752b890 | ||
|
|
997548b7d6 | ||
|
|
865d597fe8 | ||
|
|
b0a043b464 | ||
|
|
e003b6f9a7 | ||
|
|
9e9e940dfd | ||
|
|
d6dac704eb | ||
|
|
9aa8dff650 | ||
|
|
14c2503b0d | ||
|
|
cb282c6f9a | ||
|
|
66a5a40482 | ||
|
|
8d211ed20b | ||
|
|
bbf2814285 | ||
|
|
a15e479a3e | ||
|
|
505d6ec010 | ||
|
|
314ac65e23 | ||
|
|
118a9a2c5d | ||
|
|
347f47bbef | ||
|
|
a73c35468d | ||
|
|
f9a1446ed5 | ||
|
|
874ba45034 | ||
|
|
febe08eb9d | ||
|
|
9123b34c82 |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "moviepilot",
|
||||
"version": "1.9.6",
|
||||
"version": "1.9.17",
|
||||
"private": true,
|
||||
"bin": "dist/service.js",
|
||||
"scripts": {
|
||||
@@ -101,4 +101,4 @@
|
||||
"resolutions": {
|
||||
"postcss": "8"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,8 @@ import modeYamlUrl from 'ace-builds/src-noconflict/mode-yaml?url'
|
||||
|
||||
import modeCssUrl from 'ace-builds/src-noconflict/mode-css?url'
|
||||
|
||||
import 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')
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -746,17 +746,27 @@ export interface FileItem {
|
||||
// 文件名
|
||||
name: string
|
||||
// 文件名不含扩展名
|
||||
basename: string
|
||||
basename?: string
|
||||
// 文件路径
|
||||
path: string
|
||||
// 文件扩展名
|
||||
extension: string
|
||||
extension?: string
|
||||
// 文件大小
|
||||
size: number
|
||||
size?: number
|
||||
// 文件子元素
|
||||
children: FileItem[]
|
||||
children?: FileItem[]
|
||||
// 文件创建时间
|
||||
modify_time: number
|
||||
modify_time?: number
|
||||
// 文件ID
|
||||
fileid?: string
|
||||
// 上级文件ID
|
||||
parent_fileid?: string
|
||||
// 缩略图
|
||||
thumbnail?: string
|
||||
// pickcode
|
||||
pickcode?: string
|
||||
// drive_id
|
||||
drive_id?: string
|
||||
}
|
||||
|
||||
// 媒体服务器播放条目
|
||||
|
||||
@@ -1,19 +1,28 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Axios } from 'axios'
|
||||
import axios from 'axios'
|
||||
import FileList from './filebrowser/FileList.vue'
|
||||
import FileToolbar from './filebrowser/FileToolbar.vue'
|
||||
import type { EndPoints } from '@/api/types'
|
||||
import type { EndPoints, FileItem } from '@/api/types'
|
||||
import api from '@/api'
|
||||
import AliyunAuthDialog from './dialog/AliyunAuthDialog.vue'
|
||||
import U115AuthDialog from './dialog/U115AuthDialog.vue'
|
||||
import { isNullOrEmptyObject } from '@/@core/utils'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
storages: String,
|
||||
storage: String,
|
||||
path: String,
|
||||
tree: Boolean,
|
||||
endpoints: Object as PropType<EndPoints>,
|
||||
axios: Object as PropType<Axios>,
|
||||
axios: {
|
||||
type: Object as PropType<Axios>,
|
||||
required: true,
|
||||
},
|
||||
axiosconfig: Object,
|
||||
item: {
|
||||
type: Object as PropType<FileItem>,
|
||||
required: true,
|
||||
},
|
||||
itemstack: Array as PropType<FileItem[]>,
|
||||
})
|
||||
|
||||
// 对外事件
|
||||
@@ -25,6 +34,16 @@ const availableStorages = [
|
||||
code: 'local',
|
||||
icon: 'mdi-folder-multiple-outline',
|
||||
},
|
||||
{
|
||||
name: '阿里云盘',
|
||||
code: 'aliyun',
|
||||
icon: 'mdi-cloud-outline',
|
||||
},
|
||||
{
|
||||
name: '115网盘',
|
||||
code: 'u115',
|
||||
icon: 'mdi-cloud-outline',
|
||||
},
|
||||
]
|
||||
|
||||
const fileIcons = {
|
||||
@@ -57,8 +76,14 @@ const activeStorage = ref('local')
|
||||
const refreshPending = ref(false)
|
||||
// 排序
|
||||
const sort = ref('name')
|
||||
// axios实例
|
||||
const axiosInstance = ref<Axios>()
|
||||
// 阿里云盘认证对话框
|
||||
const aliyunAuthDialog = ref(false)
|
||||
// 阿里云盘用户信息
|
||||
const aliyunUserInfo = ref<{ [key: string]: any }>({})
|
||||
// 115网盘认证对话框
|
||||
const u115AuthDialog = ref(false)
|
||||
// 115网盘用户信息
|
||||
const u115UserInfo = ref<{ [key: string]: any }>({})
|
||||
|
||||
// 计算属性
|
||||
const storagesArray = computed(() => {
|
||||
@@ -68,19 +93,56 @@ const storagesArray = computed(() => {
|
||||
|
||||
// 方法
|
||||
function loadingChanged(loading: number) {
|
||||
if (loading)
|
||||
loading++
|
||||
else if (loading > 0)
|
||||
loading--
|
||||
if (loading) loading++
|
||||
else if (loading > 0) loading--
|
||||
}
|
||||
|
||||
function storageChanged(storage: string) {
|
||||
// 查询阿里云
|
||||
async function loadAliyunUserInfo() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('aliyun/userinfo')
|
||||
if (result.success) {
|
||||
aliyunUserInfo.value = result
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 查询115
|
||||
async function loadU115UserInfo() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('u115/storage')
|
||||
if (result.success) {
|
||||
u115UserInfo.value = result
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 存储切换
|
||||
async function storageChanged(storage: string) {
|
||||
if (storage == 'aliyun') {
|
||||
await loadAliyunUserInfo()
|
||||
if (isNullOrEmptyObject(aliyunUserInfo.value)) {
|
||||
aliyunAuthDialog.value = true
|
||||
return
|
||||
}
|
||||
} else if (storage == 'u115') {
|
||||
await loadU115UserInfo()
|
||||
if (isNullOrEmptyObject(u115UserInfo.value)) {
|
||||
u115AuthDialog.value = true
|
||||
return
|
||||
}
|
||||
}
|
||||
activeStorage.value = storage
|
||||
emit('pathchanged', { path: '/', fileid: 'root' })
|
||||
}
|
||||
|
||||
// 路径变化
|
||||
function pathChanged(_path: string) {
|
||||
emit('pathchanged', _path)
|
||||
function pathChanged(item: FileItem) {
|
||||
emit('pathchanged', item)
|
||||
}
|
||||
|
||||
// 排序变化
|
||||
@@ -89,33 +151,40 @@ function sortChanged(s: string) {
|
||||
refreshPending.value = true
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
activeStorage.value = props.storage ?? 'local'
|
||||
axiosInstance.value = props.axios ?? axios.create(props.axiosconfig)
|
||||
})
|
||||
// aliyun认证完成
|
||||
function aliyunAuthDone() {
|
||||
aliyunAuthDialog.value = false
|
||||
activeStorage.value = 'aliyun'
|
||||
}
|
||||
|
||||
// u115认证完成
|
||||
function u115AuthDone() {
|
||||
u115AuthDialog.value = false
|
||||
activeStorage.value = 'u115'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard class="mx-auto" :loading="loading > 0 || !path">
|
||||
<div v-if="path">
|
||||
<VCard class="mx-auto" :loading="loading > 0">
|
||||
<div v-if="activeStorage && item">
|
||||
<FileToolbar
|
||||
:path="path"
|
||||
:item="item"
|
||||
:itemstack="itemstack"
|
||||
:storages="storagesArray"
|
||||
:storage="activeStorage"
|
||||
:endpoints="endpoints"
|
||||
:axios="axiosInstance"
|
||||
:axios="axios"
|
||||
@storagechanged="storageChanged"
|
||||
@pathchanged="pathChanged"
|
||||
@foldercreated="refreshPending = true"
|
||||
@sortchanged="sortChanged"
|
||||
/>
|
||||
<FileList
|
||||
:path="path"
|
||||
:item="item"
|
||||
:storage="activeStorage"
|
||||
:icons="fileIcons"
|
||||
:endpoints="endpoints"
|
||||
:axios="axiosInstance"
|
||||
:axios="axios"
|
||||
:refreshpending="refreshPending"
|
||||
:sort="sort"
|
||||
@pathchanged="pathChanged"
|
||||
@@ -126,4 +195,11 @@ onMounted(() => {
|
||||
/>
|
||||
</div>
|
||||
</VCard>
|
||||
<AliyunAuthDialog
|
||||
v-if="aliyunAuthDialog"
|
||||
v-model="aliyunAuthDialog"
|
||||
@close="aliyunAuthDialog = false"
|
||||
@done="aliyunAuthDone"
|
||||
/>
|
||||
<U115AuthDialog v-if="u115AuthDialog" v-model="u115AuthDialog" @close="u115AuthDialog = false" @done="u115AuthDone" />
|
||||
</template>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
<!-- 插件配置页面 -->
|
||||
|
||||
@@ -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">
|
||||
|
||||
109
src/components/dialog/AliyunAuthDialog.vue
Normal file
109
src/components/dialog/AliyunAuthDialog.vue
Normal file
@@ -0,0 +1,109 @@
|
||||
<script lang="ts" setup>
|
||||
import QrcodeVue from 'qrcode.vue'
|
||||
import api from '@/api'
|
||||
|
||||
// 定义事件
|
||||
const emit = defineEmits(['done', 'close'])
|
||||
|
||||
// 二维码内容
|
||||
const qrCodeContent = ref('')
|
||||
|
||||
// ck参数
|
||||
const ck = ref('')
|
||||
|
||||
// t参数
|
||||
const t = ref('')
|
||||
|
||||
// 下方的提示信息
|
||||
const text = ref('请用阿里云盘 App 扫码')
|
||||
|
||||
// 提醒类型
|
||||
const alertType = ref<'success' | 'info' | 'error' | 'warning' | undefined>('info')
|
||||
|
||||
// timeout定时器
|
||||
let timeoutTimer: NodeJS.Timeout | undefined = undefined
|
||||
|
||||
// 完成
|
||||
async function handleDone() {
|
||||
emit('done')
|
||||
}
|
||||
|
||||
// 调用/aliyun/qrcode api生成二维码
|
||||
async function getQrcode() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('/aliyun/qrcode')
|
||||
if (result.success && result.data) {
|
||||
qrCodeContent.value = result.data.codeContent
|
||||
ck.value = result.data.ck
|
||||
t.value = result.data.t
|
||||
} else {
|
||||
text.value = result.message
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
// 调用/aliyun/check api验证二维码
|
||||
async function checkQrcode() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('/aliyun/check', {
|
||||
params: {
|
||||
ck: ck.value,
|
||||
t: t.value,
|
||||
},
|
||||
})
|
||||
if (result.success && result.data) {
|
||||
const qrCodeStatus = result.data.qrCodeStatus
|
||||
text.value = result.data.tip
|
||||
if (qrCodeStatus == 'CONFIRMED') {
|
||||
// 已确认完成
|
||||
alertType.value = 'success'
|
||||
handleDone()
|
||||
} else if (qrCodeStatus == 'NEW' || qrCodeStatus == 'SCANED') {
|
||||
alertType.value = 'info'
|
||||
// 新建、待扫码
|
||||
clearTimeout(timeoutTimer)
|
||||
timeoutTimer = setTimeout(checkQrcode, 3000)
|
||||
} else {
|
||||
// 过期或者已取消
|
||||
alertType.value = 'error'
|
||||
}
|
||||
} else {
|
||||
alertType.value = 'error'
|
||||
text.value = result.message
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await getQrcode()
|
||||
timeoutTimer = setTimeout(checkQrcode, 3000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timeoutTimer) clearTimeout(timeoutTimer)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VDialog width="40rem" scrollable max-height="85vh">
|
||||
<VCard title="阿里云盘登录" class="rounded-t">
|
||||
<DialogCloseBtn @click="emit('close')" />
|
||||
<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 />
|
||||
</VAlert>
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
<VBtn variant="elevated" @click="handleDone" prepend-icon="mdi-check" class="px-5 me-3"> 完成 </VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
@@ -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="整理方式"
|
||||
|
||||
98
src/components/dialog/U115AuthDialog.vue
Normal file
98
src/components/dialog/U115AuthDialog.vue
Normal file
@@ -0,0 +1,98 @@
|
||||
<script lang="ts" setup>
|
||||
import QrcodeVue from 'qrcode.vue'
|
||||
import api from '@/api'
|
||||
|
||||
// 定义事件
|
||||
const emit = defineEmits(['done', 'close'])
|
||||
|
||||
// 二维码内容
|
||||
const qrCodeContent = ref('')
|
||||
|
||||
// 下方的提示信息
|
||||
const text = ref('请使用微信或115客户端扫码')
|
||||
|
||||
// 提醒类型
|
||||
const alertType = ref<'success' | 'info' | 'error' | 'warning' | undefined>('info')
|
||||
|
||||
// timeout定时器
|
||||
let timeoutTimer: NodeJS.Timeout | undefined = undefined
|
||||
|
||||
// 完成
|
||||
async function handleDone() {
|
||||
emit('done')
|
||||
}
|
||||
|
||||
// 调用/aliyun/qrcode api生成二维码
|
||||
async function getQrcode() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('/u115/qrcode')
|
||||
if (result.success && result.data) {
|
||||
qrCodeContent.value = result.data.codeContent
|
||||
} else {
|
||||
text.value = result.message
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
// 调用/aliyun/check api验证二维码
|
||||
async function checkQrcode() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('/u115/check')
|
||||
if (result.success && result.data) {
|
||||
const status = result.data.status
|
||||
text.value = result.data.tip
|
||||
if (status == 1) {
|
||||
// 已确认完成
|
||||
alertType.value = 'success'
|
||||
handleDone()
|
||||
} else if (status == 0) {
|
||||
alertType.value = 'info'
|
||||
// 新建、待扫码
|
||||
clearTimeout(timeoutTimer)
|
||||
timeoutTimer = setTimeout(checkQrcode, 3000)
|
||||
} else {
|
||||
// 过期或者已取消
|
||||
alertType.value = 'error'
|
||||
}
|
||||
} else {
|
||||
alertType.value = 'error'
|
||||
text.value = result.message
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await getQrcode()
|
||||
timeoutTimer = setTimeout(checkQrcode, 3000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timeoutTimer) clearTimeout(timeoutTimer)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VDialog width="40rem" scrollable max-height="85vh">
|
||||
<VCard title="115网盘登录" class="rounded-t">
|
||||
<DialogCloseBtn @click="emit('close')" />
|
||||
<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 />
|
||||
</VAlert>
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
<VBtn variant="elevated" @click="handleDone" prepend-icon="mdi-check" class="px-5 me-3"> 完成 </VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
@@ -1,8 +1,7 @@
|
||||
<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 axios from 'axios'
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import ReorganizeDialog from '../dialog/ReorganizeDialog.vue'
|
||||
import { formatBytes } from '@core/utils/formatters'
|
||||
@@ -25,22 +24,37 @@ const appMode = computed(() => {
|
||||
const inProps = defineProps({
|
||||
icons: Object,
|
||||
storage: String,
|
||||
path: String,
|
||||
endpoints: Object as PropType<EndPoints>,
|
||||
axios: Object as PropType<Axios>,
|
||||
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)
|
||||
|
||||
// 重命名loading
|
||||
const renameLoading = ref(false)
|
||||
|
||||
// 识别进度条
|
||||
const progressDialog = ref(false)
|
||||
|
||||
@@ -50,15 +64,6 @@ const progressText = ref('请稍候 ...')
|
||||
// 识别进度
|
||||
const progressValue = ref(0)
|
||||
|
||||
// 确认框
|
||||
const createConfirm = useConfirm()
|
||||
|
||||
// 存储空间类型
|
||||
const storage = ref(inProps.storage ?? '')
|
||||
|
||||
// axios实例
|
||||
const axiosInstance = ref<Axios>(inProps.axios ?? axios)
|
||||
|
||||
// 内容列表
|
||||
const items = ref<FileItem[]>([])
|
||||
|
||||
@@ -74,170 +79,358 @@ const transferPopper = ref(false)
|
||||
// 新名称
|
||||
const newName = ref('')
|
||||
|
||||
// 当前名称
|
||||
// 处理目录内所有文件
|
||||
const renameAll = ref(false)
|
||||
|
||||
// 当前操作项
|
||||
const currentItem = ref<FileItem>()
|
||||
|
||||
// 选中的项目
|
||||
const selected = ref<FileItem[]>([])
|
||||
|
||||
// 识别结果
|
||||
const nameTestResult = ref<Context>()
|
||||
|
||||
// 识别结果对话框
|
||||
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.basename.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.basename.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, storage.value)
|
||||
.replace(/{path}/g, encodeURIComponent(inProps.path || ''))
|
||||
.replace(/{storage}/g, inProps.storage)
|
||||
.replace(/{sort}/g, inProps.sort || 'name')
|
||||
|
||||
const config = {
|
||||
const config: AxiosRequestConfig<FileItem> = {
|
||||
url,
|
||||
method: inProps.endpoints?.list.method || 'get',
|
||||
data: inProps.item,
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
items.value = (await axiosInstance.value.request(config)) ?? []
|
||||
items.value = (await inProps.axios.request(config)) ?? []
|
||||
emit('loading', false)
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
// 删除项目
|
||||
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.basename}?`,
|
||||
content: `是否确认删除选中的 ${selected.value.length} 个项目?`,
|
||||
})
|
||||
|
||||
if (confirmed) {
|
||||
emit('loading', true)
|
||||
const url = inProps.endpoints?.delete.url
|
||||
.replace(/{storage}/g, storage.value)
|
||||
.replace(/{path}/g, encodeURIComponent(item.path))
|
||||
if (!confirmed) return
|
||||
|
||||
const config = {
|
||||
url,
|
||||
method: inProps.endpoints?.delete.method || 'post',
|
||||
}
|
||||
// 显示进度条
|
||||
progressDialog.value = true
|
||||
progressValue.value = 0
|
||||
|
||||
await axiosInstance.value.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(_path: string) {
|
||||
emit('pathchanged', _path)
|
||||
function changePath(item: FileItem) {
|
||||
item.path = inProps.item.path + item.name + (item.type === 'dir' ? '/' : '')
|
||||
emit('pathchanged', item)
|
||||
}
|
||||
|
||||
// 点击列表项
|
||||
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)
|
||||
}
|
||||
|
||||
// 新窗口中下载文件
|
||||
function download(path: string) {
|
||||
if (!path) return
|
||||
const token = store.state.auth.token
|
||||
const url_path = inProps.endpoints?.download.url
|
||||
.replace(/{storage}/g, storage.value)
|
||||
.replace(/{path}/g, encodeURIComponent(path))
|
||||
const url = `${import.meta.env.VITE_API_BASE_URL}${url_path.slice(1)}&token=${token}`
|
||||
// 下载文件
|
||||
window.open(url, '_blank')
|
||||
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(path: string) {
|
||||
if (!path) return ''
|
||||
const token = store.state.auth.token
|
||||
const url_path = inProps.endpoints?.image.url
|
||||
.replace(/{storage}/g, storage.value)
|
||||
.replace(/{path}/g, encodeURIComponent(path))
|
||||
return `${import.meta.env.VITE_API_BASE_URL}${url_path.slice(1)}&token=${token}`
|
||||
// 获取图片地址
|
||||
function getImgLink(item: FileItem) {
|
||||
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}`
|
||||
}
|
||||
|
||||
// 显示重命名弹窗
|
||||
function showRenmae(item: FileItem) {
|
||||
currentItem.value = item
|
||||
newName.value = item.name
|
||||
renameAll.value = false
|
||||
renamePopper.value = true
|
||||
}
|
||||
|
||||
// 调用API获取新名称
|
||||
async function get_recommend_name() {
|
||||
renameLoading.value = true
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('transfer/name', {
|
||||
params: {
|
||||
path: `${inProps.item.path}${currentItem.value?.name}`,
|
||||
filetype: currentItem.value?.type ?? 'file',
|
||||
},
|
||||
})
|
||||
if (result.success && result.data) {
|
||||
newName.value = result.data.name
|
||||
} else {
|
||||
$toast.error(result.message)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
renameLoading.value = false
|
||||
}
|
||||
|
||||
// 重命名
|
||||
async function rename() {
|
||||
emit('loading', true)
|
||||
const url = inProps.endpoints?.rename.url
|
||||
.replace(/{storage}/g, inProps.storage)
|
||||
.replace(/{path}/g, encodeURIComponent(currentItem.value?.path || ''))
|
||||
.replace(/{newname}/g, encodeURIComponent(newName.value))
|
||||
|
||||
const config = {
|
||||
url,
|
||||
method: inProps.endpoints?.mkdir.method || 'post',
|
||||
// 关闭弹窗
|
||||
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
|
||||
await inProps.axios?.request(config)
|
||||
let url = inProps.endpoints?.rename.url
|
||||
.replace(/{storage}/g, inProps.storage)
|
||||
.replace(/{newname}/g, encodeURIComponent(newName.value))
|
||||
if (renameAll.value) {
|
||||
url += '&recursive=true'
|
||||
}
|
||||
|
||||
renamePopper.value = false
|
||||
newName.value = ''
|
||||
emit('loading', false)
|
||||
const config: AxiosRequestConfig<FileItem> = {
|
||||
url,
|
||||
method: inProps.endpoints?.rename.method || 'post',
|
||||
data: currentItem.value,
|
||||
}
|
||||
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变化
|
||||
watch(
|
||||
() => inProps.path,
|
||||
async () => {
|
||||
items.value = []
|
||||
nameTestResult.value = undefined
|
||||
nameTestDialog.value = false
|
||||
await load()
|
||||
},
|
||||
)
|
||||
|
||||
// 监听refreshPending变化
|
||||
watch(
|
||||
() => inProps.refreshpending,
|
||||
async () => {
|
||||
if (inProps.refreshpending) {
|
||||
await load()
|
||||
await list_files()
|
||||
emit('refreshed')
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// 监听item变化或者storage变化
|
||||
watch(
|
||||
[() => inProps.item, () => inProps.storage],
|
||||
async () => {
|
||||
// 清空列表
|
||||
items.value = []
|
||||
// 关闭弹窗
|
||||
nameTestResult.value = undefined
|
||||
nameTestDialog.value = false
|
||||
// 重置菜单
|
||||
dropdownItems.value = [
|
||||
{
|
||||
title: '识别',
|
||||
value: 1,
|
||||
show: true,
|
||||
props: {
|
||||
prependIcon: 'mdi-text-recognition',
|
||||
click: (_item: FileItem) => {
|
||||
recognize(_item.path || '')
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '刮削',
|
||||
value: 2,
|
||||
show: true,
|
||||
props: {
|
||||
prependIcon: 'mdi-auto-fix',
|
||||
click: (_item: FileItem) => {
|
||||
scrape(_item)
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '重命名',
|
||||
value: 3,
|
||||
show: true,
|
||||
props: {
|
||||
prependIcon: 'mdi-rename',
|
||||
click: showRenmae,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '整理',
|
||||
value: 4,
|
||||
show: true,
|
||||
props: {
|
||||
prependIcon: 'mdi-folder-arrow-right',
|
||||
click: showTransfer,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '删除',
|
||||
value: 5,
|
||||
show: true,
|
||||
props: {
|
||||
prependIcon: 'mdi-delete-outline',
|
||||
color: 'error',
|
||||
click: deleteItem,
|
||||
},
|
||||
},
|
||||
]
|
||||
await list_files()
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
// 调用API识别
|
||||
async function recognize(path: string) {
|
||||
try {
|
||||
@@ -260,75 +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)
|
||||
}
|
||||
}
|
||||
// 弹出菜单
|
||||
const dropdownItems = ref([
|
||||
{
|
||||
title: '识别',
|
||||
value: 1,
|
||||
props: {
|
||||
prependIcon: 'mdi-text-recognition',
|
||||
click: (_item: FileItem) => {
|
||||
recognize(_item.path || '')
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '刮削',
|
||||
value: 2,
|
||||
props: {
|
||||
prependIcon: 'mdi-auto-fix',
|
||||
click: (_item: FileItem) => {
|
||||
scrape(_item.path || '')
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '重命名',
|
||||
value: 3,
|
||||
props: {
|
||||
prependIcon: 'mdi-rename',
|
||||
click: showRenmae,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '整理',
|
||||
value: 4,
|
||||
props: {
|
||||
prependIcon: 'mdi-folder-arrow-right',
|
||||
click: showTransfer,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '删除',
|
||||
value: 5,
|
||||
props: {
|
||||
prependIcon: 'mdi-delete-outline',
|
||||
color: 'error',
|
||||
click: deleteItem,
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
// 批量刮削
|
||||
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>
|
||||
|
||||
@@ -348,106 +537,131 @@ 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" @click="download(inProps.path || '')">
|
||||
<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" />
|
||||
</VCardText>
|
||||
<VCardText v-if="!path" class="grow d-flex justify-center align-center grey--text"> 选择目录或文件 </VCardText>
|
||||
<VCardText v-else-if="isFile && !isImage" class="text-center break-all">
|
||||
<strong>{{ items[0]?.name }}</strong
|
||||
><br />
|
||||
大小:{{ formatBytes(items[0]?.size || 0) }}<br />
|
||||
修改时间:{{ formatTime(items[0]?.modify_time || 0) }}
|
||||
<!-- 文件详情 -->
|
||||
<VCardText v-else-if="isFile && !isImage && items.length > 0" class="text-center break-all">
|
||||
<div v-if="items[0]?.thumbnail" class="flex justify-center">
|
||||
<VImg max-width="15rem" cover :src="items[0]?.thumbnail" class="rounded border shadow-lg">
|
||||
<template #placeholder>
|
||||
<VSkeletonLoader class="object-cover w-full h-full" />
|
||||
</template>
|
||||
</VImg>
|
||||
</div>
|
||||
<div class="text-xl text-high-emphasis mt-3">{{ items[0]?.name }}</div>
|
||||
<p class="mt-2" v-if="items[0]?.size && items[0].modify_time">
|
||||
大小:{{ formatBytes(items[0]?.size || 0) }}<br />
|
||||
修改时间:{{ formatTime(items[0]?.modify_time || 0) }}
|
||||
</p>
|
||||
</VCardText>
|
||||
<VCardText v-else-if="isFile && isImage" class="grow d-flex justify-center align-center">
|
||||
<VImg :src="getImgLink(path)" max-width="100%" max-height="100%" />
|
||||
<!-- 图片 -->
|
||||
<VCardText v-else-if="isFile && isImage && items.length > 0" class="grow d-flex justify-center align-center">
|
||||
<VImg :src="getImgLink(items[0])" max-width="100%" max-height="100%" />
|
||||
</VCardText>
|
||||
<!-- 目录和文件列表 -->
|
||||
<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.path)">
|
||||
<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>
|
||||
<VListItem
|
||||
v-for="(menu, i) in dropdownItems"
|
||||
:key="i"
|
||||
variant="plain"
|
||||
:base-color="menu.props.color"
|
||||
@click="menu.props.click(item)"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon :icon="menu.props.prependIcon" />
|
||||
</template>
|
||||
<VListItemTitle v-text="menu.title" />
|
||||
</VListItem>
|
||||
<template v-for="(menu, i) in dropdownItems" :key="i">
|
||||
<VListItem
|
||||
v-if="menu.show"
|
||||
variant="plain"
|
||||
:base-color="menu.props.color"
|
||||
@click="menu.props.click(item)"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon :icon="menu.props.prependIcon" />
|
||||
</template>
|
||||
<VListItemTitle v-text="menu.title" />
|
||||
</VListItem>
|
||||
</template>
|
||||
</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="刮削">
|
||||
<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="整理">
|
||||
<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>
|
||||
@@ -469,13 +683,25 @@ onMounted(() => {
|
||||
<!-- 重命名弹窗 -->
|
||||
<VDialog v-if="renamePopper" v-model="renamePopper" max-width="50rem">
|
||||
<VCard title="重命名">
|
||||
<DialogCloseBtn @click="renamePopper = false" />
|
||||
<VDivider />
|
||||
<VCardText>
|
||||
<VTextField v-model="newName" label="名称" />
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VTextField v-model="newName" label="新名称" :loading="renameLoading" />
|
||||
</VCol>
|
||||
<VCol cols="12" md="6" v-if="currentItem && currentItem.type == 'dir'">
|
||||
<VSwitch v-model="renameAll" label="自动重命名目录内所有媒体文件" />
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VBtn depressed @click="renamePopper = false"> 取消 </VBtn>
|
||||
<VSpacer />
|
||||
<VBtn :disabled="!newName" depressed variant="tonal" @click="rename"> 重命名 </VBtn>
|
||||
<VBtn color="success" variant="elevated" @click="get_recommend_name" prepend-icon="mdi-magic" class="px-5 me-3">
|
||||
自动识别名称
|
||||
</VBtn>
|
||||
<VBtn :disabled="!newName" variant="elevated" @click="rename" prepend-icon="mdi-check" class="px-5 me-3">
|
||||
确定
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
@@ -483,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"
|
||||
/>
|
||||
<!-- 进度框 -->
|
||||
|
||||
@@ -1,14 +1,28 @@
|
||||
<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,
|
||||
item: {
|
||||
type: Object as PropType<FileItem>,
|
||||
required: true,
|
||||
},
|
||||
itemstack: {
|
||||
type: Array as PropType<FileItem[]>,
|
||||
required: true,
|
||||
},
|
||||
endpoints: Object as PropType<EndPoints>,
|
||||
axios: Object as PropType<Axios>,
|
||||
axios: {
|
||||
type: Object as PropType<Axios>,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
// 对外事件
|
||||
@@ -25,10 +39,8 @@ const sort = ref('name')
|
||||
|
||||
// 调整排序方式
|
||||
function changeSort() {
|
||||
if (sort.value === 'name')
|
||||
sort.value = 'time'
|
||||
else
|
||||
sort.value = 'name'
|
||||
if (sort.value === 'name') sort.value = 'time'
|
||||
else sort.value = 'name'
|
||||
|
||||
emit('sortchanged', sort.value)
|
||||
}
|
||||
@@ -36,18 +48,20 @@ function changeSort() {
|
||||
// 计算PATH面包屑
|
||||
const pathSegments = computed(() => {
|
||||
let path_str = ''
|
||||
const isFolder = inProps.path?.endsWith('/')
|
||||
const segments = inProps.path?.split('/').filter(item => item)
|
||||
|
||||
return segments?.map((item, index) => {
|
||||
path_str += item + ((index < segments.length - 1 || isFolder) ? '/' : '')
|
||||
return {
|
||||
name: item,
|
||||
path: path_str,
|
||||
}
|
||||
}) ?? []
|
||||
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,
|
||||
}
|
||||
}) ?? []
|
||||
)
|
||||
})
|
||||
|
||||
// 当前存储
|
||||
const storageObject = computed(() => {
|
||||
return inProps.storages?.find(item => item.code === inProps.storage)
|
||||
})
|
||||
@@ -56,20 +70,19 @@ const storageObject = computed(() => {
|
||||
function changeStorage(code: string) {
|
||||
if (inProps.storage !== code) {
|
||||
emit('storagechanged', code)
|
||||
emit('pathchanged', '')
|
||||
}
|
||||
}
|
||||
|
||||
// 路径变化
|
||||
function changePath(_path: string) {
|
||||
emit('pathchanged', _path)
|
||||
function changePath(item: FileItem) {
|
||||
emit('pathchanged', item)
|
||||
}
|
||||
|
||||
// 返回上一级
|
||||
function goUp() {
|
||||
const segments = pathSegments.value ?? []
|
||||
const path = segments?.length === 1 ? '/' : segments[segments.length - 2].path
|
||||
changePath(path)
|
||||
const fileitem = inProps.itemstack[segments.length - 1]
|
||||
changePath(fileitem)
|
||||
}
|
||||
|
||||
// 创建目录
|
||||
@@ -77,15 +90,16 @@ 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(/{name}/g, newFolderName.value)
|
||||
|
||||
const config = {
|
||||
const config: AxiosRequestConfig<FileItem> = {
|
||||
url,
|
||||
method: inProps.endpoints?.mkdir.method || 'post',
|
||||
data: inProps.item,
|
||||
}
|
||||
|
||||
// 调API
|
||||
await inProps.axios?.request(config)
|
||||
await inProps.axios.request(config)
|
||||
|
||||
newFolderPopper.value = false
|
||||
newFolderName.value = ''
|
||||
@@ -97,10 +111,8 @@ async function mkdir() {
|
||||
|
||||
// 计算排序图标
|
||||
const sortIcon = computed(() => {
|
||||
if (sort.value === 'time')
|
||||
return 'mdi-sort-clock-ascending-outline'
|
||||
else
|
||||
return 'mdi-sort-alphabetical-ascending'
|
||||
if (sort.value === 'time') return 'mdi-sort-clock-ascending-outline'
|
||||
else return 'mdi-sort-alphabetical-ascending'
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -127,16 +139,17 @@ const sortIcon = computed(() => {
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
<VBtn variant="text" :input-value="path === '/'" class="px-1" @click="changePath('/')">
|
||||
<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)"
|
||||
class="px-1"
|
||||
@click="changePath(inProps.itemstack[index + 1])"
|
||||
>
|
||||
<VIcon icon=" mdi-chevron-right" />
|
||||
{{ segment.name }}
|
||||
@@ -158,10 +171,7 @@ const sortIcon = computed(() => {
|
||||
</IconBtn>
|
||||
</template>
|
||||
</VTooltip>
|
||||
<VDialog
|
||||
v-model="newFolderPopper"
|
||||
max-width="50rem"
|
||||
>
|
||||
<VDialog v-model="newFolderPopper" max-width="50rem">
|
||||
<template #activator="{ props }">
|
||||
<IconBtn v-bind="props">
|
||||
<VTooltip text="新建文件夹">
|
||||
@@ -172,20 +182,14 @@ 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 :disabled="!newFolderName" variant="elevated" @click="mkdir" prepend-icon="mdi-check" class="px-5 me-3">
|
||||
新建
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
|
||||
@@ -8,7 +8,6 @@ const props = defineProps({
|
||||
root: {
|
||||
type: String,
|
||||
default: '/',
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -32,13 +31,15 @@ const treeItems = ref<FileItem[]>([
|
||||
extension: '',
|
||||
size: 0,
|
||||
modify_time: 0,
|
||||
fileid: '',
|
||||
parent_fileid: '',
|
||||
},
|
||||
])
|
||||
|
||||
// 拉取子目录
|
||||
async function fetchDirs(item: any) {
|
||||
return api
|
||||
.get('/filebrowser/listdir?path=' + item.path)
|
||||
.get('/local/listdir?path=' + item.path)
|
||||
.then((data: any) => {
|
||||
item.children.push(...data)
|
||||
})
|
||||
|
||||
@@ -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'" />
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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')
|
||||
}
|
||||
|
||||
@@ -43,13 +43,13 @@ onMounted(() => {
|
||||
v-model="appList"
|
||||
item-key="title"
|
||||
tag="VRow"
|
||||
delay="500"
|
||||
delay="300"
|
||||
@end="saveAppsOrder"
|
||||
:component-data="{ 'class': 'ma-0 mt-n1' }"
|
||||
>
|
||||
<template #item="{ element }">
|
||||
<VCol cols="6" md="4" lg="3" class="text-center cursor-pointer shortcut-icon">
|
||||
<VCard class="pa-4 select-none" :to="element.to" variant="flat">
|
||||
<VCol cols="6" md="4" lg="3" class="text-center cursor-pointer shortcut-icon select-none">
|
||||
<VCard class="pa-4" :to="element.to" variant="flat">
|
||||
<VAvatar size="64" variant="text">
|
||||
<VIcon size="48" :icon="element.icon" color="primary" />
|
||||
</VAvatar>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -34,6 +34,9 @@ const errorMessage = ref('')
|
||||
// 背景图片
|
||||
const backgroundImageUrl = ref('')
|
||||
|
||||
// 所有的背景图片
|
||||
const backgroundImages = ref<string[]>([])
|
||||
|
||||
// 背景图片加载状态
|
||||
const isImageLoaded = ref(false)
|
||||
|
||||
@@ -43,17 +46,21 @@ const isOTP = ref(false)
|
||||
// 用户名称输入框
|
||||
const usernameInput = ref()
|
||||
|
||||
// Interval定时器
|
||||
let intervalTimer: NodeJS.Timeout | null = null
|
||||
|
||||
// 获取背景图片
|
||||
async function fetchBackgroundImage() {
|
||||
api
|
||||
.get('/login/wallpaper')
|
||||
.then((response: any) => {
|
||||
backgroundImageUrl.value = response.message
|
||||
})
|
||||
.catch((error: any) => {
|
||||
console.log(error)
|
||||
})
|
||||
try {
|
||||
backgroundImages.value = await api.get('/login/wallpapers')
|
||||
if (backgroundImages.value && backgroundImages.value.length > 0) {
|
||||
backgroundImageUrl.value = backgroundImages.value[0]
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
// 查询是否开启双重验证
|
||||
const fetchOTP = debounce(async () => {
|
||||
const userid = usernameInput.value?.value
|
||||
@@ -151,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) => {
|
||||
// 登录失败,显示错误提示
|
||||
@@ -176,7 +181,7 @@ function login() {
|
||||
}
|
||||
|
||||
// 自动登录
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
// 从Vuex Store中获取token和remember状态
|
||||
const token = store.state.auth.token
|
||||
const remember = store.state.auth.remember
|
||||
@@ -186,81 +191,97 @@ onMounted(() => {
|
||||
router.push('/')
|
||||
} else {
|
||||
// 获取背景图片
|
||||
fetchBackgroundImage()
|
||||
await fetchBackgroundImage()
|
||||
|
||||
// 每隔5秒更换一次背景图片
|
||||
intervalTimer = setInterval(() => {
|
||||
if (backgroundImages.value.length > 0) {
|
||||
const index = Math.floor(Math.random() * backgroundImages.value.length)
|
||||
backgroundImageUrl.value = backgroundImages.value[index]
|
||||
}
|
||||
}, 5000)
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (intervalTimer) clearInterval(intervalTimer)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VImg
|
||||
aspect-ratio="4/3"
|
||||
:src="backgroundImageUrl"
|
||||
class="w-full h-full overflow-hidden"
|
||||
cover
|
||||
@load="isImageLoaded = true"
|
||||
>
|
||||
<div class="auth-wrapper d-flex align-center justify-center pa-4">
|
||||
<VCard
|
||||
class="auth-card pa-7 w-full h-full"
|
||||
:class="isImageLoaded ? 'backdrop-blur-xl bg-white/50' : ''"
|
||||
max-width="25rem"
|
||||
:theme="isImageLoaded ? 'light' : ''"
|
||||
>
|
||||
<VCardItem class="justify-center mb-7">
|
||||
<template #prepend>
|
||||
<div class="d-flex pe-0">
|
||||
<VImg :src="logo" width="64" height="64" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<VCardTitle class="font-weight-semibold text-2xl text-uppercase"> MoviePilot </VCardTitle>
|
||||
</VCardItem>
|
||||
|
||||
<VCardText>
|
||||
<VForm ref="refForm" @submit.prevent="() => {}">
|
||||
<VRow>
|
||||
<!-- username -->
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
ref="usernameInput"
|
||||
v-model="form.username"
|
||||
label="用户名"
|
||||
type="text"
|
||||
:rules="[requiredValidator]"
|
||||
@input="fetchOTP"
|
||||
/>
|
||||
</VCol>
|
||||
<!-- password -->
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
v-model="form.password"
|
||||
label="密码"
|
||||
:type="isPasswordVisible ? 'text' : 'password'"
|
||||
:append-inner-icon="isPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
|
||||
:rules="[requiredValidator]"
|
||||
@click:append-inner="isPasswordVisible = !isPasswordVisible"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VTextField v-if="isOTP" v-model="form.otp_password" label="双重验证码" type="input" />
|
||||
<!-- remember me checkbox -->
|
||||
<div class="d-flex align-center justify-space-between flex-wrap">
|
||||
<VCheckbox v-model="form.remember" label="保持登录" required />
|
||||
</div>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<!-- login button -->
|
||||
<VBtn block type="submit" @click="login"> 登录 </VBtn>
|
||||
<div v-if="errorMessage" class="text-error mt-2 text-shadow">
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
<template v-for="image in backgroundImages">
|
||||
<div v-if="backgroundImageUrl == image" class="absolute inset-0">
|
||||
<VImg :src="image" class="w-full h-full" cover position="center top" @load="isImageLoaded = true">
|
||||
<template #placeholder>
|
||||
<VSkeletonLoader v-if="!isImageLoaded" class="object-cover" />
|
||||
</template>
|
||||
<div
|
||||
class="absolute inset-0"
|
||||
style="background-image: linear-gradient(rgba(45, 55, 72, 33%) 0%, rgb(26, 32, 46) 100%)"
|
||||
/>
|
||||
</VImg>
|
||||
</div>
|
||||
</VImg>
|
||||
</template>
|
||||
<div class="auth-wrapper d-flex align-center justify-center pa-4">
|
||||
<VCard
|
||||
class="auth-card px-7 py-3 w-full h-full rounded-lg"
|
||||
:class="{ 'opacity-85': isImageLoaded }"
|
||||
max-width="24rem"
|
||||
>
|
||||
<VCardItem class="justify-center">
|
||||
<template #prepend>
|
||||
<div class="d-flex pe-0">
|
||||
<VImg :src="logo" width="64" height="64" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<VCardTitle class="font-weight-semibold text-2xl text-uppercase"> MoviePilot </VCardTitle>
|
||||
</VCardItem>
|
||||
|
||||
<VCardText>
|
||||
<VForm ref="refForm" @submit.prevent="() => {}">
|
||||
<VRow>
|
||||
<!-- username -->
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
ref="usernameInput"
|
||||
v-model="form.username"
|
||||
label="用户名"
|
||||
type="text"
|
||||
:rules="[requiredValidator]"
|
||||
@input="fetchOTP"
|
||||
/>
|
||||
</VCol>
|
||||
<!-- password -->
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
v-model="form.password"
|
||||
label="密码"
|
||||
:type="isPasswordVisible ? 'text' : 'password'"
|
||||
:append-inner-icon="isPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
|
||||
:rules="[requiredValidator]"
|
||||
@click:append-inner="isPasswordVisible = !isPasswordVisible"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VTextField v-if="isOTP" v-model="form.otp_password" label="双重验证码" type="input" />
|
||||
<!-- remember me checkbox -->
|
||||
<div class="d-flex align-center justify-space-between flex-wrap">
|
||||
<VCheckbox v-model="form.remember" label="保持登录" required />
|
||||
</div>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<!-- login button -->
|
||||
<VBtn block type="submit" @click="login"> 登录 </VBtn>
|
||||
<div v-if="errorMessage" class="text-error mt-2 text-shadow">
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -144,6 +144,7 @@ const router = createRouter({
|
||||
path: '/filemanager',
|
||||
component: () => import('../pages/filemanager.vue'),
|
||||
meta: {
|
||||
keepAlive: true,
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -10,7 +10,7 @@ cleanupOutdatedCaches()
|
||||
precacheAndRoute(self.__WB_MANIFEST)
|
||||
|
||||
// to allow work offline
|
||||
registerRoute(new NavigationRoute(createHandlerBoundToURL('index.html'), { denylist: [/^\/api/] }))
|
||||
registerRoute(new NavigationRoute(createHandlerBoundToURL('index.html'), { denylist: [/^(\/[\w-]+)*\/api/] }))
|
||||
|
||||
// 通知选项
|
||||
const options = {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -46,8 +46,10 @@ const editionFilterOptions = ref<Array<string>>([])
|
||||
// 获取分辨率过滤选项
|
||||
const resolutionFilterOptions = ref<Array<string>>([])
|
||||
|
||||
// 数据列表
|
||||
const dataList = ref<Array<SearchTorrent>>([])
|
||||
// 完整的数据列表
|
||||
let dataList: SearchTorrent[]
|
||||
// 显示用的数据列表
|
||||
const displayDataList = ref<Array<SearchTorrent>>([])
|
||||
|
||||
// 分组后的数据列表
|
||||
const groupedDataList = ref<Map<string, Context[]>>()
|
||||
@@ -123,12 +125,15 @@ onMounted(() => {
|
||||
}
|
||||
})
|
||||
groupedDataList.value = groupMap
|
||||
|
||||
})
|
||||
|
||||
// 计算过滤后的列表
|
||||
watchEffect(() => {
|
||||
// 只监听filterForm和groupedDataList的变化。因为displayDataList的变化不需要清空列表
|
||||
watch([filterForm, groupedDataList], filterData)
|
||||
function filterData() {
|
||||
// 清空列表
|
||||
dataList.value = []
|
||||
dataList = []
|
||||
displayDataList.value = []
|
||||
// 匹配过滤函数,filter中有任一值包含value则返回true
|
||||
const match = (filter: Array<string>, value: string | undefined) =>
|
||||
filter.length === 0 || (value && filter.includes(value))
|
||||
@@ -159,11 +164,23 @@ watchEffect(() => {
|
||||
const firstData = _.cloneDeepWith(matchData[0]) as SearchTorrent
|
||||
if (matchData.length > 1) firstData.more = matchData.slice(1)
|
||||
|
||||
dataList.value.push(firstData)
|
||||
// 显示前20个,4行左右。
|
||||
if (displayDataList.value.length < 20) {
|
||||
displayDataList.value.push(firstData)
|
||||
} else {
|
||||
// 后续内容不显示,存在list里。loadMore的时候再加载。
|
||||
dataList.push(firstData)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function loadMore({ done }: { done: any }) {
|
||||
const itemsToMove = dataList.splice(0, 20) // 从 dataList 中获取最前面的 20 个元素
|
||||
displayDataList.value.push(...itemsToMove)
|
||||
done('ok')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -248,9 +265,12 @@ watchEffect(() => {
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCard>
|
||||
<div class="grid gap-3 grid-torrent-card items-start">
|
||||
<div v-for="(item, index) in dataList" :key="`${index}_${item.torrent_info.title}_${item.torrent_info.site}`">
|
||||
<TorrentCard :torrent="item" :more="item.more" />
|
||||
</div>
|
||||
</div>
|
||||
<VInfiniteScroll mode="intersect" side="end" :items="displayDataList" class="overflow-hidden"
|
||||
@load="loadMore">
|
||||
<template #loading />
|
||||
<template #empty />
|
||||
<div class="grid gap-3 grid-torrent-card items-start">
|
||||
<TorrentCard v-for="item in displayDataList" :key="`${item.torrent_info.page_url}`" :torrent="item" :more="item.more" />
|
||||
</div>
|
||||
</VInfiniteScroll>
|
||||
</template>
|
||||
|
||||
@@ -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,6 +79,41 @@ function initOptions(data: Context) {
|
||||
optionValue(resolutionFilterOptions.value, meta_info?.resource_pix)
|
||||
}
|
||||
|
||||
// 对季过滤选项进行排序
|
||||
const sortSeasonFilterOptions = computed(() => {
|
||||
return seasonFilterOptions.value.sort((a, b) => {
|
||||
// 按季,集降序排序
|
||||
const parseSeasonEpisode = (str: string) => {
|
||||
const seasonRangeMatch = str.match(/S(\d+)(?:-S(\d+))?/)
|
||||
const episodeRangeMatch = str.match(/E(\d+)(?:-E(\d+))?/)
|
||||
return {
|
||||
seasonStart: seasonRangeMatch?.[1] ? parseInt(seasonRangeMatch[1]) : 0,
|
||||
seasonEnd: seasonRangeMatch?.[2] ? parseInt(seasonRangeMatch[2]) : 0,
|
||||
episodeStart: episodeRangeMatch?.[1] ? parseInt(episodeRangeMatch[1]) : 0,
|
||||
episodeEnd: episodeRangeMatch?.[2] ? parseInt(episodeRangeMatch[2]) : 0,
|
||||
}
|
||||
}
|
||||
const parsedA = parseSeasonEpisode(a)
|
||||
const parsedB = parseSeasonEpisode(b)
|
||||
// 先按季降序排序
|
||||
if (parsedB.seasonStart !== parsedA.seasonStart) {
|
||||
return parsedB.seasonStart - parsedA.seasonStart
|
||||
}
|
||||
if (parsedB.seasonEnd !== parsedA.seasonEnd) {
|
||||
return parsedB.seasonEnd - parsedA.seasonEnd
|
||||
}
|
||||
// 按集降序排序
|
||||
if (parsedB.episodeStart !== parsedA.episodeStart) {
|
||||
return parsedB.episodeStart - parsedA.episodeStart
|
||||
}
|
||||
if (parsedB.episodeEnd !== parsedA.episodeEnd) {
|
||||
return parsedB.episodeEnd - parsedA.episodeEnd
|
||||
}
|
||||
// 兜底
|
||||
return b.localeCompare(a)
|
||||
})
|
||||
})
|
||||
|
||||
// 排序
|
||||
watchEffect(() => {
|
||||
const list = dataList.value
|
||||
@@ -132,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.title}_${item.torrent_info.site}`" />
|
||||
<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">
|
||||
@@ -261,7 +293,7 @@ onMounted(() => {
|
||||
<VListItem>
|
||||
<VChipGroup v-model="filterForm.season" column multiple>
|
||||
<VChip
|
||||
v-for="season in seasonFilterOptions"
|
||||
v-for="season in sortSeasonFilterOptions"
|
||||
:key="season"
|
||||
:color="filterForm.season.includes(season) ? 'primary' : ''"
|
||||
filter
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,44 +1,65 @@
|
||||
<script lang="ts" setup>
|
||||
import api from '@/api'
|
||||
import { MediaDirectory } from '@/api/types'
|
||||
import { FileItem, MediaDirectory } from '@/api/types'
|
||||
import FileBrowser from '@/components/FileBrowser.vue'
|
||||
import store from '@/store'
|
||||
|
||||
const endpoints = {
|
||||
list: {
|
||||
url: '/filebrowser/list?path={path}&sort={sort}',
|
||||
method: 'get',
|
||||
url: '/{storage}/list?sort={sort}',
|
||||
method: 'post',
|
||||
},
|
||||
mkdir: {
|
||||
url: '/filebrowser/mkdir?path={path}',
|
||||
method: 'get',
|
||||
url: '/{storage}/mkdir?name={name}',
|
||||
method: 'post',
|
||||
},
|
||||
delete: {
|
||||
url: '/filebrowser/delete?path={path}',
|
||||
method: 'get',
|
||||
url: '/{storage}/delete',
|
||||
method: 'post',
|
||||
},
|
||||
download: {
|
||||
url: '/filebrowser/download?path={path}',
|
||||
url: '/{storage}/download',
|
||||
method: 'get',
|
||||
},
|
||||
image: {
|
||||
url: '/filebrowser/image?path={path}',
|
||||
url: '/{storage}/image',
|
||||
method: 'get',
|
||||
},
|
||||
rename: {
|
||||
url: '/filebrowser/rename?path={path}&new_name={newname}',
|
||||
method: 'get',
|
||||
url: '/{storage}/rename?new_name={newname}',
|
||||
method: 'post',
|
||||
},
|
||||
}
|
||||
|
||||
// 当前目录
|
||||
const path: Ref<string | undefined> = ref()
|
||||
const user_level = store.state.auth.level
|
||||
|
||||
// 用户存储
|
||||
const userStorage = user_level > 1 ? 'local,aliyun,u115' : 'local'
|
||||
|
||||
// 当前文件项
|
||||
const operItem = ref<FileItem>({
|
||||
type: 'dir',
|
||||
name: '/',
|
||||
path: '/',
|
||||
fileid: 'root',
|
||||
})
|
||||
|
||||
// fileid的堆栈
|
||||
const itemstack = ref<FileItem[]>([
|
||||
{
|
||||
type: 'dir',
|
||||
name: '/',
|
||||
path: '/',
|
||||
fileid: 'root',
|
||||
},
|
||||
])
|
||||
|
||||
// 下载目录列表
|
||||
const downloadDirectories = ref<MediaDirectory[]>([])
|
||||
|
||||
// 计算公共路径
|
||||
function findCommonPath(paths: string[]): string {
|
||||
let commonPath = '/'
|
||||
let commonPath
|
||||
if (!paths || paths.length === 0) {
|
||||
commonPath = '/'
|
||||
} else if (paths.length === 1) {
|
||||
@@ -64,7 +85,7 @@ function findCommonPath(paths: string[]): string {
|
||||
}
|
||||
|
||||
if (commonPath.includes(':')) {
|
||||
commonPath = commonPath.replace('/', '\\')
|
||||
commonPath = commonPath.replace('\\', '/')
|
||||
}
|
||||
|
||||
return commonPath
|
||||
@@ -76,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)
|
||||
@@ -84,21 +121,29 @@ async function loadDownloadDirectories() {
|
||||
}
|
||||
|
||||
// 目录变化
|
||||
function pathChanged(_path: string) {
|
||||
path.value = _path
|
||||
function pathChanged(item: FileItem) {
|
||||
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"
|
||||
:storages="userStorage"
|
||||
:tree="false"
|
||||
:path="path"
|
||||
:itemstack="itemstack"
|
||||
:endpoints="endpoints"
|
||||
:axios="api"
|
||||
:item="operItem"
|
||||
@pathchanged="pathChanged"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -25,6 +25,7 @@ const cookieCloudSetting = ref({
|
||||
COOKIECLOUD_INTERVAL: 0,
|
||||
USER_AGENT: '',
|
||||
COOKIECLOUD_ENABLE_LOCAL: '',
|
||||
COOKIECLOUD_BLACKLIST: '',
|
||||
})
|
||||
|
||||
// 种子优先规则下拉框
|
||||
@@ -100,6 +101,7 @@ async function loadCookieCloudSettings() {
|
||||
COOKIECLOUD_INTERVAL,
|
||||
USER_AGENT,
|
||||
COOKIECLOUD_ENABLE_LOCAL,
|
||||
COOKIECLOUD_BLACKLIST,
|
||||
} = result.data
|
||||
cookieCloudSetting.value = {
|
||||
COOKIECLOUD_HOST,
|
||||
@@ -108,6 +110,7 @@ async function loadCookieCloudSettings() {
|
||||
COOKIECLOUD_INTERVAL,
|
||||
USER_AGENT,
|
||||
COOKIECLOUD_ENABLE_LOCAL,
|
||||
COOKIECLOUD_BLACKLIST,
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -191,7 +194,16 @@ onMounted(() => {
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="cookieCloudSetting.COOKIECLOUD_BLACKLIST"
|
||||
label="同步域名黑名单"
|
||||
placeholder="多个域名,分割"
|
||||
hint="CookieCloud同步域名黑名单,多个域名,分割"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="cookieCloudSetting.USER_AGENT"
|
||||
label="浏览器User-Agent"
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user