整合文件选择工具栏

This commit is contained in:
2026-06-05 14:54:58 +08:00
parent d13cd91bd0
commit 63ef49195a
6 changed files with 188 additions and 89 deletions
+14 -4
View File
@@ -2,10 +2,16 @@
<div
class="data-table-2026 theme-2026-card theme-2026-card-hover overflow-hidden rounded-[30px] shadow-md transition-all duration-300 hover:-translate-y-0.5 hover:shadow-lg"
>
<div class="px-6 py-4 border-b" :class="[isDarkMode ? 'border-gray-700/80' : 'border-[#d9e4e6]']">
<div
class="border-b px-6 py-4"
:class="[isDarkMode ? 'border-gray-700/80' : 'border-[#d9e4e6]']"
>
<h3 class="text-lg font-medium" :class="[isDarkMode ? 'text-white' : 'text-gray-800']">
{{ title }}
</h3>
<div v-if="slots.toolbar" class="mt-4">
<slot name="toolbar"></slot>
</div>
</div>
<div class="overflow-x-auto">
<table
@@ -14,9 +20,12 @@
>
<thead>
<tr>
<th v-for="header in headers" :key="header"
<th
v-for="header in headers"
:key="header"
class="px-6 py-3.5 text-left text-xs font-medium uppercase tracking-wider"
:class="[isDarkMode ? 'text-gray-400' : 'text-gray-500']">
:class="[isDarkMode ? 'text-gray-400' : 'text-gray-500']"
>
{{ header }}
</th>
</tr>
@@ -34,7 +43,7 @@
</template>
<script setup lang="ts">
import { inject } from 'vue'
import { inject, useSlots } from 'vue'
interface Props {
title: string
@@ -43,5 +52,6 @@ interface Props {
defineProps<Props>()
const slots = useSlots()
const isDarkMode = inject('isDarkMode')
</script>
+33 -4
View File
@@ -79,6 +79,7 @@ const isLegacyEndpointUnavailable = (error: unknown) => {
const legacyForeverExpiresAt = '2099-12-31T23:59'
const detailPolicyDownloadLimit = 5
const hourInMilliseconds = 60 * 60 * 1000
const dayInMilliseconds = 24 * 60 * 60 * 1000
type DetailPolicyActionOption = {
@@ -99,7 +100,9 @@ type HealthFilterOption = {
const emptyBatchEditForm = (): AdminBatchEditForm => ({
mode: 'expiresAt',
expired_at: '',
expired_count: null
expired_count: null,
relative_expire_value: null,
relative_expire_unit: 'hours'
})
const metadataTagSeparatorPattern = /[,\n]+/
@@ -269,6 +272,12 @@ export function useAdminFiles() {
)
const selectedCount = computed(() => selectedFileIds.value.size)
const hasSelectedFiles = computed(() => selectedCount.value > 0)
const selectedFilesOnCurrentPage = computed(() =>
tableData.value.filter((file) => selectedFileIds.value.has(file.id))
)
const selectedHasPermanentFiles = computed(() =>
selectedFilesOnCurrentPage.value.some((file) => file.isPermanentFile)
)
const isAllCurrentPageSelected = computed(
() => tableData.value.length > 0 && currentPageSelectedCount.value === tableData.value.length
)
@@ -429,7 +438,7 @@ export function useAdminFiles() {
)
const batchPolicyActionOptions = computed<BatchPolicyActionOption[]>(
() => createPolicyActionOptions(false)
() => createPolicyActionOptions(selectedHasPermanentFiles.value)
)
const inferIsText = (file: FileListItem) => {
@@ -571,6 +580,7 @@ export function useAdminFiles() {
displayHealthAction: t(`fileManage.insightActions.${statusInsightNextAction}`),
isTextFile,
isExpiredFile,
isPermanentFile,
isChunkedFile,
remainingDownloadsValue,
canPreviewText: isTextFile,
@@ -823,6 +833,23 @@ export function useAdminFiles() {
}
}
if (form.mode === 'relativeExpire') {
if (
form.relative_expire_value === null ||
!Number.isFinite(form.relative_expire_value) ||
form.relative_expire_value <= 0
) {
return null
}
const duration =
form.relative_expire_value *
(form.relative_expire_unit === 'days' ? dayInMilliseconds : hourInMilliseconds)
return {
ids,
expired_at: formatLocalDateTime(new Date(Date.now() + duration))
}
}
return {
ids,
clearExpiredAt: true,
@@ -1425,10 +1452,11 @@ export function useAdminFiles() {
const applySelectedPolicyAction = async (action: AdminFilePolicyAction) => {
if (!hasSelectedFiles.value || isBatchActionRunning.value) return
const selectedAction = batchPolicyActionOptions.value.find((option) => option.action === action)
if (selectedAction?.disabled) return
const ids = Array.from(selectedFileIds.value)
const actionLabel =
batchPolicyActionOptions.value.find((option) => option.action === action)?.label || action
const actionLabel = selectedAction?.label || action
if (
!window.confirm(
t('fileManage.batchPolicyActionConfirm', {
@@ -1690,6 +1718,7 @@ export function useAdminFiles() {
selectedFileDetail,
selectedCount,
selectedFileIds,
selectedHasPermanentFiles,
selectedViewPresetId,
storageUsedText,
summary,
+6
View File
@@ -485,6 +485,12 @@ export default {
batchEditSelected: 'Update the selected {count} files',
batchEditMode: 'Update Mode',
batchEditExpiresAt: 'Set Expiration Time',
batchEditRelativeExpire: 'Expire After Duration',
batchEditRelativeExpireValue: 'Expiration Duration',
batchEditRelativeExpirePlaceholder: 'Enter amount',
batchEditRelativeExpireUnit: 'Unit',
batchEditRelativeExpireHours: 'Hours',
batchEditRelativeExpireDays: 'Days',
batchEditDownloadLimit: 'Set Retrieval Limit',
batchEditForever: 'Make Permanent',
batchEditForeverHint: 'Clear the expiration time and make retrievals unlimited.',
+6
View File
@@ -483,6 +483,12 @@ export default {
batchEditSelected: '将更新选中的 {count} 个文件',
batchEditMode: '更新方式',
batchEditExpiresAt: '设置过期时间',
batchEditRelativeExpire: '设为一段时间后消失',
batchEditRelativeExpireValue: '消失时间',
batchEditRelativeExpirePlaceholder: '输入数量',
batchEditRelativeExpireUnit: '单位',
batchEditRelativeExpireHours: '小时',
batchEditRelativeExpireDays: '天',
batchEditDownloadLimit: '设置取件次数',
batchEditForever: '设为永久有效',
batchEditForeverHint: '清空过期时间,并将取件次数设为不限。',
+6 -1
View File
@@ -47,6 +47,7 @@ export interface AdminFileViewItem extends FileListItem {
displayHealthAction: string
isTextFile: boolean
isExpiredFile: boolean
isPermanentFile: boolean
isChunkedFile: boolean
remainingDownloadsValue: number | null
canPreviewText: boolean
@@ -393,12 +394,16 @@ export interface AdminBatchPolicyActionResponse extends AdminBatchUpdateFilesRes
action?: AdminFilePolicyAction | string
}
export type AdminBatchEditMode = 'expiresAt' | 'downloadLimit' | 'forever'
export type AdminBatchExpireUnit = 'hours' | 'days'
export type AdminBatchEditMode = 'expiresAt' | 'relativeExpire' | 'downloadLimit' | 'forever'
export interface AdminBatchEditForm {
mode: AdminBatchEditMode
expired_at: string
expired_count: number | null
relative_expire_value: number | null
relative_expire_unit: AdminBatchExpireUnit
}
export interface FileUploadResponse {
+123 -80
View File
@@ -261,85 +261,86 @@
</div>
</section>
<section
v-if="tableData.length > 0"
class="theme-2026-card theme-2026-card-hover mb-4 flex flex-col gap-3 rounded-[30px] border px-4 py-3 shadow-md transition-all duration-200 hover:-translate-y-0.5 hover:shadow-lg sm:flex-row sm:items-center sm:justify-between"
:class="[panelClass, cardHoverClass]"
>
<label class="inline-flex items-center gap-2 text-sm" :class="[primaryTextClass]">
<input
type="checkbox"
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
:checked="isAllCurrentPageSelected"
:disabled="isBatchActionRunning"
:indeterminate="isCurrentPagePartiallySelected"
@change="toggleCurrentPageSelection"
/>
<span>
{{
hasSelectedFiles
? t('fileManage.selectedCount', { count: selectedCount })
: t('fileManage.selectCurrentPage')
}}
</span>
</label>
<div class="flex flex-wrap items-center gap-2">
<BaseButton
v-if="hasSelectedFiles"
variant="outline"
size="sm"
:disabled="isBatchActionRunning"
@click="clearSelection"
>
<template #icon>
<XIcon class="mr-2 h-4 w-4" />
</template>
{{ t('fileManage.clearSelection') }}
</BaseButton>
<BaseButton
v-for="option in batchPolicyActionOptions"
:key="option.action"
variant="outline"
size="sm"
:title="option.description"
:disabled="!hasSelectedFiles || isBatchDeleting || isBatchUpdating"
:loading="isBatchPolicyActionRunning"
@click="applySelectedPolicyAction(option.action)"
>
<template #icon>
<component :is="getDetailPolicyActionIcon(option.action)" class="mr-2 h-4 w-4" />
</template>
{{ option.label }}
</BaseButton>
<BaseButton
variant="secondary"
size="sm"
:disabled="!hasSelectedFiles || isBatchDeleting || isBatchPolicyActionRunning"
:loading="isBatchUpdating"
@click="openBatchEditModal"
>
<template #icon>
<ClockIcon class="mr-2 h-4 w-4" />
</template>
{{ t('fileManage.batchEdit') }}
</BaseButton>
<BaseButton
variant="danger"
size="sm"
:disabled="!hasSelectedFiles || isBatchUpdating || isBatchPolicyActionRunning"
:loading="isBatchDeleting"
@click="deleteSelectedFiles"
>
<template #icon>
<TrashIcon class="mr-2 h-4 w-4" />
</template>
{{ t('fileManage.batchDelete') }}
</BaseButton>
</div>
</section>
<DataTable :title="t('fileManage.allFiles')" :headers="fileTableHeaders">
<template #toolbar>
<div
v-if="tableData.length > 0"
class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"
>
<label class="inline-flex items-center gap-2 text-sm" :class="[primaryTextClass]">
<input
type="checkbox"
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
:checked="isAllCurrentPageSelected"
:disabled="isBatchActionRunning"
:indeterminate="isCurrentPagePartiallySelected"
@change="toggleCurrentPageSelection"
/>
<span>
{{
hasSelectedFiles
? t('fileManage.selectedCount', { count: selectedCount })
: t('fileManage.selectCurrentPage')
}}
</span>
</label>
<div class="flex flex-wrap items-center gap-2">
<BaseButton
v-if="hasSelectedFiles"
variant="outline"
size="sm"
:disabled="isBatchActionRunning"
@click="clearSelection"
>
<template #icon>
<XIcon class="mr-2 h-4 w-4" />
</template>
{{ t('fileManage.clearSelection') }}
</BaseButton>
<BaseButton
v-for="option in batchPolicyActionOptions"
:key="option.action"
variant="outline"
size="sm"
:title="option.description"
:disabled="option.disabled || !hasSelectedFiles || isBatchDeleting || isBatchUpdating"
:loading="isBatchPolicyActionRunning"
@click="applySelectedPolicyAction(option.action)"
>
<template #icon>
<component :is="getDetailPolicyActionIcon(option.action)" class="mr-2 h-4 w-4" />
</template>
{{ option.label }}
</BaseButton>
<BaseButton
variant="secondary"
size="sm"
:disabled="!hasSelectedFiles || isBatchDeleting || isBatchPolicyActionRunning"
:loading="isBatchUpdating"
@click="openBatchEditModal"
>
<template #icon>
<ClockIcon class="mr-2 h-4 w-4" />
</template>
{{ t('fileManage.batchEdit') }}
</BaseButton>
<BaseButton
variant="danger"
size="sm"
:disabled="!hasSelectedFiles || isBatchUpdating || isBatchPolicyActionRunning"
:loading="isBatchDeleting"
@click="deleteSelectedFiles"
>
<template #icon>
<TrashIcon class="mr-2 h-4 w-4" />
</template>
{{ t('fileManage.batchDelete') }}
</BaseButton>
</div>
</div>
</template>
<template #body>
<tr v-if="isLoading">
<td :colspan="fileTableHeaders.length" class="px-6 py-12 text-center">
@@ -1218,12 +1219,12 @@
<p class="text-sm font-medium" :class="[primaryTextClass]">
{{ t('fileManage.batchEditMode') }}
</p>
<div class="grid gap-2 sm:grid-cols-3">
<div class="grid gap-2 sm:grid-cols-2 xl:grid-cols-4">
<button
v-for="option in batchEditModeOptions"
:key="option.value"
type="button"
class="flex min-h-12 items-center justify-center rounded-lg border px-3 py-2 text-sm font-medium transition-colors"
class="flex min-h-12 items-center justify-center rounded-[30px] border px-3 py-2 text-sm font-medium transition-colors"
:class="getBatchEditModeClass(option.value)"
:disabled="isBatchUpdating"
@click="batchEditForm.mode = option.value"
@@ -1240,6 +1241,42 @@
type="datetime-local"
:label="t('fileManage.batchEditExpiresAt')"
/>
<div v-else-if="batchEditForm.mode === 'relativeExpire'" class="grid gap-3 sm:grid-cols-[1fr_10rem]">
<FileEditField
v-model="batchEditForm.relative_expire_value"
type="number"
:label="t('fileManage.batchEditRelativeExpireValue')"
:placeholder="t('fileManage.batchEditRelativeExpirePlaceholder')"
/>
<div class="space-y-2 group">
<label
class="flex items-center space-x-2 text-sm font-medium transition-colors duration-200"
:class="[
isDarkMode
? 'text-gray-300 group-focus-within:text-indigo-400'
: 'text-gray-700 group-focus-within:text-indigo-600'
]"
>
<span>{{ t('fileManage.batchEditRelativeExpireUnit') }}</span>
<div
class="h-px flex-1 transition-colors duration-200"
:class="[
isDarkMode
? 'bg-gray-700 group-focus-within:bg-indigo-500/50'
: 'bg-gray-200 group-focus-within:bg-indigo-500/30'
]"
></div>
</label>
<select
v-model="batchEditForm.relative_expire_unit"
class="block min-h-12 w-full rounded-[30px] border px-4 py-2.5 text-sm font-medium transition-all duration-200 focus:outline-none focus:ring-2"
:class="fieldClass"
>
<option value="hours">{{ t('fileManage.batchEditRelativeExpireHours') }}</option>
<option value="days">{{ t('fileManage.batchEditRelativeExpireDays') }}</option>
</select>
</div>
</div>
<FileEditField
v-else-if="batchEditForm.mode === 'downloadLimit'"
v-model="batchEditForm.expired_count"
@@ -1377,6 +1414,7 @@ import {
RefreshCwIcon,
RotateCcwIcon,
SearchIcon,
TimerResetIcon,
TrashIcon,
XIcon
} from 'lucide-vue-next'
@@ -1815,6 +1853,11 @@ const batchEditModeOptions = computed<
label: t('fileManage.batchEditExpiresAt'),
icon: ClockIcon
},
{
value: 'relativeExpire',
label: t('fileManage.batchEditRelativeExpire'),
icon: TimerResetIcon
},
{
value: 'downloadLimit',
label: t('fileManage.batchEditDownloadLimit'),