Feature(custom): migrate all pages to tailwind

This commit is contained in:
Kuingsmile
2026-01-25 16:04:32 +08:00
parent d0853cca1f
commit 88a5a8087a
15 changed files with 462 additions and 1942 deletions

View File

@@ -25,6 +25,7 @@
- 优化相册页面卡片样式,边界更清晰,提升选择框视觉效果。 - 优化相册页面卡片样式,边界更清晰,提升选择框视觉效果。
- 优化多个页面在窄屏下的显示,避免内容溢出。 - 优化多个页面在窄屏下的显示,避免内容溢出。
- 文件浏览侧边栏名称超出宽度时支持滚动显示完整名称。 - 文件浏览侧边栏名称超出宽度时支持滚动显示完整名称。
- 优化了视频播放页面的显示效果。
- 相册页面支持显示已选数量、匹配的 URL 列表和记忆过滤器状态。 - 相册页面支持显示已选数量、匹配的 URL 列表和记忆过滤器状态。
- 支持浏览完整插件列表、查看详情及一键安装。 - 支持浏览完整插件列表、查看详情及一键安装。
- 新增新手引导页面,首次运行自动弹出。 - 新增新手引导页面,首次运行自动弹出。
@@ -38,6 +39,7 @@
- 修复了管理页面中排序下拉框显示异常的问题。 - 修复了管理页面中排序下拉框显示异常的问题。
- 修复了管理页面图床列表未正确高亮当前选中项的问题。 - 修复了管理页面图床列表未正确高亮当前选中项的问题。
- 修复了管理页面markdown预览内容没有正确渲染的问题。
- 修复了暗色模式下任务页面的显示异常。 - 修复了暗色模式下任务页面的显示异常。
- 修复了图床设置页面“设置为默认图床”按钮状态更新不及时的问题。 - 修复了图床设置页面“设置为默认图床”按钮状态更新不及时的问题。
- 修复了预处理设置页面中,图床水印独立设置按钮状态不同步的问题。 - 修复了预处理设置页面中,图床水印独立设置按钮状态不同步的问题。

View File

@@ -29,3 +29,4 @@
@utility no-drag-region { @utility no-drag-region {
-webkit-app-region: none; -webkit-app-region: none;
} }

View File

@@ -175,7 +175,6 @@ function validateForm(): boolean {
} }
function clearFieldError(fieldName: string) { function clearFieldError(fieldName: string) {
console.log('Clearing error for field:', fieldName)
if (validationErrors[fieldName]) { if (validationErrors[fieldName]) {
delete validationErrors[fieldName] delete validationErrors[fieldName]
} }

View File

@@ -6,16 +6,14 @@
<span v-if="required" class="ml-1 text-danger">*</span> <span v-if="required" class="ml-1 text-danger">*</span>
</label> </label>
<slot name="title-extra"></slot> <slot name="title-extra"></slot>
<div v-if="tips" class="relative"> <div v-if="tips" class="group relative inline-block">
<div <div
class="flex h-[20px] w-[20px] cursor-pointer items-center justify-center rounded-full p-[2px] text-secondary hover:bg-bg-secondary hover:text-accent" class="flex h-[20px] w-[20px] cursor-pointer items-center justify-center rounded-full p-[2px] text-secondary hover:bg-bg-secondary hover:text-accent"
@click="toggleTooltip()"
> >
<Info :size="16" /> <Info :size="16" />
</div> </div>
<div <div
v-show="visibleTooltips" class="invisible absolute top-[125%] left-1/2 z-1000 w-max max-w-[200px] translate-x-[-50%] rounded-md border border-border bg-bg-tertiary p-2 text-center text-xs text-main opacity-0 shadow-md transition-opacity duration-300 group-hover:visible group-hover:opacity-100"
class="absolute top-full left-0 z-1000 max-w-[300px] min-w-[200px] rounded-md border border-border bg-bg-secondary p-3 text-xs leading-[1.4] text-main shadow-lg max-md:max-w-[250px] max-md:min-w-[150px]"
v-html="transformMarkdownToHTML(tips)" v-html="transformMarkdownToHTML(tips)"
/> />
</div> </div>
@@ -62,7 +60,6 @@ const [modelValue, modifiers] = defineModel<any>({
}) })
const type = ref('text') const type = ref('text')
const visibleTooltips = ref(false)
const { const {
isPassword = false, isPassword = false,
@@ -80,10 +77,6 @@ const {
tips?: string tips?: string
}>() }>()
function toggleTooltip() {
visibleTooltips.value = !visibleTooltips.value
}
function transformMarkdownToHTML(markdown: string) { function transformMarkdownToHTML(markdown: string) {
try { try {
return marked.parse(markdown) return marked.parse(markdown)

View File

@@ -29,16 +29,14 @@
</div> </div>
</label> </label>
<slot name="title-extra"></slot> <slot name="title-extra"></slot>
<div v-if="tips" class="relative"> <div v-if="tips" class="group relative inline-block">
<div <div
class="flex h-[20px] w-[20px] cursor-pointer items-center justify-center rounded-full p-[2px] text-secondary hover:bg-bg-secondary hover:text-accent" class="flex h-[20px] w-[20px] cursor-pointer items-center justify-center rounded-full p-[2px] text-secondary hover:bg-bg-secondary hover:text-accent"
@click="toggleTooltip()"
> >
<Info :size="16" /> <Info :size="16" />
</div> </div>
<div <div
v-show="visibleTooltips" class="invisible absolute top-[125%] left-1/2 z-1000 w-max max-w-[200px] translate-x-[-50%] rounded-md border border-border bg-bg-tertiary p-2 text-center text-xs text-main opacity-0 shadow-md transition-opacity duration-300 group-hover:visible group-hover:opacity-100"
class="absolute top-full left-0 z-1000 max-w-[300px] min-w-[200px] rounded-md border border-border bg-bg-secondary p-3 text-xs leading-[1.4] text-main shadow-lg max-md:max-w-[250px] max-md:min-w-[150px]"
v-html="transformMarkdownToHTML(tips)" v-html="transformMarkdownToHTML(tips)"
/> />
</div> </div>
@@ -48,12 +46,10 @@
<script lang="ts" setup> <script lang="ts" setup>
import { Info } from 'lucide-vue-next' import { Info } from 'lucide-vue-next'
import { marked } from 'marked' import { marked } from 'marked'
import { onMounted, ref } from 'vue' import { onMounted } from 'vue'
const emit = defineEmits(['change']) const emit = defineEmits(['change'])
const visibleTooltips = ref(false)
const modelValue = defineModel<boolean>() const modelValue = defineModel<boolean>()
const { const {
title = '', title = '',
@@ -75,10 +71,6 @@ const {
tighter?: boolean tighter?: boolean
}>() }>()
function toggleTooltip() {
visibleTooltips.value = !visibleTooltips.value
}
function transformMarkdownToHTML(markdown: string) { function transformMarkdownToHTML(markdown: string) {
try { try {
return marked.parse(markdown) return marked.parse(markdown)

View File

@@ -8,7 +8,7 @@
} }
html { html {
@apply m-0 h-full p-0 w-full bg-transparent; @apply m-0 h-full w-full bg-transparent p-0;
} }
#app { #app {

View File

@@ -3,14 +3,13 @@
<div <div
ref="bucketContainerRef" ref="bucketContainerRef"
class="relative flex h-full w-full items-center justify-center" class="relative flex h-full w-full items-center justify-center"
:class="{ 'content-fullscreen': isContentFullscreen }"
@scroll="handleBucketContainerScroll" @scroll="handleBucketContainerScroll"
> >
<div class="relative z-1 flex h-full w-full flex-col items-center justify-start gap-2 rounded-xl border-none p-0"> <div class="relative z-1 flex h-full w-full flex-col items-center justify-start gap-1 rounded-xl border-none p-0">
<!-- Header Card --> <!-- Header Card -->
<div <div
v-if="!isContentFullscreen" v-if="!isContentFullscreen"
class="flex w-full flex-wrap items-center justify-between gap-4 overflow-visible rounded-xl border border-border-secondary p-0 shadow-sm" class="flex w-full flex-wrap items-center justify-between gap-4 overflow-visible rounded-xl p-0"
> >
<div class="flex flex-1 flex-wrap items-center gap-4 p-1"> <div class="flex flex-1 flex-wrap items-center gap-4 p-1">
<!-- Custom Domain Input/Select --> <!-- Custom Domain Input/Select -->
@@ -130,7 +129,7 @@
<!-- Breadcrumb Card --> <!-- Breadcrumb Card -->
<div <div
v-if="!isContentFullscreen" v-if="!isContentFullscreen"
class="flex w-full items-center justify-between gap-4 overflow-hidden rounded-xl border border-border-secondary p-0 shadow-sm" class="flex w-full items-center justify-between gap-4 overflow-hidden rounded-sm border border-border-secondary p-0"
> >
<div class="flex flex-1 items-center gap-0 overflow-x-auto px-4 py-1"> <div class="flex flex-1 items-center gap-0 overflow-x-auto px-4 py-1">
<HomeIcon class="h-[16px] w-[16px] shrink-0 text-accent" /> <HomeIcon class="h-[16px] w-[16px] shrink-0 text-accent" />
@@ -138,7 +137,7 @@
<template v-for="(item, index) in configMap.prefix.replace(/\/$/g, '').split('/')" :key="index"> <template v-for="(item, index) in configMap.prefix.replace(/\/$/g, '').split('/')" :key="index">
<ChevronRightIcon v-if="index !== 0" class="h-[16px] w-[15px] shrink-0 text-accent" /> <ChevronRightIcon v-if="index !== 0" class="h-[16px] w-[15px] shrink-0 text-accent" />
<button <button
class="flex shrink-0 cursor-pointer items-center gap-1 rounded-md border-none bg-bg-secondary p-1 text-sm font-semibold text-secondary hover:bg-accent/10 hover:text-main" class="flex shrink-0 cursor-pointer items-center gap-1 rounded-md border-none bg-bg-secondary p-1 text-sm font-semibold text-secondary last:bg-accent/10 hover:bg-accent/10 hover:text-main"
@click="handleBreadcrumbClick(Number(index))" @click="handleBreadcrumbClick(Number(index))"
> >
{{ item === '' ? t('pages.manage.bucket.rootFolder') : item }} {{ item === '' ? t('pages.manage.bucket.rootFolder') : item }}
@@ -158,7 +157,7 @@
<!-- Control Panel Card --> <!-- Control Panel Card -->
<div <div
v-if="!isContentFullscreen" v-if="!isContentFullscreen"
class="flex w-full flex-wrap items-center justify-between gap-2 overflow-visible rounded-xl border border-border-secondary p-0 shadow-sm" class="flex w-full flex-wrap items-center justify-between gap-2 overflow-visible rounded-sm border border-border-secondary p-0"
> >
<FileInfo :current-page-files-info="currentPageFilesInfo" :calculate-all-file-size="calculateAllFileSize" /> <FileInfo :current-page-files-info="currentPageFilesInfo" :calculate-all-file-size="calculateAllFileSize" />
@@ -187,14 +186,25 @@
</template> </template>
<!-- Sort Dropdown --> <!-- Sort Dropdown -->
<div class="dropdown"> <div class="relative">
<button class="dropdown-button" @click="sortDropdownOpen = !sortDropdownOpen"> <button
class="flex cursor-pointer items-center gap-2 rounded-md border border-border bg-bg-secondary px-3 py-2 text-sm font-medium text-main hover:border-accent hover:bg-accent/10"
@click="sortDropdownOpen = !sortDropdownOpen"
>
<ArrowUpDownIcon class="action-icon" /> <ArrowUpDownIcon class="action-icon" />
{{ t(`pages.manage.bucket.sort.${currentSortType}`) }} {{ t(`pages.manage.bucket.sort.${currentSortType}`) }}
<ChevronDownIcon class="action-icon" /> <ChevronDownIcon class="action-icon" />
</button> </button>
<div v-if="sortDropdownOpen" class="dropdown-content"> <div
<div v-for="item in sortTypeList" :key="item" class="dropdown-item" @click="sortFile(item as any)"> v-if="sortDropdownOpen"
class="absolute top-full left-0 z-1000 mt-1 min-w-[150px] rounded-md border border-border bg-bg-tertiary shadow-md"
>
<div
v-for="item in sortTypeList"
:key="item"
class="cursor-pointer bg-bg-tertiary px-3 py-2 text-sm text-main transition-all duration-fast ease-apple hover:bg-accent/30 hover:text-white"
@click="sortFile(item as any)"
>
{{ t(`pages.manage.bucket.sort.${item}`) }} {{ t(`pages.manage.bucket.sort.${item}`) }}
</div> </div>
</div> </div>
@@ -235,7 +245,7 @@
class="flex w-full flex-wrap items-center justify-between gap-2 overflow-visible rounded-xl border border-border-secondary p-0 shadow-sm" class="flex w-full flex-wrap items-center justify-between gap-2 overflow-visible rounded-xl border border-border-secondary p-0 shadow-sm"
> >
<div class="flex max-w-[400px] min-w-[200px] items-center overflow-x-auto px-4 py-1"> <div class="flex max-w-[400px] min-w-[200px] items-center overflow-x-auto px-4 py-1">
<div class="fullscreen-breadcrumb rounded-md shadow-sm"> <div class="flex flex-wrap items-center gap-1 rounded-md shadow-sm">
<HomeIcon class="h-[16px] w-[16px] shrink-0 text-accent" /> <HomeIcon class="h-[16px] w-[16px] shrink-0 text-accent" />
<template v-if="configMap.prefix !== '/'"> <template v-if="configMap.prefix !== '/'">
<template v-for="(item, index) in configMap.prefix.replace(/\/$/g, '').split('/')" :key="index"> <template v-for="(item, index) in configMap.prefix.replace(/\/$/g, '').split('/')" :key="index">
@@ -258,7 +268,7 @@
</div> </div>
</div> </div>
<FileInfo :current-page-files-info="currentPageFilesInfo" :calculate-all-file-size="calculateAllFileSize" /> <FileInfo :current-page-files-info="currentPageFilesInfo" :calculate-all-file-size="calculateAllFileSize" />
<div class="fullscreen-header-right"> <div class="flex min-w-[200px] flex-1 items-center justify-end gap-3">
<!-- Search --> <!-- Search -->
<input <input
v-model="searchText" v-model="searchText"
@@ -282,7 +292,7 @@
</div> </div>
<div <div
class="no-scrollbar flex min-h-[500px] w-full flex-1 flex-col flex-wrap items-center justify-center gap-2 overflow-auto rounded-2xl border border-border-secondary p-1 shadow-md" class="no-scrollbar flex min-h-[500px] w-full flex-1 flex-col flex-wrap items-center justify-center gap-2 overflow-auto rounded-md border border-border-secondary p-1 shadow-md"
> >
<div v-if="filterList.length === 0" class="h-full w-full"> <div v-if="filterList.length === 0" class="h-full w-full">
<EmptyPage /> <EmptyPage />
@@ -366,13 +376,13 @@
</template> </template>
</div> </div>
<div class="flex shrink-0 flex-col justify-between gap-0.5"> <div class="flex min-w-0 shrink-0 flex-col justify-between gap-0.5">
<div <div
class="overflow-hidden text-sm font-medium text-ellipsis whitespace-nowrap text-main" class="w-full truncate text-center text-sm font-medium text-main"
:title="item.fileName" :title="item.fileName"
@click.stop="copyToClipboard(item.fileName ?? '')" @click.stop="copyToClipboard(item.fileName ?? '')"
> >
<div class="text-center">{{ item.fileName ?? '' }}</div> {{ item.fileName ?? '' }}
</div> </div>
<div <div
v-if="!item.isDir" v-if="!item.isDir"
@@ -512,7 +522,7 @@
@click="copyToClipboard(JSON.stringify(currentShowedFileInfo, null, 2))" @click="copyToClipboard(JSON.stringify(currentShowedFileInfo, null, 2))"
/> />
</div> </div>
<div class="modal-content"> <div class="flex-1 overflow-y-auto bg-bg p-6">
<div <div
v-for="(value, key) in currentShowedFileInfo" v-for="(value, key) in currentShowedFileInfo"
:key="key" :key="key"
@@ -548,9 +558,12 @@
<div class="mb-6 last:mb-0"> <div class="mb-6 last:mb-0">
<label class="mb-2 flex items-center gap-2 text-sm font-medium text-main"> <label class="mb-2 flex items-center gap-2 text-sm font-medium text-main">
{{ t('pages.manage.bucket.matchedPattern', { num: matchedFilesNumber.length }) }} {{ t('pages.manage.bucket.matchedPattern', { num: matchedFilesNumber.length }) }}
<div class="tooltip"> <div class="group relative inline-block">
<InfoIcon class="action-icon" /> <InfoIcon class="action-icon" />
<span class="tooltip-text">{{ t('pages.manage.bucket.regexPatternTips') }}</span> <span
class="invisible absolute top-[125%] left-1/2 z-1000 w-max max-w-[200px] translate-x-[-50%] rounded-md border border-border bg-bg-tertiary p-2 text-center text-xs text-main opacity-0 shadow-md transition-opacity duration-300 group-hover:visible group-hover:opacity-100"
>{{ t('pages.manage.bucket.regexPatternTips') }}</span
>
</div> </div>
</label> </label>
<input <input
@@ -663,55 +676,55 @@
</div> </div>
</div> </div>
<!-- Upload Drawer --> <!-- Upload Drawer -->
<div <CustomModal
v-if="isShowUploadPanel" v-if="isShowUploadPanel"
class="drawer-overlay" v-model:visible="isShowUploadPanel"
:class="{ open: isShowUploadPanel }" :title="t('pages.manage.bucket.uploadFile')"
@click="isShowUploadPanel = false" width="900px"
height="90vh"
> >
<div class="drawer-container" @click.stop> <div class="flex h-full w-full flex-col gap-2">
<div class="drawer-header"> <div class="flex justify-end">
<h3 class="drawer-title"> <CustomSwitch
{{ t('pages.manage.bucket.uploadFile') }} v-model="isUploadKeepDirStructure"
</h3> :title="
<div class="switch-container">
<label class="switch">
<input v-model="isUploadKeepDirStructure" type="checkbox" @change="handleUploadKeepDirChange" />
<span class="switch-slider" />
</label>
<span class="switch-label">
{{
isUploadKeepDirStructure isUploadKeepDirStructure
? t('pages.manage.bucket.keepDirStructure') ? t('pages.manage.bucket.keepDirStructure')
: t('pages.manage.bucket.noKeepDirStructure') : t('pages.manage.bucket.noKeepDirStructure')
}} "
</span> small
</div> no-border
<button class="modal-close" @click="isShowUploadPanel = false"> @change="handleUploadKeepDirChange"
<XIcon class="action-icon" /> />
</button>
</div> </div>
<div class="drawer-content"> <div class="no-scrollbar w-full flex-1 overflow-hidden rounded-md border border-border p-4 shadow-md">
<div class="flex h-full w-full flex-col">
<div <div
v-if="!tableData.length" v-if="!tableData.length"
class="upload-area" ref="uploadDialog"
class="h-[200px] w-full cursor-pointer rounded-lg border-2 border-dashed border-border bg-surface p-4 text-center transition-all duration-fast ease-apple hover:border-accent hover:bg-accent/10 [.dragover]:border-accent [.dragover]:bg-accent/10"
:class="{ dragover: isDragover }" :class="{ dragover: isDragover }"
@drop.prevent="onDrop" @drop.prevent="onDrop"
@dragover.prevent="isDragover = true" @dragover.prevent="isDragover = true"
@dragleave.prevent="isDragover = false" @dragleave.prevent="isDragover = false"
@click="openFileSelectDialog" @click="openFileSelectDialog"
> >
<div class="upload-area-text"> <div class="flex h-full flex-col items-center justify-center gap-2">
<div class="mb-2 text-lg font-semibold text-secondary">
{{ t('pages.manage.bucket.dragUpload') }} {{ t('pages.manage.bucket.dragUpload') }}
</div> </div>
<div class="upload-area-subtext"> <div class="text-sm font-medium text-secondary">
{{ t('pages.manage.bucket.clickUpload') }} {{ t('pages.manage.bucket.clickUpload') }}
</div> </div>
</div> </div>
</div>
<!-- Upload File List --> <!-- Upload File List -->
<div v-if="tableData.length"> <div
v-if="tableData.length"
class="flex h-[200px] w-full cursor-pointer rounded-lg border-2 border-dashed border-border bg-surface p-4 text-center transition-all duration-fast ease-apple"
>
<VirtualScroller <VirtualScroller
:items=" :items="
tableData.sort((a, b) => tableData.sort((a, b) =>
@@ -719,23 +732,32 @@
) )
" "
:item-height="90" :item-height="90"
:style="{ height: '100%' }" class="min-h-0 w-full flex-1 p-3"
view-mode="list" view-mode="list"
> >
<template #default="{ item }"> <template #default="{ item }">
<div class="file-list-item"> <div
<div class="file-list-icon"> class="m-0 flex w-full cursor-pointer items-center gap-2 rounded-md border border-border-secondary bg-bg-secondary px-4 py-3 hover:bg-accent/10"
<FolderIcon v-if="item.isFolder" class="file-icon" /> >
<FileIcon v-else class="file-icon" /> <div class="flex h-[25px] w-[25px] shrink-0 items-center justify-center">
<FolderIcon v-if="item.isFolder" class="h-[48px] w-[48px] text-tertiary" />
<FileIcon v-else class="h-[48px] w-[48px] text-tertiary" />
</div> </div>
<div class="file-list-info"> <div class="min-w-0 flex-1 flex-col">
<div class="file-list-name"> <div class="flex flex-row justify-between gap-3">
{{ formatFileName(item.name) }} <div
class="mb-1 cursor-pointer overflow-hidden text-sm font-semibold text-ellipsis whitespace-nowrap text-secondary"
>
{{ item.name }}
</div> </div>
<div v-if="item.fullPath" class="file-list-path"> <div
v-if="item.fullPath"
class="overflow-hidden text-sm font-medium text-ellipsis whitespace-nowrap text-secondary"
>
{{ item.fullPath }} {{ item.fullPath }}
</div> </div>
<div class="file-list-meta"> </div>
<div class="flex text-xs text-secondary">
<span>{{ formatFileSize(item.fileSize) }}</span> <span>{{ formatFileSize(item.fileSize) }}</span>
<span v-if="item.isFolder"> {{ item.filesList.length }} files </span> <span v-if="item.isFolder"> {{ item.filesList.length }} files </span>
</div> </div>
@@ -746,162 +768,129 @@
</div> </div>
<!-- Upload Actions --> <!-- Upload Actions -->
<div v-if="tableData.length" style="display: flex; justify-content: center; gap: 1rem; margin-top: 1rem"> <div v-if="tableData.length" class="mt-4 flex justify-center gap-4">
<button class="action-button primary" :disabled="isLoadingUploadPanelFiles" @click="uploadFiles"> <CustomButton
<UploadIcon class="action-icon" /> type="primary"
{{ isLoadingUploadPanelFiles ? t('pages.manage.bucket.readingDir') : t('pages.manage.bucket.upload') }} :disabled="isLoadingUploadPanelFiles"
</button> :text="
<button class="action-button secondary" :disabled="isLoadingUploadPanelFiles" @click="clearTableData"> isLoadingUploadPanelFiles ? t('pages.manage.bucket.readingDir') : t('pages.manage.bucket.upload')
<Trash2Icon class="action-icon" /> "
{{ t('pages.manage.bucket.clear') }} :icon="UploadIcon"
</button> @click="uploadFiles"
/>
<CustomButton
type="secondary"
:icon="Trash2Icon"
:text="t('pages.manage.bucket.clear')"
:disabled="isLoadingUploadPanelFiles"
@click="clearTableData"
/>
</div> </div>
<!-- Upload Tasks Tabs --> <!-- Upload Tasks Tabs -->
<div class="tabs-container"> <div class="flex flex-1 flex-col gap-2 overflow-hidden border-t border-border-secondary">
<div class="tabs-header"> <div class="flex shrink-0 border-b border-b-border">
<button <button
class="tab-button" class="relative flex-1 rounded-md border-b-2 border-b-transparent bg-none px-6 py-3 text-sm font-semibold text-secondary shadow-sm transition-all duration-fast ease-apple hover:border-b-accent hover:text-main [.active]:border-b-accent [.active]:bg-accent [.active]:text-white"
:class="{ active: activeUpLoadTab === 'uploading' }" :class="{ active: activeUpLoadTab === 'uploading' }"
@click="activeUpLoadTab = 'uploading'" @click="activeUpLoadTab = 'uploading'"
> >
{{ t('pages.manage.bucket.uploading') }} {{ t('pages.manage.bucket.uploading') }}
<span v-if="uploadingTaskList.length" class="tab-badge"> <span
v-if="uploadingTaskList.length"
class="absolute top-1 right-1 min-w-[16px] rounded-full bg-accent px-1.5 py-0.5 text-center text-xs text-white"
>
{{ uploadingTaskList.length }} {{ uploadingTaskList.length }}
</span> </span>
</button> </button>
<button <button
class="tab-button" class="relative flex-1 rounded-md border-b-2 border-b-transparent bg-none px-6 py-3 text-sm font-semibold text-secondary shadow-sm transition-all duration-fast ease-apple hover:border-b-accent hover:text-main [.active]:border-b-accent [.active]:bg-accent [.active]:text-white"
:class="{ active: activeUpLoadTab === 'finished' }" :class="{ active: activeUpLoadTab === 'finished' }"
@click="activeUpLoadTab = 'finished'" @click="activeUpLoadTab = 'finished'"
> >
{{ t('pages.manage.bucket.success') }} {{ t('pages.manage.bucket.success') }}
<span v-if="uploadedTaskList.filter(item => item.status === 'uploaded').length" class="tab-badge"> <span
v-if="uploadedTaskList.filter(item => item.status === 'uploaded').length"
class="absolute top-1 right-1 min-w-[16px] rounded-full bg-accent px-1.5 py-0.5 text-center text-xs text-white"
>
{{ uploadedTaskList.filter(item => item.status === 'uploaded').length }} {{ uploadedTaskList.filter(item => item.status === 'uploaded').length }}
</span> </span>
</button> </button>
<button <button
class="tab-button" class="relative flex-1 rounded-md border-b-2 border-b-transparent bg-none px-6 py-3 text-sm font-semibold text-secondary shadow-sm transition-all duration-fast ease-apple hover:border-b-accent hover:text-main [.active]:border-b-accent [.active]:bg-accent [.active]:text-white"
:class="{ active: activeUpLoadTab === 'failed' }" :class="{ active: activeUpLoadTab === 'failed' }"
@click="activeUpLoadTab = 'failed'" @click="activeUpLoadTab = 'failed'"
> >
{{ t('pages.manage.bucket.failed') }} {{ t('pages.manage.bucket.failed') }}
<span v-if="uploadedTaskList.filter(item => item.status !== 'uploaded').length" class="tab-badge"> <span
v-if="uploadedTaskList.filter(item => item.status !== 'uploaded').length"
class="absolute top-1 right-1 min-w-[16px] rounded-full bg-accent px-1.5 py-0.5 text-center text-xs text-white"
>
{{ uploadedTaskList.filter(item => item.status !== 'uploaded').length }} {{ uploadedTaskList.filter(item => item.status !== 'uploaded').length }}
</span> </span>
</button> </button>
</div> </div>
<div class="tab-content"> <div class="flex flex-row justify-center gap-3 rounded-md border border-border shadow-sm">
<CustomButton
type="secondary"
:text="t('pages.manage.bucket.copyUploadTask')"
:icon="CopyIcon"
@click="handleCopyUploadingTaskInfo"
/>
<CustomButton
type="secondary"
:text="t('pages.manage.bucket.clearFinishedTasks')"
:icon="Trash2Icon"
@click="handleDeleteUploadedTask"
/>
<CustomButton
type="secondary"
:text="t('pages.manage.bucket.clearAll')"
:icon="Trash2Icon"
@click="handleDeleteAllUploadedTask"
/>
</div>
<div class="w-full flex-1 overflow-auto rounded-md border border-border-secondary p-2">
<!-- Uploading Tab --> <!-- Uploading Tab -->
<div v-if="activeUpLoadTab === 'uploading'" class="tab-panel">
<div style="display: flex; gap: 0.5rem; margin-bottom: 1rem">
<button class="action-button secondary" @click="handleCopyUploadingTaskInfo">
<CopyIcon class="action-icon" />
{{ t('pages.manage.bucket.copyUploadTask') }}
</button>
<button class="action-button secondary" @click="handleDeleteUploadedTask">
<Trash2Icon class="action-icon" />
{{ t('pages.manage.bucket.clearFinishedTasks') }}
</button>
<button class="action-button secondary" @click="handleDeleteAllUploadedTask">
<Trash2Icon class="action-icon" />
{{ t('pages.manage.bucket.clearAll') }}
</button>
</div>
<VirtualScroller <VirtualScroller
:items="uploadingTaskList" :items="
activeUpLoadTab === 'uploading'
? uploadingTaskList
: activeUpLoadTab === 'finished'
? uploadedTaskList.filter(item => item.status === 'uploaded')
: uploadedTaskList.filter(item => item.status !== 'uploaded')
"
:item-height="70" :item-height="70"
:style="{ height: '100%' }" class="min-h-0 w-full flex-1 p-3"
view-mode="list" view-mode="list"
> >
<template #default="{ item }"> <template #default="{ item }">
<div class="file-list-item"> <div
<div class="file-list-info"> class="m-0 flex w-full cursor-pointer items-center gap-3 rounded-md border border-border bg-bg-secondary px-4 py-3 hover:border-accent hover:shadow-md"
<div class="file-list-name">
{{ formatFileName(item.sourceFileName) }}
</div>
<div class="progress-bar">
<div class="progress-fill" :style="{ width: `${item.progress || 50}%` }" />
</div>
</div>
</div>
</template>
</VirtualScroller>
</div>
<!-- Finished Tab -->
<div v-if="activeUpLoadTab === 'finished'" class="tab-panel">
<div style="display: flex; gap: 0.5rem; margin-bottom: 1rem">
<button class="action-button secondary" @click="handleCopyUploadingTaskInfo">
<CopyIcon class="action-icon" />
{{ t('pages.manage.bucket.copyUploadTask') }}
</button>
<button class="action-button secondary" @click="handleDeleteUploadedTask">
<Trash2Icon class="action-icon" />
{{ t('pages.manage.bucket.clearFinishedTasks') }}
</button>
<button class="action-button secondary" @click="handleDeleteAllUploadedTask">
<Trash2Icon class="action-icon" />
{{ t('pages.manage.bucket.clearAll') }}
</button>
</div>
<VirtualScroller
:items="uploadedTaskList.filter(item => item.status === 'uploaded')"
:item-height="70"
:style="{ height: '100%' }"
view-mode="list"
> >
<template #default="{ item }"> <div class="flex flex-1 flex-col gap-1">
<div class="file-list-item"> <div class="overflow-hidden text-sm font-medium text-ellipsis whitespace-nowrap text-secondary">
<div class="file-list-info"> {{ item.sourceFileName }}
<div class="file-list-name">
{{ formatFileName(item.sourceFileName) }}
</div> </div>
<div class="file-list-meta"> <div
v-if="activeUpLoadTab === 'uploading'"
class="h-[8px] w-full overflow-hidden rounded-[4px] bg-surface-elevated"
>
<div
class="h-full rounded-[4px] bg-accent transition-all duration-300 ease-apple"
:style="{ width: `${item.progress}%` }"
/>
</div>
<div v-else class="flex gap-4 text-xs text-secondary">
<span>{{ item.finishTime }}</span> <span>{{ item.finishTime }}</span>
<span class="badge success"> <span class="text-xs font-semibold text-success">
{{ t('pages.manage.bucket.success') }} {{
</span> activeUpLoadTab === 'finished'
</div> ? t('pages.manage.bucket.success')
</div> : t('pages.manage.bucket.failed')
</div> }}
</template>
</VirtualScroller>
</div>
<!-- Failed Tab -->
<div v-if="activeUpLoadTab === 'failed'" class="tab-panel">
<div style="display: flex; gap: 0.5rem; margin-bottom: 1rem">
<button class="action-button secondary" @click="handleCopyUploadingTaskInfo">
<CopyIcon class="action-icon" />
{{ t('pages.manage.bucket.copyUploadTask') }}
</button>
<button class="action-button secondary" @click="handleDeleteUploadedTask">
<Trash2Icon class="action-icon" />
{{ t('pages.manage.bucket.clearFinishedTasks') }}
</button>
<button class="action-button secondary" @click="handleDeleteAllUploadedTask">
<Trash2Icon class="action-icon" />
{{ t('pages.manage.bucket.clearAll') }}
</button>
</div>
<VirtualScroller
:items="uploadedTaskList.filter(item => item.status !== 'uploaded')"
:item-height="70"
:style="{ height: '100%' }"
view-mode="list"
>
<template #default="{ item }">
<div class="file-list-item">
<div class="file-list-info">
<div class="file-list-name">
{{ formatFileName(item.sourceFileName) }}
</div>
<div class="file-list-meta">
<span>{{ item.finishTime }}</span>
<span class="badge error">
{{ t('pages.manage.bucket.failed') }}
</span> </span>
</div> </div>
</div> </div>
@@ -913,184 +902,128 @@
</div> </div>
</div> </div>
</div> </div>
</div> </CustomModal>
<!-- Download Drawer --> <!-- Download Drawer -->
<div <CustomModal
v-if="isShowDownloadPanel" v-if="isShowDownloadPanel"
class="drawer-overlay" v-model:visible="isShowDownloadPanel"
:class="{ open: isShowDownloadPanel }" :title="t('pages.manage.bucket.downloadPage')"
@click="isShowDownloadPanel = false" width="900px"
height="90vh"
> >
<div class="drawer-container" @click.stop> <div class="no-scrollbar w-full flex-1 overflow-hidden rounded-md border border-border p-4 shadow-md">
<div class="drawer-header"> <div class="flex h-full w-full flex-col">
<h3 class="drawer-title">
{{ t('pages.manage.bucket.downloadPage') }}
</h3>
<button class="modal-close" @click="isShowDownloadPanel = false">
<XIcon class="action-icon" />
</button>
</div>
<div class="drawer-content">
<!-- Download Tasks Tabs --> <!-- Download Tasks Tabs -->
<div class="tabs-container"> <div class="flex flex-1 flex-col gap-2 overflow-hidden border-t border-border-secondary">
<div class="tabs-header"> <div class="flex shrink-0 border-b border-b-border">
<button <button
class="tab-button" class="relative flex-1 rounded-md border-b-2 border-b-transparent bg-none px-6 py-3 text-sm font-semibold text-secondary shadow-sm transition-all duration-fast ease-apple hover:border-b-accent hover:text-main [.active]:border-b-accent [.active]:bg-accent [.active]:text-white"
:class="{ active: activeDownLoadTab === 'downloading' }" :class="{ active: activeDownLoadTab === 'downloading' }"
@click="activeDownLoadTab = 'downloading'" @click="activeDownLoadTab = 'downloading'"
> >
{{ t('pages.manage.bucket.downloading') }} {{ t('pages.manage.bucket.downloading') }}
<span v-if="downloadingTaskList.length" class="tab-badge"> <span
v-if="downloadingTaskList.length"
class="absolute top-1 right-1 min-w-[16px] rounded-full bg-accent px-1.5 py-0.5 text-center text-xs text-white"
>
{{ downloadingTaskList.length }} {{ downloadingTaskList.length }}
</span> </span>
</button> </button>
<button <button
class="tab-button" class="relative flex-1 rounded-md border-b-2 border-b-transparent bg-none px-6 py-3 text-sm font-semibold text-secondary shadow-sm transition-all duration-fast ease-apple hover:border-b-accent hover:text-main [.active]:border-b-accent [.active]:bg-accent [.active]:text-white"
:class="{ active: activeDownLoadTab === 'finished' }" :class="{ active: activeDownLoadTab === 'finished' }"
@click="activeDownLoadTab = 'finished'" @click="activeDownLoadTab = 'finished'"
> >
{{ t('pages.manage.bucket.success') }} {{ t('pages.manage.bucket.success') }}
<span v-if="downloadedTaskList.filter(item => item.status === 'downloaded').length" class="tab-badge"> <span
v-if="downloadedTaskList.filter(item => item.status === 'downloaded').length"
class="absolute top-1 right-1 min-w-[16px] rounded-full bg-accent px-1.5 py-0.5 text-center text-xs text-white"
>
{{ downloadedTaskList.filter(item => item.status === 'downloaded').length }} {{ downloadedTaskList.filter(item => item.status === 'downloaded').length }}
</span> </span>
</button> </button>
<button <button
class="tab-button" class="relative flex-1 rounded-md border-b-2 border-b-transparent bg-none px-6 py-3 text-sm font-semibold text-secondary shadow-sm transition-all duration-fast ease-apple hover:border-b-accent hover:text-main [.active]:border-b-accent [.active]:bg-accent [.active]:text-white"
:class="{ active: activeDownLoadTab === 'failed' }" :class="{ active: activeDownLoadTab === 'failed' }"
@click="activeDownLoadTab = 'failed'" @click="activeDownLoadTab = 'failed'"
> >
{{ t('pages.manage.bucket.failed') }} {{ t('pages.manage.bucket.failed') }}
<span v-if="downloadedTaskList.filter(item => item.status !== 'downloaded').length" class="tab-badge"> <span
v-if="downloadedTaskList.filter(item => item.status !== 'downloaded').length"
class="absolute top-1 right-1 min-w-[16px] rounded-full bg-accent px-1.5 py-0.5 text-center text-xs text-white"
>
{{ downloadedTaskList.filter(item => item.status !== 'downloaded').length }} {{ downloadedTaskList.filter(item => item.status !== 'downloaded').length }}
</span> </span>
</button> </button>
</div> </div>
<!-- Download Tabs Content --> <div class="flex flex-row justify-center gap-3 rounded-md border border-border shadow-sm">
<div class="tab-content"> <CustomButton
type="secondary"
:text="t('pages.manage.bucket.copyDownloadTask')"
:icon="CopyIcon"
@click="handleCopyDownloadingTaskInfo"
/>
<CustomButton
type="secondary"
:text="t('pages.manage.bucket.clearFinishedTasks')"
:icon="Trash2Icon"
@click="handleDeleteDownloadedTask"
/>
<CustomButton
type="secondary"
:text="t('pages.manage.bucket.clearAll')"
:icon="Trash2Icon"
@click="handleDeleteAllDownloadedTask"
/>
<CustomButton
type="secondary"
:text="t('pages.manage.bucket.openDownloadFolder')"
:icon="FolderIcon"
@click="handleOpenDownloadedFolder"
/>
</div>
<div class="w-full flex-1 overflow-auto rounded-md border border-border-secondary p-2">
<!-- Downloading Tab --> <!-- Downloading Tab -->
<div v-if="activeDownLoadTab === 'downloading'" class="tab-panel">
<div style="display: flex; gap: 0.5rem; margin-bottom: 1rem">
<button class="action-button secondary" @click="handleCopyDownloadingTaskInfo">
<CopyIcon class="action-icon" />
{{ t('pages.manage.bucket.copyDownloadTask') }}
</button>
<button class="action-button secondary" @click="handleDeleteDownloadedTask">
<Trash2Icon class="action-icon" />
{{ t('pages.manage.bucket.clearFinishedTasks') }}
</button>
<button class="action-button secondary" @click="handleDeleteAllDownloadedTask">
<Trash2Icon class="action-icon" />
{{ t('pages.manage.bucket.clearAll') }}
</button>
<button class="action-button secondary" @click="handleOpenDownloadedFolder">
<FolderIcon class="action-icon" />
{{ t('pages.manage.bucket.openDownloadFolder') }}
</button>
</div>
<VirtualScroller <VirtualScroller
:items="downloadingTaskList" :items="
activeDownLoadTab === 'downloading'
? downloadingTaskList
: activeDownLoadTab === 'finished'
? downloadedTaskList.filter(item => item.status === 'downloaded')
: downloadedTaskList.filter(item => item.status !== 'downloaded')
"
:item-height="70" :item-height="70"
:style="{ height: '100%' }" class="min-h-0 w-full flex-1 p-3"
view-mode="list" view-mode="list"
> >
<template #default="{ item }"> <template #default="{ item }">
<div class="file-list-item"> <div
<div class="file-list-info"> class="m-0 flex w-full cursor-pointer items-center gap-3 rounded-md border border-border bg-bg-secondary px-4 py-3 hover:border-accent hover:shadow-md"
<div class="file-list-name">
{{ formatFileName(item.sourceFileName) }}
</div>
<div class="progress-bar">
<div class="progress-fill" :style="{ width: `${item.progress}%` }" />
</div>
</div>
</div>
</template>
</VirtualScroller>
</div>
<!-- Finished Tab -->
<div v-if="activeDownLoadTab === 'finished'" class="tab-panel">
<div style="display: flex; gap: 0.5rem; margin-bottom: 1rem">
<button class="action-button secondary" @click="handleCopyDownloadingTaskInfo">
<CopyIcon class="action-icon" />
{{ t('pages.manage.bucket.copyDownloadTask') }}
</button>
<button class="action-button secondary" @click="handleDeleteDownloadedTask">
<Trash2Icon class="action-icon" />
{{ t('pages.manage.bucket.clearFinishedTasks') }}
</button>
<button class="action-button secondary" @click="handleDeleteAllDownloadedTask">
<Trash2Icon class="action-icon" />
{{ t('pages.manage.bucket.clearAll') }}
</button>
<button class="action-button secondary" @click="handleOpenDownloadedFolder">
<FolderIcon class="action-icon" />
{{ t('pages.manage.bucket.openDownloadFolder') }}
</button>
</div>
<VirtualScroller
:items="downloadedTaskList.filter(item => item.status === 'downloaded')"
:item-height="70"
:style="{ height: '100%' }"
view-mode="list"
> >
<template #default="{ item }"> <div class="flex flex-1 flex-col gap-1">
<div class="file-list-item"> <div class="overflow-hidden text-sm font-medium text-ellipsis whitespace-nowrap text-secondary">
<div class="file-list-info"> {{ item.sourceFileName }}
<div class="file-list-name">
{{ formatFileName(item.sourceFileName) }}
</div> </div>
<div class="file-list-meta"> <div
v-if="activeDownLoadTab === 'downloading'"
class="relative h-[8px] w-full overflow-hidden rounded-[4px] bg-surface-elevated"
>
<div
class="h-full rounded-[4px] bg-accent transition-all duration-300 ease-apple"
:style="{ width: `${item.progress}%` }"
/>
</div>
<div v-else class="flex gap-4 text-xs text-secondary">
<span>{{ item.finishTime }}</span> <span>{{ item.finishTime }}</span>
<span class="badge success"> <span class="text-xs font-semibold text-success">
{{ t('pages.manage.bucket.success') }} {{
</span> activeUpLoadTab === 'finished'
</div> ? t('pages.manage.bucket.success')
</div> : t('pages.manage.bucket.failed')
</div> }}
</template>
</VirtualScroller>
</div>
<!-- Failed Tab -->
<div v-if="activeDownLoadTab === 'failed'" class="tab-panel">
<div style="display: flex; gap: 0.5rem; margin-bottom: 1rem">
<button class="action-button secondary" @click="handleCopyDownloadingTaskInfo">
<CopyIcon class="action-icon" />
{{ t('pages.manage.bucket.copyDownloadTask') }}
</button>
<button class="action-button secondary" @click="handleDeleteDownloadedTask">
<Trash2Icon class="action-icon" />
{{ t('pages.manage.bucket.clearFinishedTasks') }}
</button>
<button class="action-button secondary" @click="handleDeleteAllDownloadedTask">
<Trash2Icon class="action-icon" />
{{ t('pages.manage.bucket.clearAll') }}
</button>
<button class="action-button secondary" @click="handleOpenDownloadedFolder">
<FolderIcon class="action-icon" />
{{ t('pages.manage.bucket.openDownloadFolder') }}
</button>
</div>
<VirtualScroller
:items="downloadedTaskList.filter(item => item.status !== 'downloaded')"
:style="{ height: '100%' }"
:item-height="70"
view-mode="list"
>
<template #default="{ item }">
<div class="file-list-item">
<div class="file-list-info">
<div class="file-list-name">
{{ formatFileName(item.sourceFileName) }}
</div>
<div class="file-list-meta">
<span>{{ item.finishTime }}</span>
<span class="badge error">
{{ t('pages.manage.bucket.failed') }}
</span> </span>
</div> </div>
</div> </div>
@@ -1101,57 +1034,68 @@
</div> </div>
</div> </div>
</div> </div>
</div> </CustomModal>
</div>
<!-- Markdown Preview Dialog --> <!-- Markdown Preview Dialog -->
<div v-if="isShowMarkDownDialog" class="modal-overlay" @click="isShowMarkDownDialog = false"> <CustomModal
<div class="modal-container" style="width: 90vw; height: 90vh" @click.stop> v-if="isShowMarkDownDialog"
<div class="modal-header"> v-model:visible="isShowMarkDownDialog"
<h3 class="modal-title"> width="80vw"
{{ t('pages.manage.bucket.preview') }} height="80vh"
</h3> :title="t('pages.manage.bucket.preview')"
<button class="modal-close" @click="isShowMarkDownDialog = false"> >
<XIcon class="action-icon" /> <div class="flex h-full w-full">
</button> <div class="notes-body" style="user-select: text" v-html="markDownContent" />
</div>
<div class="modal-content" style="user-select: text" v-html="markDownContent" />
</div>
</div> </div>
</CustomModal>
<!-- Text File Preview Dialog --> <!-- Text File Preview Dialog -->
<div v-if="isShowTextFileDialog" class="modal-overlay" @click="isShowTextFileDialog = false"> <CustomModal
<div class="modal-container" style="width: 90vw; height: 90vh" @click.stop> v-if="isShowTextFileDialog"
<div class="modal-header"> v-model:visible="isShowTextFileDialog"
<h3 class="modal-title"> width="80vw"
{{ t('pages.manage.bucket.preview') }} height="80vh"
</h3> :title="t('pages.manage.bucket.preview')"
<button class="modal-close" @click="isShowTextFileDialog = false"> >
<XIcon class="action-icon" /> <div class="flex h-full w-full">
</button> <pre class="overflow-auto font-['SF_Mono',Monaco,Menlo,'Ubuntu_Mono',monospace] text-base text-main">{{
</div> textfileContent
<div class="modal-content"> }}</pre>
<pre style="font-family: monospace; white-space: pre-wrap; user-select: text">{{ textfileContent }}</pre>
</div>
</div>
</div> </div>
</CustomModal>
<!-- Video Player Dialog --> <!-- Video Player Dialog -->
<div v-if="isShowVideoFileDialog" class="modal-overlay" @click="isShowVideoFileDialog = false"> <CustomModal
<div class="modal-container" style="width: 90vw; height: 90vh" @click.stop> v-if="isShowVideoFileDialog"
<div class="modal-header"> v-model:visible="isShowVideoFileDialog"
<h3 class="modal-title"> width="90vw"
{{ t('pages.manage.bucket.play') }} height="90vh"
</h3> :title="t('pages.manage.bucket.play')"
<button class="modal-close" @click="isShowVideoFileDialog = false"> >
<XIcon class="action-icon" /> <div class="flex h-full w-full items-center justify-center bg-black">
</button> <video-player
</div> class="video-player"
<div class="modal-content"> :src="videoFileUrl"
<video :src="videoFileUrl" controls loop autoplay style="width: 100%; height: auto; max-height: 70vh" /> :volume="0.6"
</div> :options="{
</div> autoplay: true,
muted: false,
responsive: true,
fill: true,
fluid: false,
controlBar: {
volumePanel: {
inline: false,
},
},
}"
crossorigin="anonymous"
controls
playsinline
loop
/>
</div> </div>
</CustomModal>
<!-- Create Folder Dialog --> <!-- Create Folder Dialog -->
<CustomModal <CustomModal
@@ -1234,7 +1178,6 @@ import { useDownloadFileTransferStore, useFileTransferStore, useManageStore } fr
import { import {
customStrMatch, customStrMatch,
customStrReplace, customStrReplace,
formatFileName,
formatFileSize, formatFileSize,
formatLink, formatLink,
getFileIconPath, getFileIconPath,
@@ -1245,6 +1188,7 @@ import { getConfig, saveConfig } from '@/manage/utils/dataSender'
import { textFileExt } from '@/manage/utils/textfile' import { textFileExt } from '@/manage/utils/textfile'
import { videoExt } from '@/manage/utils/videofile' import { videoExt } from '@/manage/utils/videofile'
import { trimPath } from '@/utils/common' import { trimPath } from '@/utils/common'
import { useDragEventListeners } from '@/utils/drag'
import { IRPCActionType } from '@/utils/enum' import { IRPCActionType } from '@/utils/enum'
import { cancelDownloadLoadingFileList, refreshDownloadFileTransferList } from '@/utils/static' import { cancelDownloadLoadingFileList, refreshDownloadFileTransferList } from '@/utils/static'
/* /*
@@ -1265,6 +1209,8 @@ const props = defineProps<{
type ISortTypeList = 'name' | 'size' | 'time' | 'ext' | 'check' | 'init' type ISortTypeList = 'name' | 'size' | 'time' | 'ext' | 'check' | 'init'
const uploadDialog = useTemplateRef<HTMLDivElement>('uploadDialog')
useDragEventListeners(uploadDialog)
let fileTransferInterval: NodeJS.Timeout | undefined let fileTransferInterval: NodeJS.Timeout | undefined
let downloadInterval: NodeJS.Timeout | undefined let downloadInterval: NodeJS.Timeout | undefined
let scrollTimeout: ReturnType<typeof setTimeout> | undefined let scrollTimeout: ReturnType<typeof setTimeout> | undefined
@@ -1536,8 +1482,8 @@ function getList() {
}) })
} }
function handleUploadKeepDirChange() { function handleUploadKeepDirChange(value: boolean) {
saveConfig('settings.isUploadKeepDirStructure', isUploadKeepDirStructure.value) saveConfig('settings.isUploadKeepDirStructure', value)
manageStore.refreshConfig() manageStore.refreshConfig()
} }
@@ -1896,7 +1842,7 @@ async function handleClickFile(item: any) {
const fileUrl = item.url const fileUrl = item.url
const res = await fetch(fileUrl, options) const res = await fetch(fileUrl, options)
const content = await res.text() const content = await res.text()
markDownContent.value = await marked.parse(content) markDownContent.value = await marked(content, { breaks: true, gfm: true })
isShowMarkDownDialog.value = true isShowMarkDownDialog.value = true
} catch (_error) { } catch (_error) {
message.error(t('pages.manage.bucket.loadingFailed')) message.error(t('pages.manage.bucket.loadingFailed'))

View File

@@ -40,16 +40,16 @@
> >
<div class="flex h-full w-full"> <div class="flex h-full w-full">
<div <div
class="flex min-h-0 max-w-[400px] min-w-[40px] flex-col border-r-2 border-r-border transition-all duration-100 ease-out" class="flex min-h-0 w-[40px] max-w-[400px] min-w-[40px] flex-col border-r-2 border-r-border transition-all duration-100 ease-out"
:style="{ width: sidebarWidth + 'px' }" :style="{ width: sidebarWidth + 'px' }"
> >
<div class="shrink-0 border-b-2 border-b-border-secondary p-2"> <div v-if="menuTitleMap[currentPicBedName]" class="shrink-0 border-b-2 border-b-border-secondary p-2">
<h3 class="m-0 text-center text-sm font-semibold text-secondary"> <h3 class="m-0 text-center text-sm font-semibold text-secondary">
{{ menuTitleMap[currentPicBedName] }} {{ menuTitleMap[currentPicBedName] }}
</h3> </h3>
</div> </div>
<div class="min-h-0 flex-1 overflow-y-auto p-2"> <div class="min-h-0 flex-1 overflow-y-auto">
<div v-if="isLoadingBucketList" class="flex flex-col items-center justify-center gap-2 p-8"> <div v-if="isLoadingBucketList" class="flex flex-col items-center justify-center gap-2 p-8">
<div <div
class="h-[25px] w-[25px] animate-spin rounded-full border-3 border-t-2 border-border border-t-accent" class="h-[25px] w-[25px] animate-spin rounded-full border-3 border-t-2 border-border border-t-accent"
@@ -287,7 +287,7 @@ const configMap = ref<any>(null)
const currentAlias = ref(route.query.alias as string) const currentAlias = ref(route.query.alias as string)
const currentPicBedName = ref(route.query.picBedName as string) const currentPicBedName = ref(route.query.picBedName as string)
const sidebarWidth = ref(160) const sidebarWidth = ref(120)
const isResizing = ref(false) const isResizing = ref(false)
let allPicBedConfigure = JSON.parse(route.query.allPicBedConfigure as string) let allPicBedConfigure = JSON.parse(route.query.allPicBedConfigure as string)

File diff suppressed because it is too large Load Diff

View File

@@ -242,12 +242,12 @@
</div> </div>
</div> </div>
<div class="flex shrink-0 flex-col justify-between"> <div class="flex min-w-0 shrink-0 flex-col justify-between">
<div <div
class="mb-1.5 overflow-hidden text-sm font-medium text-ellipsis whitespace-nowrap text-main" class="mb-1.5 w-full truncate text-center text-sm font-medium text-main"
:title="(item.fileName || '').toString().length > 30 ? item.fileName || '' : ''" :title="(item.fileName || '').toString().length > 30 ? item.fileName || '' : ''"
> >
<div class="text-center">{{ formatFileName(item.fileName || '') }}</div> {{ formatFileName(item.fileName || '') }}
</div> </div>
<div class="mr-2 flex items-center justify-between"> <div class="mr-2 flex items-center justify-between">

View File

@@ -5,7 +5,7 @@
:class="[osGlobal === 'linux' ? 'rounded-none bg-size-[100vh_100vw]' : 'rounded-full bg-size-[90vh_90vw]']" :class="[osGlobal === 'linux' ? 'rounded-none bg-size-[100vh_100vw]' : 'rounded-full bg-size-[90vh_90vw]']"
> >
<div <div
id="upload-area" ref="uploadArea"
class="h-full w-full transition-all duration-200 ease-in-out" class="h-full w-full transition-all duration-200 ease-in-out"
:class="{ :class="{
'bg-[rgba(0,0,0,0.3)]': dragover, 'bg-[rgba(0,0,0,0.3)]': dragover,
@@ -35,11 +35,12 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { IConfig } from 'piclist' import type { IConfig } from 'piclist'
import { onBeforeMount, onBeforeUnmount, ref, watch } from 'vue' import { onBeforeMount, onBeforeUnmount, ref, useTemplateRef, watch } from 'vue'
import { osGlobal } from '@/hooks/useGlobal' import { osGlobal } from '@/hooks/useGlobal'
import { isUrl } from '@/utils/common' import { isUrl } from '@/utils/common'
import { getConfig } from '@/utils/dataSender' import { getConfig } from '@/utils/dataSender'
import { useDragEventListeners } from '@/utils/drag'
import { IRPCActionType } from '@/utils/enum' import { IRPCActionType } from '@/utils/enum'
const logoPath = ref('') const logoPath = ref('')
@@ -51,6 +52,9 @@ const wX = ref(-1)
const wY = ref(-1) const wY = ref(-1)
const screenX = ref(-1) const screenX = ref(-1)
const screenY = ref(-1) const screenY = ref(-1)
const uploadArea = useTemplateRef<HTMLDivElement>('uploadArea')
useDragEventListeners(uploadArea)
let removeListeners: () => void = () => {} let removeListeners: () => void = () => {}

View File

@@ -135,7 +135,7 @@
small small
no-border no-border
:title="t('pages.settings.system.isHideDock')" :title="t('pages.settings.system.isHideDock')"
@change="handleHideDockChange(formOfSetting.isHideDock)" @change="handleHideDockChange"
/> />
</SettingCard> </SettingCard>
@@ -163,7 +163,7 @@
small small
no-border no-border
:title="t('pages.settings.system.miniWindowOnTop')" :title="t('pages.settings.system.miniWindowOnTop')"
@click="handleMiniWindowOntop(formOfSetting.miniWindowOntop)" @change="handleMiniWindowOntop"
/> />
</SettingCard> </SettingCard>
@@ -197,7 +197,7 @@
no-border no-border
:title="t('pages.settings.system.autoLaunch')" :title="t('pages.settings.system.autoLaunch')"
:description="t('pages.settings.system.autoLaunchDesc')" :description="t('pages.settings.system.autoLaunchDesc')"
@change="handleAutoStartChange(formOfSetting.autoStart)" @change="handleAutoStartChange"
/> />
</SettingCard> </SettingCard>
<CustomNavCard <CustomNavCard

View File

@@ -111,6 +111,7 @@
> >
<div <div
id="upload-area" id="upload-area"
ref="uploadArea"
class="group/upload relative flex h-full w-full cursor-pointer items-center justify-center rounded-xl border-2 border-dashed border-border bg-bg-secondary px-1 py-12 duration-medium ease-standard focus-visible:focus-ring focus-visible:outline-offset-4 max-md:px-4 max-md:py-8 max-xs:px-2 max-xs:py-6 [:hover,.drag-active]:border-accent [:hover,.drag-active]:bg-[linear-gradient(135deg,var(--color-surface-elevated)_0%,color-mix(in_srgb,var(--color-accent),transparent_95%)_100%)] [:hover,.drag-active]:shadow-lg [:hover,.drag-active&]:-translate-y-[2px]" class="group/upload relative flex h-full w-full cursor-pointer items-center justify-center rounded-xl border-2 border-dashed border-border bg-bg-secondary px-1 py-12 duration-medium ease-standard focus-visible:focus-ring focus-visible:outline-offset-4 max-md:px-4 max-md:py-8 max-xs:px-2 max-xs:py-6 [:hover,.drag-active]:border-accent [:hover,.drag-active]:bg-[linear-gradient(135deg,var(--color-surface-elevated)_0%,color-mix(in_srgb,var(--color-accent),transparent_95%)_100%)] [:hover,.drag-active]:shadow-lg [:hover,.drag-active&]:-translate-y-[2px]"
:class="{ 'drag-active': dragover }" :class="{ 'drag-active': dragover }"
@drop.prevent="onDrop" @drop.prevent="onDrop"
@@ -736,7 +737,8 @@ interface IUploadTaskQueueStatus {
} }
} }
useDragEventListeners() const uploadArea = useTemplateRef('uploadArea')
useDragEventListeners(uploadArea)
const $router = useRouter() const $router = useRouter()
const { t } = useI18n() const { t } = useI18n()
const message = useMessage() const message = useMessage()

View File

@@ -1,23 +1,26 @@
import { onBeforeUnmount, onMounted } from 'vue' import { onBeforeUnmount, onMounted, type Ref } from 'vue'
export function useDragEventListeners(dropZoneRef: Ref<HTMLElement | null>) {
function disableDrag(e: DragEvent) { function disableDrag(e: DragEvent) {
const dropzone = document.getElementById('upload-area') const dropzone = dropZoneRef.value
if (dropzone === null || !dropzone.contains(e.target as Node)) { if (!dropzone || !dropzone.contains(e.target as Node)) {
e.preventDefault() e.preventDefault()
e.dataTransfer!.effectAllowed = 'none' if (e.dataTransfer) {
e.dataTransfer!.dropEffect = 'none' e.dataTransfer.effectAllowed = 'none'
e.dataTransfer.dropEffect = 'none'
}
} }
} }
export function useDragEventListeners() {
onMounted(() => { onMounted(() => {
window.addEventListener('dragenter', disableDrag, false) window.addEventListener('dragenter', disableDrag, false)
window.addEventListener('dragover', disableDrag) window.addEventListener('dragover', disableDrag, false)
window.addEventListener('drop', disableDrag) window.addEventListener('drop', disableDrag, false)
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {
window.removeEventListener('dragenter', disableDrag, false) window.removeEventListener('dragenter', disableDrag, false)
window.removeEventListener('dragover', disableDrag) window.removeEventListener('dragover', disableDrag, false)
window.removeEventListener('drop', disableDrag) window.removeEventListener('drop', disableDrag, false)
}) })
} }

View File

@@ -42,7 +42,6 @@
| .amv | AMV视频文件 | .mpg | MPEG视频文件 | | .amv | AMV视频文件 | .mpg | MPEG视频文件 |
| .avi | AVI视频文件 | .mts | AVCHD视频文件 | | .avi | AVI视频文件 | .mts | AVCHD视频文件 |
| .flac | FLAC音频文件 | .ogg | Ogg Vorbis音频文件 | | .flac | FLAC音频文件 | .ogg | Ogg Vorbis音频文件 |
| .flv | Flash视频文件 | .ogv | Ogg Theora视频文件 |
| .m2ts | M2TS视频文件 | .vob | DVD视频文件 | | .m2ts | M2TS视频文件 | .vob | DVD视频文件 |
| .m4a | MPEG-4音频文件 | .wav | WAV音频文件 | | .m4a | MPEG-4音频文件 | .wav | WAV音频文件 |
| .m4v | MPEG-4视频文件 | .webm | WebM视频文件 | | .m4v | MPEG-4视频文件 | .webm | WebM视频文件 |