mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-05 15:36:49 +08:00
refactor: 优化代码格式,简化部分逻辑,提升可读性
This commit is contained in:
@@ -161,15 +161,15 @@ const isDragging = ref(false)
|
|||||||
const dragStartX = ref(0)
|
const dragStartX = ref(0)
|
||||||
const dragStartWidth = ref(0)
|
const dragStartWidth = ref(0)
|
||||||
|
|
||||||
watch(sort, (val) => {
|
watch(sort, val => {
|
||||||
localStorage.setItem(SORT_KEY, val)
|
localStorage.setItem(SORT_KEY, val)
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(showDirTree, (val) => {
|
watch(showDirTree, val => {
|
||||||
localStorage.setItem(SHOW_TREE_KEY, String(val))
|
localStorage.setItem(SHOW_TREE_KEY, String(val))
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(navigatorWidth, (val) => {
|
watch(navigatorWidth, val => {
|
||||||
localStorage.setItem(NAV_WIDTH_KEY, String(val))
|
localStorage.setItem(NAV_WIDTH_KEY, String(val))
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -182,7 +182,6 @@ const storagesArray = computed(() => {
|
|||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
// 方法
|
// 方法
|
||||||
function loadingChanged(isLoading: number) {
|
function loadingChanged(isLoading: number) {
|
||||||
if (isLoading) loading.value++
|
if (isLoading) loading.value++
|
||||||
@@ -272,7 +271,7 @@ function stopDrag() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="mx-auto" :loading="loading > 0">
|
<div class="mx-auto overflow-hidden" :loading="loading > 0">
|
||||||
<div v-if="item">
|
<div v-if="item">
|
||||||
<FileToolbar
|
<FileToolbar
|
||||||
ref="toolbarRef"
|
ref="toolbarRef"
|
||||||
|
|||||||
@@ -288,7 +288,10 @@ async function handleResetSettings() {
|
|||||||
v-bind="customizerContainerProps"
|
v-bind="customizerContainerProps"
|
||||||
:style="customizerContainerStyle"
|
:style="customizerContainerStyle"
|
||||||
>
|
>
|
||||||
<div class="theme-customizer-panel" :class="{ 'theme-customizer-panel--dialog': appMode, 'app-surface': appMode }">
|
<div
|
||||||
|
class="theme-customizer-panel"
|
||||||
|
:class="{ 'theme-customizer-panel--dialog': appMode, 'app-surface': appMode }"
|
||||||
|
>
|
||||||
<div class="theme-customizer-header py-5 px-4">
|
<div class="theme-customizer-header py-5 px-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="theme-customizer-title">{{ t('theme.customizer.title') }}</h2>
|
<h2 class="theme-customizer-title">{{ t('theme.customizer.title') }}</h2>
|
||||||
@@ -493,7 +496,7 @@ async function handleResetSettings() {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
block-size: 100dvh !important;
|
block-size: 100dvh !important;
|
||||||
border-inline-start: 1px solid rgba(var(--v-theme-on-surface), 0.08) !important;
|
border-inline-start: 1px solid rgba(var(--v-theme-on-surface), 0.08) !important;
|
||||||
box-shadow: -2px 0 6px rgba(0, 0, 0, 10%) !important;
|
box-shadow: var(--app-surface-shadow) !important;
|
||||||
inset-block: 0 !important;
|
inset-block: 0 !important;
|
||||||
inset-inline-end: 0 !important;
|
inset-inline-end: 0 !important;
|
||||||
max-block-size: 100dvh !important;
|
max-block-size: 100dvh !important;
|
||||||
@@ -534,6 +537,7 @@ async function handleResetSettings() {
|
|||||||
background: rgb(var(--v-theme-surface));
|
background: rgb(var(--v-theme-surface));
|
||||||
block-size: var(--theme-customizer-viewport-height, 100dvh);
|
block-size: var(--theme-customizer-viewport-height, 100dvh);
|
||||||
max-block-size: var(--theme-customizer-viewport-height, 100dvh);
|
max-block-size: var(--theme-customizer-viewport-height, 100dvh);
|
||||||
|
|
||||||
/* fullscreen dialog 会贴到 viewport-fit=cover 顶部,iOS 需要在面板内部避开系统状态栏。 */
|
/* fullscreen dialog 会贴到 viewport-fit=cover 顶部,iOS 需要在面板内部避开系统状态栏。 */
|
||||||
padding-block-start: env(safe-area-inset-top);
|
padding-block-start: env(safe-area-inset-top);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,82 +71,72 @@ async function deleteDownload() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<VCard
|
<VHover>
|
||||||
v-if="cardState"
|
<template #default="hover">
|
||||||
:key="props.info?.hash"
|
<VCard
|
||||||
class="downloading-card flex flex-col h-full overflow-hidden"
|
v-if="cardState"
|
||||||
min-height="150"
|
v-bind="hover.props"
|
||||||
>
|
:key="props.info?.hash"
|
||||||
<template #image>
|
class="downloading-card app-surface flex flex-col h-full overflow-hidden"
|
||||||
<VImg
|
:class="{
|
||||||
:src="props.info?.media.image"
|
'transition transform-cpu duration-300 -translate-y-1': hover.isHovering,
|
||||||
class="downloading-card-image"
|
}"
|
||||||
aspect-ratio="2/3"
|
min-height="150"
|
||||||
cover
|
|
||||||
@load="imageLoadHandler"
|
|
||||||
position="top"
|
|
||||||
>
|
>
|
||||||
<template #placeholder>
|
<template #image>
|
||||||
<div class="w-full h-full">
|
<VImg
|
||||||
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
|
:src="props.info?.media.image"
|
||||||
</div>
|
class="downloading-card-image"
|
||||||
|
aspect-ratio="2/3"
|
||||||
|
cover
|
||||||
|
@load="imageLoadHandler"
|
||||||
|
position="top"
|
||||||
|
>
|
||||||
|
<template #placeholder>
|
||||||
|
<div class="w-full h-full">
|
||||||
|
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #default>
|
||||||
|
<div class="absolute inset-0 outline-none downloading-card-background"></div>
|
||||||
|
</template>
|
||||||
|
</VImg>
|
||||||
</template>
|
</template>
|
||||||
<template #default>
|
|
||||||
<div class="absolute inset-0 outline-none downloading-card-background"></div>
|
<div>
|
||||||
</template>
|
<VCardTitle class="break-words whitespace-normal text-white">
|
||||||
</VImg>
|
{{ props.info?.media.title || props.info?.name }}
|
||||||
|
{{
|
||||||
|
props.info?.media.episode
|
||||||
|
? `${props.info?.media.season} ${props.info?.media.episode}`
|
||||||
|
: props.info?.season_episode
|
||||||
|
}}
|
||||||
|
</VCardTitle>
|
||||||
|
|
||||||
|
<VCardSubtitle class="break-words whitespace-normal text-white">
|
||||||
|
{{ props.info?.title }}
|
||||||
|
</VCardSubtitle>
|
||||||
|
|
||||||
|
<VCardText class="text-subtitle-1 pt-3 pb-1 text-white">
|
||||||
|
{{ getSpeedText() }}
|
||||||
|
</VCardText>
|
||||||
|
|
||||||
|
<VCardText v-if="getPercentage() > 0" class="text-white">
|
||||||
|
<VProgressLinear :model-value="getPercentage()" bg-color="success" color="success" />
|
||||||
|
</VCardText>
|
||||||
|
|
||||||
|
<VCardActions class="justify-space-between">
|
||||||
|
<VBtn :icon="`${isDownloading ? 'mdi-pause' : 'mdi-play'}`" @click="toggleDownload" />
|
||||||
|
<VBtn color="error" icon="mdi-trash-can-outline" @click="deleteDownload" />
|
||||||
|
</VCardActions>
|
||||||
|
</div>
|
||||||
|
</VCard>
|
||||||
</template>
|
</template>
|
||||||
|
</VHover>
|
||||||
<div>
|
|
||||||
<VCardTitle class="break-words whitespace-normal text-white">
|
|
||||||
{{ props.info?.media.title || props.info?.name }}
|
|
||||||
{{
|
|
||||||
props.info?.media.episode
|
|
||||||
? `${props.info?.media.season} ${props.info?.media.episode}`
|
|
||||||
: props.info?.season_episode
|
|
||||||
}}
|
|
||||||
</VCardTitle>
|
|
||||||
|
|
||||||
<VCardSubtitle class="break-words whitespace-normal text-white">
|
|
||||||
{{ props.info?.title }}
|
|
||||||
</VCardSubtitle>
|
|
||||||
|
|
||||||
<VCardText class="text-subtitle-1 pt-3 pb-1 text-white">
|
|
||||||
{{ getSpeedText() }}
|
|
||||||
</VCardText>
|
|
||||||
|
|
||||||
<VCardText v-if="getPercentage() > 0" class="text-white">
|
|
||||||
<VProgressLinear :model-value="getPercentage()" bg-color="success" color="success" />
|
|
||||||
</VCardText>
|
|
||||||
|
|
||||||
<VCardActions class="justify-space-between">
|
|
||||||
<VBtn :icon="`${isDownloading ? 'mdi-pause' : 'mdi-play'}`" @click="toggleDownload" />
|
|
||||||
<VBtn color="error" icon="mdi-trash-can-outline" @click="deleteDownload" />
|
|
||||||
</VCardActions>
|
|
||||||
</div>
|
|
||||||
</VCard>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.downloading-card {
|
/* stylelint-disable selector-pseudo-class-no-unknown */
|
||||||
position: relative;
|
|
||||||
isolation: isolate;
|
|
||||||
background-color: rgb(31, 41, 55) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 图片槽和渐变遮罩统一继承卡片圆角,避免阴影增强后四角露出页面底色。
|
|
||||||
.downloading-card :deep(.v-card__image),
|
|
||||||
.downloading-card :deep(.v-responsive),
|
|
||||||
.downloading-card :deep(.v-img),
|
|
||||||
.downloading-card :deep(.v-img__img),
|
|
||||||
.downloading-card :deep(.v-responsive__content) {
|
|
||||||
overflow: hidden;
|
|
||||||
border-radius: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.downloading-card :deep(.v-card__image) {
|
|
||||||
background-color: rgb(31, 41, 55);
|
|
||||||
}
|
|
||||||
|
|
||||||
.downloading-card-image {
|
.downloading-card-image {
|
||||||
block-size: 100%;
|
block-size: 100%;
|
||||||
|
|||||||
@@ -418,17 +418,14 @@ watch(
|
|||||||
)
|
)
|
||||||
|
|
||||||
// 切换媒体类型或识别源时,非 TMDB 电视剧不保留剧集组选择。
|
// 切换媒体类型或识别源时,非 TMDB 电视剧不保留剧集组选择。
|
||||||
watch(
|
watch([() => transferForm.type_name, () => mediaSource.value], ([typeName, source]) => {
|
||||||
[() => transferForm.type_name, () => mediaSource.value],
|
if (typeName === '电视剧' && source === 'themoviedb' && transferForm.tmdbid) {
|
||||||
([typeName, source]) => {
|
getEpisodeGroups(transferForm.tmdbid)
|
||||||
if (typeName === '电视剧' && source === 'themoviedb' && transferForm.tmdbid) {
|
return
|
||||||
getEpisodeGroups(transferForm.tmdbid)
|
}
|
||||||
return
|
transferForm.episode_group = null
|
||||||
}
|
episodeGroups.value = []
|
||||||
transferForm.episode_group = null
|
})
|
||||||
episodeGroups.value = []
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => transferForm.episode_group,
|
() => transferForm.episode_group,
|
||||||
@@ -500,9 +497,7 @@ function normalizeTargetPath(path?: string | null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 归一化剧集组值,兼容历史对象态值。
|
// 归一化剧集组值,兼容历史对象态值。
|
||||||
function normalizeEpisodeGroup(
|
function normalizeEpisodeGroup(episodeGroup?: string | { value?: string | null } | null) {
|
||||||
episodeGroup?: string | { value?: string | null } | null,
|
|
||||||
) {
|
|
||||||
if (!episodeGroup) return null
|
if (!episodeGroup) return null
|
||||||
if (typeof episodeGroup === 'string') {
|
if (typeof episodeGroup === 'string') {
|
||||||
const normalizedEpisodeGroup = episodeGroup.trim()
|
const normalizedEpisodeGroup = episodeGroup.trim()
|
||||||
@@ -632,11 +627,7 @@ const previewFileRows = computed(() => {
|
|||||||
// 标准化预览项中的识别词命中详情
|
// 标准化预览项中的识别词命中详情
|
||||||
function getPreviewApplyWords(item: ManualTransferPreviewItem) {
|
function getPreviewApplyWords(item: ManualTransferPreviewItem) {
|
||||||
return [
|
return [
|
||||||
...new Set(
|
...new Set((item.apply_words ?? []).map(word => word?.trim()).filter((word): word is string => Boolean(word))),
|
||||||
(item.apply_words ?? [])
|
|
||||||
.map(word => word?.trim())
|
|
||||||
.filter((word): word is string => Boolean(word)),
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -765,9 +756,7 @@ const episodeFormatRecommendSelectedFileItems = computed(() => {
|
|||||||
const episodeFormatRecommendHasValidSelectedFiles = computed(() => {
|
const episodeFormatRecommendHasValidSelectedFiles = computed(() => {
|
||||||
if (episodeFormatRecommendSelectedFileItems.value.length <= 1) return false
|
if (episodeFormatRecommendSelectedFileItems.value.length <= 1) return false
|
||||||
|
|
||||||
const directoryKeys = new Set(
|
const directoryKeys = new Set(episodeFormatRecommendSelectedFileItems.value.map(item => getFileParentKey(item)))
|
||||||
episodeFormatRecommendSelectedFileItems.value.map(item => getFileParentKey(item)),
|
|
||||||
)
|
|
||||||
return directoryKeys.size === 1
|
return directoryKeys.size === 1
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -778,8 +767,7 @@ const episodeFormatRecommendSourceItem = computed<FileItem | undefined>(() => {
|
|||||||
|
|
||||||
const canRecommendEpisodeFormat = computed(() => {
|
const canRecommendEpisodeFormat = computed(() => {
|
||||||
return (
|
return (
|
||||||
(Boolean(episodeFormatRecommendSourceItem.value?.path) ||
|
(Boolean(episodeFormatRecommendSourceItem.value?.path) || episodeFormatRecommendHasValidSelectedFiles.value) &&
|
||||||
episodeFormatRecommendHasValidSelectedFiles.value) &&
|
|
||||||
!progressDialog.value &&
|
!progressDialog.value &&
|
||||||
!episodeFormatRecommendState.loading
|
!episodeFormatRecommendState.loading
|
||||||
)
|
)
|
||||||
@@ -793,10 +781,7 @@ const episodeFormatRecommendSelectionKey = computed(() => {
|
|||||||
|
|
||||||
const episodeFormatRecommendTooltip = computed(() => {
|
const episodeFormatRecommendTooltip = computed(() => {
|
||||||
if (episodeFormatRecommendState.loading) return t('dialog.reorganize.episodeFormatRecommendLoading')
|
if (episodeFormatRecommendState.loading) return t('dialog.reorganize.episodeFormatRecommendLoading')
|
||||||
if (
|
if (normalizedItems.value.length > 1 && !episodeFormatRecommendHasValidSelectedFiles.value) {
|
||||||
normalizedItems.value.length > 1 &&
|
|
||||||
!episodeFormatRecommendHasValidSelectedFiles.value
|
|
||||||
) {
|
|
||||||
return t('dialog.reorganize.episodeFormatRecommendInvalidSelection')
|
return t('dialog.reorganize.episodeFormatRecommendInvalidSelection')
|
||||||
}
|
}
|
||||||
if (!episodeFormatRecommendSourceItem.value?.path && !episodeFormatRecommendHasValidSelectedFiles.value) {
|
if (!episodeFormatRecommendSourceItem.value?.path && !episodeFormatRecommendHasValidSelectedFiles.value) {
|
||||||
@@ -832,11 +817,7 @@ function getBatchItemsLabel(items: FileItem[]) {
|
|||||||
|
|
||||||
// 构造整理请求
|
// 构造整理请求
|
||||||
function createTransferPayload(options: { item?: FileItem; items?: FileItem[]; logid?: number; preview?: boolean }) {
|
function createTransferPayload(options: { item?: FileItem; items?: FileItem[]; logid?: number; preview?: boolean }) {
|
||||||
const sourceItem =
|
const sourceItem = options.item ?? (options.items?.length ? options.items[0] : ({} as FileItem))
|
||||||
options.item ??
|
|
||||||
(options.items?.length
|
|
||||||
? options.items[0]
|
|
||||||
: ({} as FileItem))
|
|
||||||
const payload: ManualTransferPayload = {
|
const payload: ManualTransferPayload = {
|
||||||
...transferForm,
|
...transferForm,
|
||||||
fileitem: sourceItem,
|
fileitem: sourceItem,
|
||||||
@@ -1702,7 +1683,7 @@ onUnmounted(() => {
|
|||||||
<div
|
<div
|
||||||
v-for="(item, index) in pagedPreviewRows"
|
v-for="(item, index) in pagedPreviewRows"
|
||||||
:key="`${item.source}-${item.target}-${index}`"
|
:key="`${item.source}-${item.target}-${index}`"
|
||||||
class="preview-file-row"
|
class="preview-file-row app-surface-shape"
|
||||||
:class="{ 'preview-file-row--failed': item.success === false }"
|
:class="{ 'preview-file-row--failed': item.success === false }"
|
||||||
>
|
>
|
||||||
<div class="preview-file-row__card preview-file-row__card--source">
|
<div class="preview-file-row__card preview-file-row__card--source">
|
||||||
@@ -1983,18 +1964,18 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.preview-custom-words__source {
|
.preview-custom-words__source {
|
||||||
overflow-wrap: anywhere;
|
|
||||||
color: rgb(var(--v-theme-on-surface));
|
color: rgb(var(--v-theme-on-surface));
|
||||||
font-size: 0.8125rem;
|
font-size: 0.8125rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-custom-words__original {
|
.preview-custom-words__original {
|
||||||
overflow-wrap: anywhere;
|
|
||||||
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-custom-words__chips {
|
.preview-custom-words__chips {
|
||||||
@@ -2107,10 +2088,10 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
.preview-file-row__path {
|
.preview-file-row__path {
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
overflow-wrap: anywhere;
|
|
||||||
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
||||||
font-size: 0.8125rem;
|
font-size: 0.8125rem;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ const { appMode } = usePWA()
|
|||||||
|
|
||||||
// 计算列表可用高度
|
// 计算列表可用高度
|
||||||
// componentOffset = FileToolbar(48) + FileList操作栏(40) + VCard边距(4) = 92
|
// componentOffset = FileToolbar(48) + FileList操作栏(40) + VCard边距(4) = 92
|
||||||
const { availableHeight: listAvailableHeight } = useAvailableHeight(92, 300)
|
const { availableHeight: listAvailableHeight } = useAvailableHeight(89, 300)
|
||||||
|
|
||||||
// 输入参数
|
// 输入参数
|
||||||
const inProps = defineProps({
|
const inProps = defineProps({
|
||||||
@@ -267,7 +267,7 @@ async function list_files(context: KeepAliveRefreshContext = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 加载数据
|
// 加载数据
|
||||||
const data = ((await inProps.axios.request<FileItem[], FileItem[]>(config))) ?? []
|
const data = (await inProps.axios.request<FileItem[], FileItem[]>(config)) ?? []
|
||||||
// 如果当前路径已经变化,则放弃此次加载结果
|
// 如果当前路径已经变化,则放弃此次加载结果
|
||||||
if (prevURI !== takeURISnapshot()) {
|
if (prevURI !== takeURISnapshot()) {
|
||||||
return
|
return
|
||||||
@@ -381,7 +381,7 @@ async function download(item: FileItem) {
|
|||||||
responseType: 'blob',
|
responseType: 'blob',
|
||||||
}
|
}
|
||||||
// 加载数据
|
// 加载数据
|
||||||
const result: Blob = (await inProps.axios.request<Blob, Blob>(config))
|
const result: Blob = await inProps.axios.request<Blob, Blob>(config)
|
||||||
if (result) {
|
if (result) {
|
||||||
const downloadUrl = URL.createObjectURL(result)
|
const downloadUrl = URL.createObjectURL(result)
|
||||||
window.open(downloadUrl, '_blank')
|
window.open(downloadUrl, '_blank')
|
||||||
@@ -402,7 +402,7 @@ async function getImgLink(item: FileItem) {
|
|||||||
responseType: 'blob',
|
responseType: 'blob',
|
||||||
}
|
}
|
||||||
// 加载二进制数据
|
// 加载二进制数据
|
||||||
const result: Blob = (await inProps.axios.request<Blob, Blob>(config))
|
const result: Blob = await inProps.axios.request<Blob, Blob>(config)
|
||||||
if (result) {
|
if (result) {
|
||||||
// 创建图片地址
|
// 创建图片地址
|
||||||
revokeCurrentImgLink()
|
revokeCurrentImgLink()
|
||||||
@@ -508,7 +508,7 @@ async function rename() {
|
|||||||
method: inProps.endpoints?.rename.method || 'post',
|
method: inProps.endpoints?.rename.method || 'post',
|
||||||
data: currentItem.value,
|
data: currentItem.value,
|
||||||
}
|
}
|
||||||
const result: { [key: string]: any } = (await inProps.axios?.request<any, { [key: string]: any }>(config))
|
const result: { [key: string]: any } = await inProps.axios?.request<any, { [key: string]: any }>(config)
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
$toast.error(result.message)
|
$toast.error(result.message)
|
||||||
}
|
}
|
||||||
@@ -768,17 +768,9 @@ onUnmounted(() => {
|
|||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.file-list-container {
|
|
||||||
overflow: hidden auto;
|
|
||||||
block-size: 100%;
|
|
||||||
max-block-size: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<VCard class="d-flex flex-column w-full h-full">
|
<VCard class="d-flex flex-column w-full h-full file-list">
|
||||||
<div v-if="!loading" class="flex">
|
<div v-if="!loading" class="flex">
|
||||||
<IconBtn v-if="display.mdAndUp.value">
|
<IconBtn v-if="display.mdAndUp.value">
|
||||||
<VIcon v-if="showTree" icon="mdi-file-tree" @click="switchFileTree(false)" />
|
<VIcon v-if="showTree" icon="mdi-file-tree" @click="switchFileTree(false)" />
|
||||||
@@ -792,7 +784,7 @@ onUnmounted(() => {
|
|||||||
density="compact"
|
density="compact"
|
||||||
variant="plain"
|
variant="plain"
|
||||||
:placeholder="t('file.filterPlaceholder')"
|
:placeholder="t('file.filterPlaceholder')"
|
||||||
:prepend-inner-icon="(filter.includes('*') || filter.includes('?')) ? 'mdi-asterisk' : 'mdi-filter-outline'"
|
:prepend-inner-icon="filter.includes('*') || filter.includes('?') ? 'mdi-asterisk' : 'mdi-filter-outline'"
|
||||||
class="mx-2"
|
class="mx-2"
|
||||||
rounded
|
rounded
|
||||||
/>
|
/>
|
||||||
@@ -931,3 +923,17 @@ onUnmounted(() => {
|
|||||||
</VCard>
|
</VCard>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.file-list {
|
||||||
|
border-radius: 0 !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-list-container {
|
||||||
|
overflow: hidden auto;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
block-size: 100%;
|
||||||
|
max-block-size: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ async function loadSubdirectories(path: string) {
|
|||||||
data: fakeItem,
|
data: fakeItem,
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = (await props.axios?.request(config))
|
const result = await props.axios?.request(config)
|
||||||
if (result && Array.isArray(result)) {
|
if (result && Array.isArray(result)) {
|
||||||
// 过滤出目录项
|
// 过滤出目录项
|
||||||
const dirs = result.filter(item => item.type === 'dir')
|
const dirs = result.filter(item => item.type === 'dir')
|
||||||
@@ -249,7 +249,6 @@ function getTreeRowStyle(level: number) {
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadRootDirectories()
|
await loadRootDirectories()
|
||||||
})
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -296,13 +295,7 @@ onMounted(async () => {
|
|||||||
:style="getTreeRowStyle(item.level)"
|
:style="getTreeRowStyle(item.level)"
|
||||||
>
|
>
|
||||||
<div class="folder-toggle" @click.stop="toggleFolder(item.dir.path || '')">
|
<div class="folder-toggle" @click.stop="toggleFolder(item.dir.path || '')">
|
||||||
<VProgressCircular
|
<VProgressCircular v-if="loading[item.dir.path || '']" indeterminate size="14" width="2" color="primary" />
|
||||||
v-if="loading[item.dir.path || '']"
|
|
||||||
indeterminate
|
|
||||||
size="14"
|
|
||||||
width="2"
|
|
||||||
color="primary"
|
|
||||||
/>
|
|
||||||
<VIcon
|
<VIcon
|
||||||
v-else
|
v-else
|
||||||
size="small"
|
size="small"
|
||||||
@@ -332,9 +325,10 @@ onMounted(async () => {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
border-radius: 0 !important;
|
||||||
background: rgb(var(--v-table-header-background));
|
background: rgb(var(--v-table-header-background));
|
||||||
block-size: 100%;
|
block-size: 100%;
|
||||||
border-end-start-radius: 12px;
|
box-shadow: none !important;
|
||||||
inline-size: 240px;
|
inline-size: 240px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+16
-11
@@ -141,7 +141,6 @@ html[data-theme-shadow='high'] {
|
|||||||
.v-card,
|
.v-card,
|
||||||
.v-sheet,
|
.v-sheet,
|
||||||
.v-list,
|
.v-list,
|
||||||
.v-table,
|
|
||||||
.v-expansion-panel {
|
.v-expansion-panel {
|
||||||
border: var(--app-surface-border);
|
border: var(--app-surface-border);
|
||||||
border-radius: var(--app-surface-radius) !important;
|
border-radius: var(--app-surface-radius) !important;
|
||||||
@@ -159,7 +158,6 @@ html[data-theme-shadow='high'] {
|
|||||||
.v-card:hover,
|
.v-card:hover,
|
||||||
.v-sheet:hover,
|
.v-sheet:hover,
|
||||||
.v-list:hover,
|
.v-list:hover,
|
||||||
.v-table:hover,
|
|
||||||
.v-expansion-panel:hover {
|
.v-expansion-panel:hover {
|
||||||
box-shadow: var(--app-surface-hover-shadow) !important;
|
box-shadow: var(--app-surface-hover-shadow) !important;
|
||||||
}
|
}
|
||||||
@@ -190,6 +188,16 @@ html[data-theme-shadow='high'] {
|
|||||||
transition: border-radius 0.2s ease, box-shadow 0.2s ease;
|
transition: border-radius 0.2s ease, box-shadow 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.v-btn:not(.v-btn--variant-plain, .v-btn--variant-text) {
|
||||||
|
box-shadow: var(--app-surface-shadow) !important;
|
||||||
|
transition: box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-btn:not(.v-btn--rounded, [class^='rounded-'], [class*=' rounded-']) {
|
||||||
|
border-radius: var(--app-surface-radius);
|
||||||
|
transition: border-radius 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
// 只给外层 surface 加边框和阴影,卡片内部的列表、表格等子组件保持平面,避免层级噪声。
|
// 只给外层 surface 加边框和阴影,卡片内部的列表、表格等子组件保持平面,避免层级噪声。
|
||||||
.v-card .v-list:not(.app-surface),
|
.v-card .v-list:not(.app-surface),
|
||||||
.v-card .v-sheet:not(.app-surface),
|
.v-card .v-sheet:not(.app-surface),
|
||||||
@@ -213,15 +221,6 @@ html[data-theme-shadow='high'] {
|
|||||||
--app-surface-shadow: none;
|
--app-surface-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 菜单只让直接承载层拥有弹出阴影,内部嵌套 surface 不再重复制造层级。
|
|
||||||
.v-menu
|
|
||||||
> .v-overlay__content
|
|
||||||
> :where(.v-card, .v-sheet, .v-list)
|
|
||||||
:where(.v-card, .v-sheet, .v-list, .v-table, .v-expansion-panel):not(.app-card-colorful) {
|
|
||||||
--app-surface-hover-shadow: none;
|
|
||||||
--app-surface-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 主题定制器的 bordered 皮肤:保持原布局密度,只给非 Vuetify 外壳增加清晰边界。
|
// 主题定制器的 bordered 皮肤:保持原布局密度,只给非 Vuetify 外壳增加清晰边界。
|
||||||
html[data-theme-skin='bordered'] {
|
html[data-theme-skin='bordered'] {
|
||||||
.layout-vertical-nav {
|
.layout-vertical-nav {
|
||||||
@@ -619,10 +618,16 @@ html[data-theme="transparent"] .app-card-colorful,
|
|||||||
|
|
||||||
// 组件样式
|
// 组件样式
|
||||||
.v-alert--variant-elevated, .v-alert--variant-flat {
|
.v-alert--variant-elevated, .v-alert--variant-flat {
|
||||||
|
border-radius: var(--app-surface-radius);
|
||||||
background: rgb(var(--v-table-header-background));
|
background: rgb(var(--v-table-header-background));
|
||||||
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.Vue-Toastification__toast {
|
||||||
|
border-radius: var(--app-surface-radius);
|
||||||
|
box-shadow: var(--app-overlay-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
.backdrop-blur {
|
.backdrop-blur {
|
||||||
--tw-backdrop-blur: blur(8px)!important;
|
--tw-backdrop-blur: blur(8px)!important;
|
||||||
|
|
||||||
|
|||||||
@@ -77,14 +77,14 @@ function findCommonPath(paths: string[]): string {
|
|||||||
const STORAGE_KEY = 'fileBrowserView.activeStorage'
|
const STORAGE_KEY = 'fileBrowserView.activeStorage'
|
||||||
|
|
||||||
interface BrowserInitialParams {
|
interface BrowserInitialParams {
|
||||||
storage: string;
|
storage: string
|
||||||
path: string;
|
path: string
|
||||||
name: string;
|
name: string
|
||||||
}
|
}
|
||||||
// determine which entry to select initially
|
// determine which entry to select initially
|
||||||
function determineBrowserInitialParams(downloadDirectories: TransferDirectoryConf[]): BrowserInitialParams {
|
function determineBrowserInitialParams(downloadDirectories: TransferDirectoryConf[]): BrowserInitialParams {
|
||||||
const isAvailable = (storage: string) => storageTypes.value.includes(storage);
|
const isAvailable = (storage: string) => storageTypes.value.includes(storage)
|
||||||
const buckets = downloadDirectories.reduce<Map<string, string[]>>((dict, item) => {
|
const buckets = downloadDirectories.reduce<Map<string, string[]>>((dict, item) => {
|
||||||
// filter out directories whose storage is not available
|
// filter out directories whose storage is not available
|
||||||
if (!isAvailable(item.storage)) {
|
if (!isAvailable(item.storage)) {
|
||||||
return dict
|
return dict
|
||||||
@@ -98,37 +98,35 @@ function determineBrowserInitialParams(downloadDirectories: TransferDirectoryCon
|
|||||||
dict.get(item.storage)!.push(item.download_path)
|
dict.get(item.storage)!.push(item.download_path)
|
||||||
}
|
}
|
||||||
return dict
|
return dict
|
||||||
}, new Map());
|
}, new Map())
|
||||||
|
|
||||||
const cachedStorage = localStorage.getItem(STORAGE_KEY) || '';
|
const cachedStorage = localStorage.getItem(STORAGE_KEY) || ''
|
||||||
// if no download directories are configured, fall back to cached storage or first available storage
|
// if no download directories are configured, fall back to cached storage or first available storage
|
||||||
if (buckets.size === 0) {
|
if (buckets.size === 0) {
|
||||||
return {
|
return {
|
||||||
storage: isAvailable(cachedStorage)
|
storage: isAvailable(cachedStorage) ? cachedStorage : storageTypes.value[0] || 'local',
|
||||||
? cachedStorage
|
|
||||||
: (storageTypes.value[0] || 'local'),
|
|
||||||
path: '/',
|
path: '/',
|
||||||
name: '/',
|
name: '/',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let selectedEntry: [string, string[]];
|
let selectedEntry: [string, string[]]
|
||||||
if (cachedStorage && buckets.has(cachedStorage)) {
|
if (cachedStorage && buckets.has(cachedStorage)) {
|
||||||
selectedEntry = [cachedStorage, buckets.get(cachedStorage)!];
|
selectedEntry = [cachedStorage, buckets.get(cachedStorage)!]
|
||||||
} else {
|
} else {
|
||||||
// if no storage selected previously, use the most populous one
|
// if no storage selected previously, use the most populous one
|
||||||
selectedEntry = Array.from(buckets.entries()).reduce((prev, curr) => {
|
selectedEntry = Array.from(buckets.entries()).reduce((prev, curr) => {
|
||||||
return curr[1].length > prev[1].length ? curr : prev;
|
return curr[1].length > prev[1].length ? curr : prev
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const path = findCommonPath(selectedEntry[1]);
|
const path = findCommonPath(selectedEntry[1])
|
||||||
return {
|
return {
|
||||||
storage: selectedEntry[0],
|
storage: selectedEntry[0],
|
||||||
path,
|
path,
|
||||||
name: path.split('/').filter(Boolean).pop() ?? '',
|
name: path.split('/').filter(Boolean).pop() ?? '',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查询下载目录
|
// 查询下载目录
|
||||||
async function loadDownloadDirectories() {
|
async function loadDownloadDirectories() {
|
||||||
try {
|
try {
|
||||||
@@ -138,7 +136,7 @@ async function loadDownloadDirectories() {
|
|||||||
|
|
||||||
const result: { [key: string]: any } = await api.get('system/setting/Directories')
|
const result: { [key: string]: any } = await api.get('system/setting/Directories')
|
||||||
if (result.success && result.data?.value) {
|
if (result.success && result.data?.value) {
|
||||||
const { storage, path, name } = determineBrowserInitialParams(result.data.value);
|
const { storage, path, name } = determineBrowserInitialParams(result.data.value)
|
||||||
// operItem初始化
|
// operItem初始化
|
||||||
operItem.value = {
|
operItem.value = {
|
||||||
type: 'dir',
|
type: 'dir',
|
||||||
@@ -154,8 +152,8 @@ async function loadDownloadDirectories() {
|
|||||||
name: '/',
|
name: '/',
|
||||||
path: '/',
|
path: '/',
|
||||||
fileid: 'root',
|
fileid: 'root',
|
||||||
}
|
},
|
||||||
];
|
]
|
||||||
// 将初始数据拆分到堆栈中
|
// 将初始数据拆分到堆栈中
|
||||||
const paths = path.split('/').filter(Boolean)
|
const paths = path.split('/').filter(Boolean)
|
||||||
paths.map((name, index) => {
|
paths.map((name, index) => {
|
||||||
@@ -206,7 +204,7 @@ onMounted(loadDownloadDirectories)
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="file-browser-view">
|
<div class="file-browser-view app-surface">
|
||||||
<FileBrowser
|
<FileBrowser
|
||||||
v-if="operItem"
|
v-if="operItem"
|
||||||
:storages="storages"
|
:storages="storages"
|
||||||
@@ -223,6 +221,7 @@ onMounted(loadDownloadDirectories)
|
|||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.file-browser-view {
|
.file-browser-view {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
block-size: 100%;
|
block-size: 100%;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -144,8 +144,8 @@ onMounted(getModules)
|
|||||||
isChecking
|
isChecking
|
||||||
? t('moduleTest.checking')
|
? t('moduleTest.checking')
|
||||||
: checkComplete
|
: checkComplete
|
||||||
? t('moduleTest.complete')
|
? t('moduleTest.complete')
|
||||||
: t('moduleTest.preparing')
|
: t('moduleTest.preparing')
|
||||||
}}
|
}}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
@@ -230,16 +230,15 @@ onMounted(getModules)
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.system-health-check {
|
.system-health-check {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
overflow: hidden;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
|
flex-direction: column;
|
||||||
block-size: 100%;
|
block-size: 100%;
|
||||||
min-block-size: 0;
|
min-block-size: 0;
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-container {
|
.progress-container {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
background: var(--v-surface-variant);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-card {
|
.progress-card {
|
||||||
@@ -336,10 +335,6 @@ onMounted(getModules)
|
|||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-item:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.module-item.success {
|
.module-item.success {
|
||||||
border-color: #4caf50;
|
border-color: #4caf50;
|
||||||
background: linear-gradient(135deg, #f8fff9 0%, #e8f5e8 100%);
|
background: linear-gradient(135deg, #f8fff9 0%, #e8f5e8 100%);
|
||||||
|
|||||||
Reference in New Issue
Block a user