✨ 整合文件选择工具栏
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -483,6 +483,12 @@ export default {
|
||||
batchEditSelected: '将更新选中的 {count} 个文件',
|
||||
batchEditMode: '更新方式',
|
||||
batchEditExpiresAt: '设置过期时间',
|
||||
batchEditRelativeExpire: '设为一段时间后消失',
|
||||
batchEditRelativeExpireValue: '消失时间',
|
||||
batchEditRelativeExpirePlaceholder: '输入数量',
|
||||
batchEditRelativeExpireUnit: '单位',
|
||||
batchEditRelativeExpireHours: '小时',
|
||||
batchEditRelativeExpireDays: '天',
|
||||
batchEditDownloadLimit: '设置取件次数',
|
||||
batchEditForever: '设为永久有效',
|
||||
batchEditForeverHint: '清空过期时间,并将取件次数设为不限。',
|
||||
|
||||
+6
-1
@@ -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 {
|
||||
|
||||
@@ -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'),
|
||||
|
||||
Reference in New Issue
Block a user