整合文件选择工具栏

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 <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" 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']"> <h3 class="text-lg font-medium" :class="[isDarkMode ? 'text-white' : 'text-gray-800']">
{{ title }} {{ title }}
</h3> </h3>
<div v-if="slots.toolbar" class="mt-4">
<slot name="toolbar"></slot>
</div>
</div> </div>
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table <table
@@ -14,9 +20,12 @@
> >
<thead> <thead>
<tr> <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="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 }} {{ header }}
</th> </th>
</tr> </tr>
@@ -34,7 +43,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { inject } from 'vue' import { inject, useSlots } from 'vue'
interface Props { interface Props {
title: string title: string
@@ -43,5 +52,6 @@ interface Props {
defineProps<Props>() defineProps<Props>()
const slots = useSlots()
const isDarkMode = inject('isDarkMode') const isDarkMode = inject('isDarkMode')
</script> </script>
+33 -4
View File
@@ -79,6 +79,7 @@ const isLegacyEndpointUnavailable = (error: unknown) => {
const legacyForeverExpiresAt = '2099-12-31T23:59' const legacyForeverExpiresAt = '2099-12-31T23:59'
const detailPolicyDownloadLimit = 5 const detailPolicyDownloadLimit = 5
const hourInMilliseconds = 60 * 60 * 1000
const dayInMilliseconds = 24 * 60 * 60 * 1000 const dayInMilliseconds = 24 * 60 * 60 * 1000
type DetailPolicyActionOption = { type DetailPolicyActionOption = {
@@ -99,7 +100,9 @@ type HealthFilterOption = {
const emptyBatchEditForm = (): AdminBatchEditForm => ({ const emptyBatchEditForm = (): AdminBatchEditForm => ({
mode: 'expiresAt', mode: 'expiresAt',
expired_at: '', expired_at: '',
expired_count: null expired_count: null,
relative_expire_value: null,
relative_expire_unit: 'hours'
}) })
const metadataTagSeparatorPattern = /[,\n]+/ const metadataTagSeparatorPattern = /[,\n]+/
@@ -269,6 +272,12 @@ export function useAdminFiles() {
) )
const selectedCount = computed(() => selectedFileIds.value.size) const selectedCount = computed(() => selectedFileIds.value.size)
const hasSelectedFiles = computed(() => selectedCount.value > 0) 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( const isAllCurrentPageSelected = computed(
() => tableData.value.length > 0 && currentPageSelectedCount.value === tableData.value.length () => tableData.value.length > 0 && currentPageSelectedCount.value === tableData.value.length
) )
@@ -429,7 +438,7 @@ export function useAdminFiles() {
) )
const batchPolicyActionOptions = computed<BatchPolicyActionOption[]>( const batchPolicyActionOptions = computed<BatchPolicyActionOption[]>(
() => createPolicyActionOptions(false) () => createPolicyActionOptions(selectedHasPermanentFiles.value)
) )
const inferIsText = (file: FileListItem) => { const inferIsText = (file: FileListItem) => {
@@ -571,6 +580,7 @@ export function useAdminFiles() {
displayHealthAction: t(`fileManage.insightActions.${statusInsightNextAction}`), displayHealthAction: t(`fileManage.insightActions.${statusInsightNextAction}`),
isTextFile, isTextFile,
isExpiredFile, isExpiredFile,
isPermanentFile,
isChunkedFile, isChunkedFile,
remainingDownloadsValue, remainingDownloadsValue,
canPreviewText: isTextFile, 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 { return {
ids, ids,
clearExpiredAt: true, clearExpiredAt: true,
@@ -1425,10 +1452,11 @@ export function useAdminFiles() {
const applySelectedPolicyAction = async (action: AdminFilePolicyAction) => { const applySelectedPolicyAction = async (action: AdminFilePolicyAction) => {
if (!hasSelectedFiles.value || isBatchActionRunning.value) return 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 ids = Array.from(selectedFileIds.value)
const actionLabel = const actionLabel = selectedAction?.label || action
batchPolicyActionOptions.value.find((option) => option.action === action)?.label || action
if ( if (
!window.confirm( !window.confirm(
t('fileManage.batchPolicyActionConfirm', { t('fileManage.batchPolicyActionConfirm', {
@@ -1690,6 +1718,7 @@ export function useAdminFiles() {
selectedFileDetail, selectedFileDetail,
selectedCount, selectedCount,
selectedFileIds, selectedFileIds,
selectedHasPermanentFiles,
selectedViewPresetId, selectedViewPresetId,
storageUsedText, storageUsedText,
summary, summary,
+6
View File
@@ -485,6 +485,12 @@ export default {
batchEditSelected: 'Update the selected {count} files', batchEditSelected: 'Update the selected {count} files',
batchEditMode: 'Update Mode', batchEditMode: 'Update Mode',
batchEditExpiresAt: 'Set Expiration Time', batchEditExpiresAt: 'Set Expiration Time',
batchEditRelativeExpire: 'Expire After Duration',
batchEditRelativeExpireValue: 'Expiration Duration',
batchEditRelativeExpirePlaceholder: 'Enter amount',
batchEditRelativeExpireUnit: 'Unit',
batchEditRelativeExpireHours: 'Hours',
batchEditRelativeExpireDays: 'Days',
batchEditDownloadLimit: 'Set Retrieval Limit', batchEditDownloadLimit: 'Set Retrieval Limit',
batchEditForever: 'Make Permanent', batchEditForever: 'Make Permanent',
batchEditForeverHint: 'Clear the expiration time and make retrievals unlimited.', batchEditForeverHint: 'Clear the expiration time and make retrievals unlimited.',
+6
View File
@@ -483,6 +483,12 @@ export default {
batchEditSelected: '将更新选中的 {count} 个文件', batchEditSelected: '将更新选中的 {count} 个文件',
batchEditMode: '更新方式', batchEditMode: '更新方式',
batchEditExpiresAt: '设置过期时间', batchEditExpiresAt: '设置过期时间',
batchEditRelativeExpire: '设为一段时间后消失',
batchEditRelativeExpireValue: '消失时间',
batchEditRelativeExpirePlaceholder: '输入数量',
batchEditRelativeExpireUnit: '单位',
batchEditRelativeExpireHours: '小时',
batchEditRelativeExpireDays: '天',
batchEditDownloadLimit: '设置取件次数', batchEditDownloadLimit: '设置取件次数',
batchEditForever: '设为永久有效', batchEditForever: '设为永久有效',
batchEditForeverHint: '清空过期时间,并将取件次数设为不限。', batchEditForeverHint: '清空过期时间,并将取件次数设为不限。',
+6 -1
View File
@@ -47,6 +47,7 @@ export interface AdminFileViewItem extends FileListItem {
displayHealthAction: string displayHealthAction: string
isTextFile: boolean isTextFile: boolean
isExpiredFile: boolean isExpiredFile: boolean
isPermanentFile: boolean
isChunkedFile: boolean isChunkedFile: boolean
remainingDownloadsValue: number | null remainingDownloadsValue: number | null
canPreviewText: boolean canPreviewText: boolean
@@ -393,12 +394,16 @@ export interface AdminBatchPolicyActionResponse extends AdminBatchUpdateFilesRes
action?: AdminFilePolicyAction | string action?: AdminFilePolicyAction | string
} }
export type AdminBatchEditMode = 'expiresAt' | 'downloadLimit' | 'forever' export type AdminBatchExpireUnit = 'hours' | 'days'
export type AdminBatchEditMode = 'expiresAt' | 'relativeExpire' | 'downloadLimit' | 'forever'
export interface AdminBatchEditForm { export interface AdminBatchEditForm {
mode: AdminBatchEditMode mode: AdminBatchEditMode
expired_at: string expired_at: string
expired_count: number | null expired_count: number | null
relative_expire_value: number | null
relative_expire_unit: AdminBatchExpireUnit
} }
export interface FileUploadResponse { export interface FileUploadResponse {
+51 -8
View File
@@ -261,10 +261,11 @@
</div> </div>
</section> </section>
<section <DataTable :title="t('fileManage.allFiles')" :headers="fileTableHeaders">
<template #toolbar>
<div
v-if="tableData.length > 0" 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="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"
:class="[panelClass, cardHoverClass]"
> >
<label class="inline-flex items-center gap-2 text-sm" :class="[primaryTextClass]"> <label class="inline-flex items-center gap-2 text-sm" :class="[primaryTextClass]">
<input <input
@@ -303,7 +304,7 @@
variant="outline" variant="outline"
size="sm" size="sm"
:title="option.description" :title="option.description"
:disabled="!hasSelectedFiles || isBatchDeleting || isBatchUpdating" :disabled="option.disabled || !hasSelectedFiles || isBatchDeleting || isBatchUpdating"
:loading="isBatchPolicyActionRunning" :loading="isBatchPolicyActionRunning"
@click="applySelectedPolicyAction(option.action)" @click="applySelectedPolicyAction(option.action)"
> >
@@ -337,9 +338,9 @@
{{ t('fileManage.batchDelete') }} {{ t('fileManage.batchDelete') }}
</BaseButton> </BaseButton>
</div> </div>
</section> </div>
</template>
<DataTable :title="t('fileManage.allFiles')" :headers="fileTableHeaders">
<template #body> <template #body>
<tr v-if="isLoading"> <tr v-if="isLoading">
<td :colspan="fileTableHeaders.length" class="px-6 py-12 text-center"> <td :colspan="fileTableHeaders.length" class="px-6 py-12 text-center">
@@ -1218,12 +1219,12 @@
<p class="text-sm font-medium" :class="[primaryTextClass]"> <p class="text-sm font-medium" :class="[primaryTextClass]">
{{ t('fileManage.batchEditMode') }} {{ t('fileManage.batchEditMode') }}
</p> </p>
<div class="grid gap-2 sm:grid-cols-3"> <div class="grid gap-2 sm:grid-cols-2 xl:grid-cols-4">
<button <button
v-for="option in batchEditModeOptions" v-for="option in batchEditModeOptions"
:key="option.value" :key="option.value"
type="button" 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)" :class="getBatchEditModeClass(option.value)"
:disabled="isBatchUpdating" :disabled="isBatchUpdating"
@click="batchEditForm.mode = option.value" @click="batchEditForm.mode = option.value"
@@ -1240,6 +1241,42 @@
type="datetime-local" type="datetime-local"
:label="t('fileManage.batchEditExpiresAt')" :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 <FileEditField
v-else-if="batchEditForm.mode === 'downloadLimit'" v-else-if="batchEditForm.mode === 'downloadLimit'"
v-model="batchEditForm.expired_count" v-model="batchEditForm.expired_count"
@@ -1377,6 +1414,7 @@ import {
RefreshCwIcon, RefreshCwIcon,
RotateCcwIcon, RotateCcwIcon,
SearchIcon, SearchIcon,
TimerResetIcon,
TrashIcon, TrashIcon,
XIcon XIcon
} from 'lucide-vue-next' } from 'lucide-vue-next'
@@ -1815,6 +1853,11 @@ const batchEditModeOptions = computed<
label: t('fileManage.batchEditExpiresAt'), label: t('fileManage.batchEditExpiresAt'),
icon: ClockIcon icon: ClockIcon
}, },
{
value: 'relativeExpire',
label: t('fileManage.batchEditRelativeExpire'),
icon: TimerResetIcon
},
{ {
value: 'downloadLimit', value: 'downloadLimit',
label: t('fileManage.batchEditDownloadLimit'), label: t('fileManage.batchEditDownloadLimit'),