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
+2
View File
@@ -25,6 +25,7 @@
- 优化相册页面卡片样式,边界更清晰,提升选择框视觉效果。
- 优化多个页面在窄屏下的显示,避免内容溢出。
- 文件浏览侧边栏名称超出宽度时支持滚动显示完整名称。
- 优化了视频播放页面的显示效果。
- 相册页面支持显示已选数量、匹配的 URL 列表和记忆过滤器状态。
- 支持浏览完整插件列表、查看详情及一键安装。
- 新增新手引导页面,首次运行自动弹出。
@@ -38,6 +39,7 @@
- 修复了管理页面中排序下拉框显示异常的问题。
- 修复了管理页面图床列表未正确高亮当前选中项的问题。
- 修复了管理页面markdown预览内容没有正确渲染的问题。
- 修复了暗色模式下任务页面的显示异常。
- 修复了图床设置页面“设置为默认图床”按钮状态更新不及时的问题。
- 修复了预处理设置页面中,图床水印独立设置按钮状态不同步的问题。
+1
View File
@@ -29,3 +29,4 @@
@utility no-drag-region {
-webkit-app-region: none;
}
@@ -175,7 +175,6 @@ function validateForm(): boolean {
}
function clearFieldError(fieldName: string) {
console.log('Clearing error for field:', fieldName)
if (validationErrors[fieldName]) {
delete validationErrors[fieldName]
}
@@ -6,16 +6,14 @@
<span v-if="required" class="ml-1 text-danger">*</span>
</label>
<slot name="title-extra"></slot>
<div v-if="tips" class="relative">
<div v-if="tips" class="group relative inline-block">
<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"
@click="toggleTooltip()"
>
<Info :size="16" />
</div>
<div
v-show="visibleTooltips"
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]"
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"
v-html="transformMarkdownToHTML(tips)"
/>
</div>
@@ -62,7 +60,6 @@ const [modelValue, modifiers] = defineModel<any>({
})
const type = ref('text')
const visibleTooltips = ref(false)
const {
isPassword = false,
@@ -80,10 +77,6 @@ const {
tips?: string
}>()
function toggleTooltip() {
visibleTooltips.value = !visibleTooltips.value
}
function transformMarkdownToHTML(markdown: string) {
try {
return marked.parse(markdown)
@@ -29,16 +29,14 @@
</div>
</label>
<slot name="title-extra"></slot>
<div v-if="tips" class="relative">
<div v-if="tips" class="group relative inline-block">
<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"
@click="toggleTooltip()"
>
<Info :size="16" />
</div>
<div
v-show="visibleTooltips"
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]"
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"
v-html="transformMarkdownToHTML(tips)"
/>
</div>
@@ -48,12 +46,10 @@
<script lang="ts" setup>
import { Info } from 'lucide-vue-next'
import { marked } from 'marked'
import { onMounted, ref } from 'vue'
import { onMounted } from 'vue'
const emit = defineEmits(['change'])
const visibleTooltips = ref(false)
const modelValue = defineModel<boolean>()
const {
title = '',
@@ -75,10 +71,6 @@ const {
tighter?: boolean
}>()
function toggleTooltip() {
visibleTooltips.value = !visibleTooltips.value
}
function transformMarkdownToHTML(markdown: string) {
try {
return marked.parse(markdown)
+1 -1
View File
@@ -8,7 +8,7 @@
}
html {
@apply m-0 h-full p-0 w-full bg-transparent;
@apply m-0 h-full w-full bg-transparent p-0;
}
#app {
+304 -358
View File
@@ -3,14 +3,13 @@
<div
ref="bucketContainerRef"
class="relative flex h-full w-full items-center justify-center"
:class="{ 'content-fullscreen': isContentFullscreen }"
@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 -->
<div
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">
<!-- Custom Domain Input/Select -->
@@ -130,7 +129,7 @@
<!-- Breadcrumb Card -->
<div
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">
<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">
<ChevronRightIcon v-if="index !== 0" class="h-[16px] w-[15px] shrink-0 text-accent" />
<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))"
>
{{ item === '' ? t('pages.manage.bucket.rootFolder') : item }}
@@ -158,7 +157,7 @@
<!-- Control Panel Card -->
<div
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" />
@@ -187,14 +186,25 @@
</template>
<!-- Sort Dropdown -->
<div class="dropdown">
<button class="dropdown-button" @click="sortDropdownOpen = !sortDropdownOpen">
<div class="relative">
<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" />
{{ t(`pages.manage.bucket.sort.${currentSortType}`) }}
<ChevronDownIcon class="action-icon" />
</button>
<div v-if="sortDropdownOpen" class="dropdown-content">
<div v-for="item in sortTypeList" :key="item" class="dropdown-item" @click="sortFile(item as any)">
<div
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}`) }}
</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"
>
<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" />
<template v-if="configMap.prefix !== '/'">
<template v-for="(item, index) in configMap.prefix.replace(/\/$/g, '').split('/')" :key="index">
@@ -258,7 +268,7 @@
</div>
</div>
<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 -->
<input
v-model="searchText"
@@ -282,7 +292,7 @@
</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">
<EmptyPage />
@@ -366,13 +376,13 @@
</template>
</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
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"
@click.stop="copyToClipboard(item.fileName ?? '')"
>
<div class="text-center">{{ item.fileName ?? '' }}</div>
{{ item.fileName ?? '' }}
</div>
<div
v-if="!item.isDir"
@@ -512,7 +522,7 @@
@click="copyToClipboard(JSON.stringify(currentShowedFileInfo, null, 2))"
/>
</div>
<div class="modal-content">
<div class="flex-1 overflow-y-auto bg-bg p-6">
<div
v-for="(value, key) in currentShowedFileInfo"
:key="key"
@@ -548,9 +558,12 @@
<div class="mb-6 last:mb-0">
<label class="mb-2 flex items-center gap-2 text-sm font-medium text-main">
{{ t('pages.manage.bucket.matchedPattern', { num: matchedFilesNumber.length }) }}
<div class="tooltip">
<div class="group relative inline-block">
<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>
</label>
<input
@@ -663,55 +676,55 @@
</div>
</div>
<!-- Upload Drawer -->
<div
<CustomModal
v-if="isShowUploadPanel"
class="drawer-overlay"
:class="{ open: isShowUploadPanel }"
@click="isShowUploadPanel = false"
v-model:visible="isShowUploadPanel"
:title="t('pages.manage.bucket.uploadFile')"
width="900px"
height="90vh"
>
<div class="drawer-container" @click.stop>
<div class="drawer-header">
<h3 class="drawer-title">
{{ t('pages.manage.bucket.uploadFile') }}
</h3>
<div class="switch-container">
<label class="switch">
<input v-model="isUploadKeepDirStructure" type="checkbox" @change="handleUploadKeepDirChange" />
<span class="switch-slider" />
</label>
<span class="switch-label">
{{
<div class="flex h-full w-full flex-col gap-2">
<div class="flex justify-end">
<CustomSwitch
v-model="isUploadKeepDirStructure"
:title="
isUploadKeepDirStructure
? t('pages.manage.bucket.keepDirStructure')
: t('pages.manage.bucket.noKeepDirStructure')
}}
</span>
</div>
<button class="modal-close" @click="isShowUploadPanel = false">
<XIcon class="action-icon" />
</button>
"
small
no-border
@change="handleUploadKeepDirChange"
/>
</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
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 }"
@drop.prevent="onDrop"
@dragover.prevent="isDragover = true"
@dragleave.prevent="isDragover = false"
@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') }}
</div>
<div class="upload-area-subtext">
<div class="text-sm font-medium text-secondary">
{{ t('pages.manage.bucket.clickUpload') }}
</div>
</div>
</div>
<!-- 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
:items="
tableData.sort((a, b) =>
@@ -719,23 +732,32 @@
)
"
:item-height="90"
:style="{ height: '100%' }"
class="min-h-0 w-full flex-1 p-3"
view-mode="list"
>
<template #default="{ item }">
<div class="file-list-item">
<div class="file-list-icon">
<FolderIcon v-if="item.isFolder" class="file-icon" />
<FileIcon v-else class="file-icon" />
<div
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"
>
<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 class="file-list-info">
<div class="file-list-name">
{{ formatFileName(item.name) }}
<div class="min-w-0 flex-1 flex-col">
<div class="flex flex-row justify-between gap-3">
<div
class="mb-1 cursor-pointer overflow-hidden text-sm font-semibold text-ellipsis whitespace-nowrap text-secondary"
>
{{ item.name }}
</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 }}
</div>
<div class="file-list-meta">
</div>
<div class="flex text-xs text-secondary">
<span>{{ formatFileSize(item.fileSize) }}</span>
<span v-if="item.isFolder"> {{ item.filesList.length }} files </span>
</div>
@@ -746,162 +768,129 @@
</div>
<!-- Upload Actions -->
<div v-if="tableData.length" style="display: flex; justify-content: center; gap: 1rem; margin-top: 1rem">
<button class="action-button primary" :disabled="isLoadingUploadPanelFiles" @click="uploadFiles">
<UploadIcon class="action-icon" />
{{ isLoadingUploadPanelFiles ? t('pages.manage.bucket.readingDir') : t('pages.manage.bucket.upload') }}
</button>
<button class="action-button secondary" :disabled="isLoadingUploadPanelFiles" @click="clearTableData">
<Trash2Icon class="action-icon" />
{{ t('pages.manage.bucket.clear') }}
</button>
<div v-if="tableData.length" class="mt-4 flex justify-center gap-4">
<CustomButton
type="primary"
:disabled="isLoadingUploadPanelFiles"
:text="
isLoadingUploadPanelFiles ? t('pages.manage.bucket.readingDir') : t('pages.manage.bucket.upload')
"
:icon="UploadIcon"
@click="uploadFiles"
/>
<CustomButton
type="secondary"
:icon="Trash2Icon"
:text="t('pages.manage.bucket.clear')"
:disabled="isLoadingUploadPanelFiles"
@click="clearTableData"
/>
</div>
<!-- Upload Tasks Tabs -->
<div class="tabs-container">
<div class="tabs-header">
<div class="flex flex-1 flex-col gap-2 overflow-hidden border-t border-border-secondary">
<div class="flex shrink-0 border-b border-b-border">
<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' }"
@click="activeUpLoadTab = '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 }}
</span>
</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' }"
@click="activeUpLoadTab = 'finished'"
>
{{ 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 }}
</span>
</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' }"
@click="activeUpLoadTab = '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 }}
</span>
</button>
</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 -->
<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
:items="uploadingTaskList"
:items="
activeUpLoadTab === 'uploading'
? uploadingTaskList
: activeUpLoadTab === 'finished'
? uploadedTaskList.filter(item => item.status === 'uploaded')
: uploadedTaskList.filter(item => item.status !== 'uploaded')
"
:item-height="70"
:style="{ height: '100%' }"
class="min-h-0 w-full flex-1 p-3"
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="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"
<div
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"
>
<template #default="{ item }">
<div class="file-list-item">
<div class="file-list-info">
<div class="file-list-name">
{{ formatFileName(item.sourceFileName) }}
<div class="flex flex-1 flex-col gap-1">
<div class="overflow-hidden text-sm font-medium text-ellipsis whitespace-nowrap text-secondary">
{{ item.sourceFileName }}
</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 class="badge success">
{{ t('pages.manage.bucket.success') }}
</span>
</div>
</div>
</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 class="text-xs font-semibold text-success">
{{
activeUpLoadTab === 'finished'
? t('pages.manage.bucket.success')
: t('pages.manage.bucket.failed')
}}
</span>
</div>
</div>
@@ -913,184 +902,128 @@
</div>
</div>
</div>
</div>
</CustomModal>
<!-- Download Drawer -->
<div
<CustomModal
v-if="isShowDownloadPanel"
class="drawer-overlay"
:class="{ open: isShowDownloadPanel }"
@click="isShowDownloadPanel = false"
v-model:visible="isShowDownloadPanel"
:title="t('pages.manage.bucket.downloadPage')"
width="900px"
height="90vh"
>
<div class="drawer-container" @click.stop>
<div class="drawer-header">
<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">
<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">
<!-- Download Tasks Tabs -->
<div class="tabs-container">
<div class="tabs-header">
<div class="flex flex-1 flex-col gap-2 overflow-hidden border-t border-border-secondary">
<div class="flex shrink-0 border-b border-b-border">
<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' }"
@click="activeDownLoadTab = '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 }}
</span>
</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' }"
@click="activeDownLoadTab = 'finished'"
>
{{ 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 }}
</span>
</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' }"
@click="activeDownLoadTab = '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 }}
</span>
</button>
</div>
<!-- Download Tabs Content -->
<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.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 -->
<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
:items="downloadingTaskList"
:items="
activeDownLoadTab === 'downloading'
? downloadingTaskList
: activeDownLoadTab === 'finished'
? downloadedTaskList.filter(item => item.status === 'downloaded')
: downloadedTaskList.filter(item => item.status !== 'downloaded')
"
:item-height="70"
:style="{ height: '100%' }"
class="min-h-0 w-full flex-1 p-3"
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="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"
<div
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"
>
<template #default="{ item }">
<div class="file-list-item">
<div class="file-list-info">
<div class="file-list-name">
{{ formatFileName(item.sourceFileName) }}
<div class="flex flex-1 flex-col gap-1">
<div class="overflow-hidden text-sm font-medium text-ellipsis whitespace-nowrap text-secondary">
{{ item.sourceFileName }}
</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 class="badge success">
{{ t('pages.manage.bucket.success') }}
</span>
</div>
</div>
</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 class="text-xs font-semibold text-success">
{{
activeUpLoadTab === 'finished'
? t('pages.manage.bucket.success')
: t('pages.manage.bucket.failed')
}}
</span>
</div>
</div>
@@ -1101,57 +1034,68 @@
</div>
</div>
</div>
</div>
</div>
</CustomModal>
<!-- Markdown Preview Dialog -->
<div v-if="isShowMarkDownDialog" class="modal-overlay" @click="isShowMarkDownDialog = false">
<div class="modal-container" style="width: 90vw; height: 90vh" @click.stop>
<div class="modal-header">
<h3 class="modal-title">
{{ t('pages.manage.bucket.preview') }}
</h3>
<button class="modal-close" @click="isShowMarkDownDialog = false">
<XIcon class="action-icon" />
</button>
</div>
<div class="modal-content" style="user-select: text" v-html="markDownContent" />
</div>
<CustomModal
v-if="isShowMarkDownDialog"
v-model:visible="isShowMarkDownDialog"
width="80vw"
height="80vh"
:title="t('pages.manage.bucket.preview')"
>
<div class="flex h-full w-full">
<div class="notes-body" style="user-select: text" v-html="markDownContent" />
</div>
</CustomModal>
<!-- Text File Preview Dialog -->
<div v-if="isShowTextFileDialog" class="modal-overlay" @click="isShowTextFileDialog = false">
<div class="modal-container" style="width: 90vw; height: 90vh" @click.stop>
<div class="modal-header">
<h3 class="modal-title">
{{ t('pages.manage.bucket.preview') }}
</h3>
<button class="modal-close" @click="isShowTextFileDialog = false">
<XIcon class="action-icon" />
</button>
</div>
<div class="modal-content">
<pre style="font-family: monospace; white-space: pre-wrap; user-select: text">{{ textfileContent }}</pre>
</div>
</div>
<CustomModal
v-if="isShowTextFileDialog"
v-model:visible="isShowTextFileDialog"
width="80vw"
height="80vh"
:title="t('pages.manage.bucket.preview')"
>
<div class="flex h-full w-full">
<pre class="overflow-auto font-['SF_Mono',Monaco,Menlo,'Ubuntu_Mono',monospace] text-base text-main">{{
textfileContent
}}</pre>
</div>
</CustomModal>
<!-- Video Player Dialog -->
<div v-if="isShowVideoFileDialog" class="modal-overlay" @click="isShowVideoFileDialog = false">
<div class="modal-container" style="width: 90vw; height: 90vh" @click.stop>
<div class="modal-header">
<h3 class="modal-title">
{{ t('pages.manage.bucket.play') }}
</h3>
<button class="modal-close" @click="isShowVideoFileDialog = false">
<XIcon class="action-icon" />
</button>
</div>
<div class="modal-content">
<video :src="videoFileUrl" controls loop autoplay style="width: 100%; height: auto; max-height: 70vh" />
</div>
</div>
<CustomModal
v-if="isShowVideoFileDialog"
v-model:visible="isShowVideoFileDialog"
width="90vw"
height="90vh"
:title="t('pages.manage.bucket.play')"
>
<div class="flex h-full w-full items-center justify-center bg-black">
<video-player
class="video-player"
:src="videoFileUrl"
:volume="0.6"
:options="{
autoplay: true,
muted: false,
responsive: true,
fill: true,
fluid: false,
controlBar: {
volumePanel: {
inline: false,
},
},
}"
crossorigin="anonymous"
controls
playsinline
loop
/>
</div>
</CustomModal>
<!-- Create Folder Dialog -->
<CustomModal
@@ -1234,7 +1178,6 @@ import { useDownloadFileTransferStore, useFileTransferStore, useManageStore } fr
import {
customStrMatch,
customStrReplace,
formatFileName,
formatFileSize,
formatLink,
getFileIconPath,
@@ -1245,6 +1188,7 @@ import { getConfig, saveConfig } from '@/manage/utils/dataSender'
import { textFileExt } from '@/manage/utils/textfile'
import { videoExt } from '@/manage/utils/videofile'
import { trimPath } from '@/utils/common'
import { useDragEventListeners } from '@/utils/drag'
import { IRPCActionType } from '@/utils/enum'
import { cancelDownloadLoadingFileList, refreshDownloadFileTransferList } from '@/utils/static'
/*
@@ -1265,6 +1209,8 @@ const props = defineProps<{
type ISortTypeList = 'name' | 'size' | 'time' | 'ext' | 'check' | 'init'
const uploadDialog = useTemplateRef<HTMLDivElement>('uploadDialog')
useDragEventListeners(uploadDialog)
let fileTransferInterval: NodeJS.Timeout | undefined
let downloadInterval: NodeJS.Timeout | undefined
let scrollTimeout: ReturnType<typeof setTimeout> | undefined
@@ -1536,8 +1482,8 @@ function getList() {
})
}
function handleUploadKeepDirChange() {
saveConfig('settings.isUploadKeepDirStructure', isUploadKeepDirStructure.value)
function handleUploadKeepDirChange(value: boolean) {
saveConfig('settings.isUploadKeepDirStructure', value)
manageStore.refreshConfig()
}
@@ -1896,7 +1842,7 @@ async function handleClickFile(item: any) {
const fileUrl = item.url
const res = await fetch(fileUrl, options)
const content = await res.text()
markDownContent.value = await marked.parse(content)
markDownContent.value = await marked(content, { breaks: true, gfm: true })
isShowMarkDownDialog.value = true
} catch (_error) {
message.error(t('pages.manage.bucket.loadingFailed'))
+4 -4
View File
@@ -40,16 +40,16 @@
>
<div class="flex h-full w-full">
<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' }"
>
<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">
{{ menuTitleMap[currentPicBedName] }}
</h3>
</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
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 currentPicBedName = ref(route.query.picBedName as string)
const sidebarWidth = ref(160)
const sidebarWidth = ref(120)
const isResizing = ref(false)
let allPicBedConfigure = JSON.parse(route.query.allPicBedConfigure as string)
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -242,12 +242,12 @@
</div>
</div>
<div class="flex shrink-0 flex-col justify-between">
<div class="flex min-w-0 shrink-0 flex-col justify-between">
<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 || '' : ''"
>
<div class="text-center">{{ formatFileName(item.fileName || '') }}</div>
{{ formatFileName(item.fileName || '') }}
</div>
<div class="mr-2 flex items-center justify-between">
+6 -2
View File
@@ -5,7 +5,7 @@
:class="[osGlobal === 'linux' ? 'rounded-none bg-size-[100vh_100vw]' : 'rounded-full bg-size-[90vh_90vw]']"
>
<div
id="upload-area"
ref="uploadArea"
class="h-full w-full transition-all duration-200 ease-in-out"
:class="{
'bg-[rgba(0,0,0,0.3)]': dragover,
@@ -35,11 +35,12 @@
<script lang="ts" setup>
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 { isUrl } from '@/utils/common'
import { getConfig } from '@/utils/dataSender'
import { useDragEventListeners } from '@/utils/drag'
import { IRPCActionType } from '@/utils/enum'
const logoPath = ref('')
@@ -51,6 +52,9 @@ const wX = ref(-1)
const wY = ref(-1)
const screenX = ref(-1)
const screenY = ref(-1)
const uploadArea = useTemplateRef<HTMLDivElement>('uploadArea')
useDragEventListeners(uploadArea)
let removeListeners: () => void = () => {}
+3 -3
View File
@@ -135,7 +135,7 @@
small
no-border
:title="t('pages.settings.system.isHideDock')"
@change="handleHideDockChange(formOfSetting.isHideDock)"
@change="handleHideDockChange"
/>
</SettingCard>
@@ -163,7 +163,7 @@
small
no-border
:title="t('pages.settings.system.miniWindowOnTop')"
@click="handleMiniWindowOntop(formOfSetting.miniWindowOntop)"
@change="handleMiniWindowOntop"
/>
</SettingCard>
@@ -197,7 +197,7 @@
no-border
:title="t('pages.settings.system.autoLaunch')"
:description="t('pages.settings.system.autoLaunchDesc')"
@change="handleAutoStartChange(formOfSetting.autoStart)"
@change="handleAutoStartChange"
/>
</SettingCard>
<CustomNavCard
+3 -1
View File
@@ -111,6 +111,7 @@
>
<div
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="{ 'drag-active': dragover }"
@drop.prevent="onDrop"
@@ -736,7 +737,8 @@ interface IUploadTaskQueueStatus {
}
}
useDragEventListeners()
const uploadArea = useTemplateRef('uploadArea')
useDragEventListeners(uploadArea)
const $router = useRouter()
const { t } = useI18n()
const message = useMessage()
+13 -10
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) {
const dropzone = document.getElementById('upload-area')
if (dropzone === null || !dropzone.contains(e.target as Node)) {
const dropzone = dropZoneRef.value
if (!dropzone || !dropzone.contains(e.target as Node)) {
e.preventDefault()
e.dataTransfer!.effectAllowed = 'none'
e.dataTransfer!.dropEffect = 'none'
if (e.dataTransfer) {
e.dataTransfer.effectAllowed = 'none'
e.dataTransfer.dropEffect = 'none'
}
}
}
export function useDragEventListeners() {
onMounted(() => {
window.addEventListener('dragenter', disableDrag, false)
window.addEventListener('dragover', disableDrag)
window.addEventListener('drop', disableDrag)
window.addEventListener('dragover', disableDrag, false)
window.addEventListener('drop', disableDrag, false)
})
onBeforeUnmount(() => {
window.removeEventListener('dragenter', disableDrag, false)
window.removeEventListener('dragover', disableDrag)
window.removeEventListener('drop', disableDrag)
window.removeEventListener('dragover', disableDrag, false)
window.removeEventListener('drop', disableDrag, false)
})
}
-1
View File
@@ -42,7 +42,6 @@
| .amv | AMV视频文件 | .mpg | MPEG视频文件 |
| .avi | AVI视频文件 | .mts | AVCHD视频文件 |
| .flac | FLAC音频文件 | .ogg | Ogg Vorbis音频文件 |
| .flv | Flash视频文件 | .ogv | Ogg Theora视频文件 |
| .m2ts | M2TS视频文件 | .vob | DVD视频文件 |
| .m4a | MPEG-4音频文件 | .wav | WAV音频文件 |
| .m4v | MPEG-4视频文件 | .webm | WebM视频文件 |