feat: add admin batch policy editor
This commit is contained in:
@@ -3,7 +3,11 @@ import { useI18n } from 'vue-i18n'
|
|||||||
import { FileService } from '@/services'
|
import { FileService } from '@/services'
|
||||||
import { useAlertStore } from '@/stores/alertStore'
|
import { useAlertStore } from '@/stores/alertStore'
|
||||||
import type {
|
import type {
|
||||||
|
AdminBatchEditForm,
|
||||||
AdminBatchDeleteFilesResponse,
|
AdminBatchDeleteFilesResponse,
|
||||||
|
AdminBatchUpdateFilesRequest,
|
||||||
|
AdminBatchUpdateFilesResponse,
|
||||||
|
AdminFilePatchPayload,
|
||||||
AdminFileListParams,
|
AdminFileListParams,
|
||||||
AdminFileStatusFilter,
|
AdminFileStatusFilter,
|
||||||
AdminFileSummary,
|
AdminFileSummary,
|
||||||
@@ -46,11 +50,19 @@ type ErrorWithResponse = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isLegacyBatchDeleteUnavailable = (error: unknown) => {
|
const isLegacyBatchActionUnavailable = (error: unknown) => {
|
||||||
const status = (error as ErrorWithResponse)?.response?.status
|
const status = (error as ErrorWithResponse)?.response?.status
|
||||||
return status === 404 || status === 405
|
return status === 404 || status === 405
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const legacyForeverExpiresAt = '2099-12-31T23:59'
|
||||||
|
|
||||||
|
const emptyBatchEditForm = (): AdminBatchEditForm => ({
|
||||||
|
mode: 'expiresAt',
|
||||||
|
expired_at: '',
|
||||||
|
expired_count: null
|
||||||
|
})
|
||||||
|
|
||||||
export function useAdminFiles() {
|
export function useAdminFiles() {
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const alertStore = useAlertStore()
|
const alertStore = useAlertStore()
|
||||||
@@ -80,6 +92,8 @@ export function useAdminFiles() {
|
|||||||
expired_at: '',
|
expired_at: '',
|
||||||
expired_count: null
|
expired_count: null
|
||||||
})
|
})
|
||||||
|
const showBatchEditModal = ref(false)
|
||||||
|
const batchEditForm = ref<AdminBatchEditForm>(emptyBatchEditForm())
|
||||||
|
|
||||||
const showTextPreview = ref(false)
|
const showTextPreview = ref(false)
|
||||||
const previewText = ref('')
|
const previewText = ref('')
|
||||||
@@ -89,6 +103,8 @@ export function useAdminFiles() {
|
|||||||
const downloadingFileId = ref<number | null>(null)
|
const downloadingFileId = ref<number | null>(null)
|
||||||
const selectedFileIds = ref<Set<number>>(new Set())
|
const selectedFileIds = ref<Set<number>>(new Set())
|
||||||
const isBatchDeleting = ref(false)
|
const isBatchDeleting = ref(false)
|
||||||
|
const isBatchUpdating = ref(false)
|
||||||
|
const isBatchActionRunning = computed(() => isBatchDeleting.value || isBatchUpdating.value)
|
||||||
const totalPages = computed(() => Math.max(Math.ceil(params.value.total / params.value.size), 1))
|
const totalPages = computed(() => Math.max(Math.ceil(params.value.total / params.value.size), 1))
|
||||||
const storageUsedText = computed(() => formatFileSize(summary.value.storageUsed))
|
const storageUsedText = computed(() => formatFileSize(summary.value.storageUsed))
|
||||||
const currentPageSelectedCount = computed(
|
const currentPageSelectedCount = computed(
|
||||||
@@ -233,6 +249,72 @@ export function useAdminFiles() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const resetBatchEditForm = () => {
|
||||||
|
batchEditForm.value = emptyBatchEditForm()
|
||||||
|
}
|
||||||
|
|
||||||
|
const openBatchEditModal = () => {
|
||||||
|
if (!hasSelectedFiles.value || isBatchActionRunning.value) return
|
||||||
|
|
||||||
|
resetBatchEditForm()
|
||||||
|
showBatchEditModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeBatchEditModal = (force = false) => {
|
||||||
|
if (isBatchUpdating.value && !force) return
|
||||||
|
|
||||||
|
showBatchEditModal.value = false
|
||||||
|
resetBatchEditForm()
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildBatchUpdatePayload = (ids: number[]): AdminBatchUpdateFilesRequest | null => {
|
||||||
|
const form = batchEditForm.value
|
||||||
|
|
||||||
|
if (form.mode === 'expiresAt') {
|
||||||
|
if (!form.expired_at) return null
|
||||||
|
return {
|
||||||
|
ids,
|
||||||
|
expired_at: form.expired_at
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (form.mode === 'downloadLimit') {
|
||||||
|
if (form.expired_count === null || !Number.isFinite(form.expired_count)) return null
|
||||||
|
return {
|
||||||
|
ids,
|
||||||
|
expired_count: form.expired_count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ids,
|
||||||
|
clearExpiredAt: true,
|
||||||
|
clear_expired_at: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildLegacyBatchPatchPayload = (
|
||||||
|
id: number,
|
||||||
|
payload: AdminBatchUpdateFilesRequest
|
||||||
|
): AdminFilePatchPayload => {
|
||||||
|
if (payload.clearExpiredAt || payload.clear_expired_at) {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
expired_at: legacyForeverExpiresAt,
|
||||||
|
expired_count: -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const patchPayload: AdminFilePatchPayload = { id }
|
||||||
|
if (payload.expired_at !== undefined) {
|
||||||
|
patchPayload.expired_at = payload.expired_at
|
||||||
|
}
|
||||||
|
if (payload.expired_count !== undefined) {
|
||||||
|
patchPayload.expired_count = payload.expired_count
|
||||||
|
}
|
||||||
|
return patchPayload
|
||||||
|
}
|
||||||
|
|
||||||
const loadFiles = async () => {
|
const loadFiles = async () => {
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
try {
|
try {
|
||||||
@@ -351,7 +433,7 @@ export function useAdminFiles() {
|
|||||||
try {
|
try {
|
||||||
return await FileService.deleteAdminFiles(ids)
|
return await FileService.deleteAdminFiles(ids)
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
if (!isLegacyBatchDeleteUnavailable(error)) {
|
if (!isLegacyBatchActionUnavailable(error)) {
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -369,8 +451,36 @@ export function useAdminFiles() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateAdminFilesWithFallback = async (
|
||||||
|
ids: number[],
|
||||||
|
payload: AdminBatchUpdateFilesRequest
|
||||||
|
): Promise<ApiResponse<AdminBatchUpdateFilesResponse>> => {
|
||||||
|
try {
|
||||||
|
return await FileService.updateAdminFiles(payload)
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (!isLegacyBatchActionUnavailable(error)) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
ids.map((id) => FileService.updateFile(buildLegacyBatchPatchPayload(id, payload)))
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
detail: {
|
||||||
|
updatedCount: ids.length,
|
||||||
|
updated_count: ids.length,
|
||||||
|
updated: ids,
|
||||||
|
missing: [],
|
||||||
|
failed: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const deleteSelectedFiles = async () => {
|
const deleteSelectedFiles = async () => {
|
||||||
if (!hasSelectedFiles.value || isBatchDeleting.value) return
|
if (!hasSelectedFiles.value || isBatchActionRunning.value) return
|
||||||
|
|
||||||
const ids = Array.from(selectedFileIds.value)
|
const ids = Array.from(selectedFileIds.value)
|
||||||
if (!window.confirm(t('fileManage.batchDeleteConfirm', { count: ids.length }))) {
|
if (!window.confirm(t('fileManage.batchDeleteConfirm', { count: ids.length }))) {
|
||||||
@@ -409,6 +519,49 @@ export function useAdminFiles() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleBatchUpdate = async () => {
|
||||||
|
if (!hasSelectedFiles.value || isBatchActionRunning.value) return
|
||||||
|
|
||||||
|
const ids = Array.from(selectedFileIds.value)
|
||||||
|
const payload = buildBatchUpdatePayload(ids)
|
||||||
|
if (!payload) {
|
||||||
|
alertStore.showAlert(t('fileManage.batchUpdateNoFields'), 'warning')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!window.confirm(t('fileManage.batchUpdateConfirm', { count: ids.length }))) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isBatchUpdating.value = true
|
||||||
|
try {
|
||||||
|
const response = await updateAdminFilesWithFallback(ids, payload)
|
||||||
|
const detail = response.detail
|
||||||
|
const updatedCount = detail?.updatedCount ?? detail?.updated_count ?? ids.length
|
||||||
|
const failedCount = detail?.failedCount ?? detail?.failed_count ?? 0
|
||||||
|
|
||||||
|
clearSelection()
|
||||||
|
await loadFiles()
|
||||||
|
closeBatchEditModal(true)
|
||||||
|
alertStore.showAlert(
|
||||||
|
t(
|
||||||
|
failedCount > 0
|
||||||
|
? 'fileManage.batchUpdatePartialSuccess'
|
||||||
|
: 'fileManage.batchUpdateSuccess',
|
||||||
|
{
|
||||||
|
count: updatedCount,
|
||||||
|
failed: failedCount
|
||||||
|
}
|
||||||
|
),
|
||||||
|
failedCount > 0 ? 'warning' : 'success'
|
||||||
|
)
|
||||||
|
} catch (error: unknown) {
|
||||||
|
alertStore.showAlert(getErrorMessage(error, t('fileManage.batchUpdateFailed')), 'error')
|
||||||
|
} finally {
|
||||||
|
isBatchUpdating.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const openTextPreview = async (file: AdminFileViewItem) => {
|
const openTextPreview = async (file: AdminFileViewItem) => {
|
||||||
if (!file.isTextFile || isPreviewLoading.value) return
|
if (!file.isTextFile || isPreviewLoading.value) return
|
||||||
|
|
||||||
@@ -491,10 +644,13 @@ export function useAdminFiles() {
|
|||||||
hasActiveFilters,
|
hasActiveFilters,
|
||||||
isLoading,
|
isLoading,
|
||||||
isAllCurrentPageSelected,
|
isAllCurrentPageSelected,
|
||||||
|
isBatchActionRunning,
|
||||||
isBatchDeleting,
|
isBatchDeleting,
|
||||||
|
isBatchUpdating,
|
||||||
isCurrentPagePartiallySelected,
|
isCurrentPagePartiallySelected,
|
||||||
isPreviewLoading,
|
isPreviewLoading,
|
||||||
isSaving,
|
isSaving,
|
||||||
|
batchEditForm,
|
||||||
downloadingFileId,
|
downloadingFileId,
|
||||||
hasSelectedFiles,
|
hasSelectedFiles,
|
||||||
params,
|
params,
|
||||||
@@ -504,12 +660,14 @@ export function useAdminFiles() {
|
|||||||
selectedFileIds,
|
selectedFileIds,
|
||||||
storageUsedText,
|
storageUsedText,
|
||||||
summary,
|
summary,
|
||||||
|
showBatchEditModal,
|
||||||
showEditModal,
|
showEditModal,
|
||||||
editForm,
|
editForm,
|
||||||
showTextPreview,
|
showTextPreview,
|
||||||
previewText,
|
previewText,
|
||||||
totalPages,
|
totalPages,
|
||||||
closeEditModal,
|
closeEditModal,
|
||||||
|
closeBatchEditModal,
|
||||||
closeTextPreview,
|
closeTextPreview,
|
||||||
copyText,
|
copyText,
|
||||||
clearSelection,
|
clearSelection,
|
||||||
@@ -519,8 +677,10 @@ export function useAdminFiles() {
|
|||||||
exportPreviewText,
|
exportPreviewText,
|
||||||
handlePageChange,
|
handlePageChange,
|
||||||
handleSearch,
|
handleSearch,
|
||||||
|
handleBatchUpdate,
|
||||||
handleUpdate,
|
handleUpdate,
|
||||||
loadFiles,
|
loadFiles,
|
||||||
|
openBatchEditModal,
|
||||||
openEditModal,
|
openEditModal,
|
||||||
openTextPreview,
|
openTextPreview,
|
||||||
refreshFiles,
|
refreshFiles,
|
||||||
|
|||||||
@@ -427,6 +427,20 @@ export default {
|
|||||||
batchDeleteSuccess: 'Deleted {count} files',
|
batchDeleteSuccess: 'Deleted {count} files',
|
||||||
batchDeletePartialSuccess: 'Deleted {count} files, {failed} failed',
|
batchDeletePartialSuccess: 'Deleted {count} files, {failed} failed',
|
||||||
batchDeleteFailed: 'Batch delete failed',
|
batchDeleteFailed: 'Batch delete failed',
|
||||||
|
batchEdit: 'Batch Edit',
|
||||||
|
batchEditTitle: 'Batch Edit File Policy',
|
||||||
|
batchEditSelected: 'Update the selected {count} files',
|
||||||
|
batchEditMode: 'Update Mode',
|
||||||
|
batchEditExpiresAt: 'Set Expiration Time',
|
||||||
|
batchEditDownloadLimit: 'Set Retrieval Limit',
|
||||||
|
batchEditForever: 'Make Permanent',
|
||||||
|
batchEditForeverHint: 'Clear the expiration time and make retrievals unlimited.',
|
||||||
|
batchUpdateConfirm: 'Update the selected {count} files?',
|
||||||
|
batchUpdateSuccess: 'Updated {count} files',
|
||||||
|
batchUpdatePartialSuccess: 'Updated {count} files, {failed} failed',
|
||||||
|
batchUpdateFailed: 'Batch update failed',
|
||||||
|
batchUpdateNoFields: 'Choose a policy to update',
|
||||||
|
applyBatchEdit: 'Apply Changes',
|
||||||
headers: {
|
headers: {
|
||||||
select: 'Select',
|
select: 'Select',
|
||||||
code: 'Retrieve Code',
|
code: 'Retrieve Code',
|
||||||
|
|||||||
@@ -425,6 +425,20 @@ export default {
|
|||||||
batchDeleteSuccess: '已删除 {count} 个文件',
|
batchDeleteSuccess: '已删除 {count} 个文件',
|
||||||
batchDeletePartialSuccess: '已删除 {count} 个文件,{failed} 个失败',
|
batchDeletePartialSuccess: '已删除 {count} 个文件,{failed} 个失败',
|
||||||
batchDeleteFailed: '批量删除失败',
|
batchDeleteFailed: '批量删除失败',
|
||||||
|
batchEdit: '批量编辑',
|
||||||
|
batchEditTitle: '批量编辑文件策略',
|
||||||
|
batchEditSelected: '将更新选中的 {count} 个文件',
|
||||||
|
batchEditMode: '更新方式',
|
||||||
|
batchEditExpiresAt: '设置过期时间',
|
||||||
|
batchEditDownloadLimit: '设置取件次数',
|
||||||
|
batchEditForever: '设为永久有效',
|
||||||
|
batchEditForeverHint: '清空过期时间,并将取件次数设为不限。',
|
||||||
|
batchUpdateConfirm: '确认更新选中的 {count} 个文件?',
|
||||||
|
batchUpdateSuccess: '已更新 {count} 个文件',
|
||||||
|
batchUpdatePartialSuccess: '已更新 {count} 个文件,{failed} 个失败',
|
||||||
|
batchUpdateFailed: '批量更新失败',
|
||||||
|
batchUpdateNoFields: '请选择要更新的策略',
|
||||||
|
applyBatchEdit: '应用更改',
|
||||||
headers: {
|
headers: {
|
||||||
select: '选择',
|
select: '选择',
|
||||||
code: '取件码',
|
code: '取件码',
|
||||||
|
|||||||
+10
-1
@@ -2,6 +2,9 @@ import api, { rawApiClient } from './client'
|
|||||||
import { multipartUploadConfig } from './shared'
|
import { multipartUploadConfig } from './shared'
|
||||||
import type {
|
import type {
|
||||||
AdminBatchDeleteFilesResponse,
|
AdminBatchDeleteFilesResponse,
|
||||||
|
AdminBatchUpdateFilesRequest,
|
||||||
|
AdminBatchUpdateFilesResponse,
|
||||||
|
AdminFilePatchPayload,
|
||||||
AdminFileListParams,
|
AdminFileListParams,
|
||||||
AdminFilePreviewResponse,
|
AdminFilePreviewResponse,
|
||||||
ApiResponse,
|
ApiResponse,
|
||||||
@@ -125,10 +128,16 @@ export class FileService {
|
|||||||
return api.get('/admin/file/list', { params })
|
return api.get('/admin/file/list', { params })
|
||||||
}
|
}
|
||||||
|
|
||||||
static async updateFile(data: FileEditForm): Promise<ApiResponse> {
|
static async updateFile(data: FileEditForm | AdminFilePatchPayload): Promise<ApiResponse> {
|
||||||
return api.patch('/admin/file/update', data)
|
return api.patch('/admin/file/update', data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async updateAdminFiles(
|
||||||
|
data: AdminBatchUpdateFilesRequest
|
||||||
|
): Promise<ApiResponse<AdminBatchUpdateFilesResponse>> {
|
||||||
|
return api.patch('/admin/file/batch-update', data)
|
||||||
|
}
|
||||||
|
|
||||||
static async deleteAdminFile(id: number): Promise<ApiResponse> {
|
static async deleteAdminFile(id: number): Promise<ApiResponse> {
|
||||||
return api.delete('/admin/file/delete', {
|
return api.delete('/admin/file/delete', {
|
||||||
data: { id }
|
data: { id }
|
||||||
|
|||||||
@@ -83,6 +83,15 @@ export interface FileEditForm {
|
|||||||
expired_count: number | null
|
expired_count: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AdminFilePatchPayload {
|
||||||
|
id: number
|
||||||
|
code?: string
|
||||||
|
prefix?: string
|
||||||
|
suffix?: string
|
||||||
|
expired_at?: string | null
|
||||||
|
expired_count?: number | null
|
||||||
|
}
|
||||||
|
|
||||||
export interface FileListResponse {
|
export interface FileListResponse {
|
||||||
data: FileListItem[]
|
data: FileListItem[]
|
||||||
total: number
|
total: number
|
||||||
@@ -130,6 +139,43 @@ export interface AdminBatchDeleteFilesResponse {
|
|||||||
failed?: AdminBatchDeleteFileFailure[]
|
failed?: AdminBatchDeleteFileFailure[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AdminBatchUpdateFileFailure {
|
||||||
|
id: number
|
||||||
|
reason: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminBatchUpdateFilesRequest {
|
||||||
|
ids: number[]
|
||||||
|
expired_at?: string | null
|
||||||
|
expired_count?: number | null
|
||||||
|
clearExpiredAt?: boolean
|
||||||
|
clear_expired_at?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminBatchUpdateFilesResponse {
|
||||||
|
requestedCount?: number
|
||||||
|
requested_count?: number
|
||||||
|
uniqueCount?: number
|
||||||
|
unique_count?: number
|
||||||
|
updatedCount?: number
|
||||||
|
updated_count?: number
|
||||||
|
missingCount?: number
|
||||||
|
missing_count?: number
|
||||||
|
failedCount?: number
|
||||||
|
failed_count?: number
|
||||||
|
updated?: number[]
|
||||||
|
missing?: number[]
|
||||||
|
failed?: AdminBatchUpdateFileFailure[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AdminBatchEditMode = 'expiresAt' | 'downloadLimit' | 'forever'
|
||||||
|
|
||||||
|
export interface AdminBatchEditForm {
|
||||||
|
mode: AdminBatchEditMode
|
||||||
|
expired_at: string
|
||||||
|
expired_count: number | null
|
||||||
|
}
|
||||||
|
|
||||||
export interface FileUploadResponse {
|
export interface FileUploadResponse {
|
||||||
code: string
|
code: string
|
||||||
name: string
|
name: string
|
||||||
|
|||||||
@@ -154,7 +154,7 @@
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
||||||
:checked="isAllCurrentPageSelected"
|
:checked="isAllCurrentPageSelected"
|
||||||
:disabled="isBatchDeleting"
|
:disabled="isBatchActionRunning"
|
||||||
:indeterminate="isCurrentPagePartiallySelected"
|
:indeterminate="isCurrentPagePartiallySelected"
|
||||||
@change="toggleCurrentPageSelection"
|
@change="toggleCurrentPageSelection"
|
||||||
/>
|
/>
|
||||||
@@ -172,7 +172,7 @@
|
|||||||
v-if="hasSelectedFiles"
|
v-if="hasSelectedFiles"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
:disabled="isBatchDeleting"
|
:disabled="isBatchActionRunning"
|
||||||
@click="clearSelection"
|
@click="clearSelection"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
@@ -180,10 +180,22 @@
|
|||||||
</template>
|
</template>
|
||||||
{{ t('fileManage.clearSelection') }}
|
{{ t('fileManage.clearSelection') }}
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
|
<BaseButton
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
:disabled="!hasSelectedFiles || isBatchDeleting"
|
||||||
|
:loading="isBatchUpdating"
|
||||||
|
@click="openBatchEditModal"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<ClockIcon class="mr-2 h-4 w-4" />
|
||||||
|
</template>
|
||||||
|
{{ t('fileManage.batchEdit') }}
|
||||||
|
</BaseButton>
|
||||||
<BaseButton
|
<BaseButton
|
||||||
variant="danger"
|
variant="danger"
|
||||||
size="sm"
|
size="sm"
|
||||||
:disabled="!hasSelectedFiles"
|
:disabled="!hasSelectedFiles || isBatchUpdating"
|
||||||
:loading="isBatchDeleting"
|
:loading="isBatchDeleting"
|
||||||
@click="deleteSelectedFiles"
|
@click="deleteSelectedFiles"
|
||||||
>
|
>
|
||||||
@@ -262,7 +274,7 @@
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
||||||
:checked="selectedFileIds.has(file.id)"
|
:checked="selectedFileIds.has(file.id)"
|
||||||
:disabled="isBatchDeleting"
|
:disabled="isBatchActionRunning"
|
||||||
:aria-label="t('fileManage.selectFile', { name: file.displayName })"
|
:aria-label="t('fileManage.selectFile', { name: file.displayName })"
|
||||||
@change="toggleFileSelection(file.id)"
|
@change="toggleFileSelection(file.id)"
|
||||||
/>
|
/>
|
||||||
@@ -467,6 +479,95 @@
|
|||||||
</template>
|
</template>
|
||||||
</BaseModal>
|
</BaseModal>
|
||||||
|
|
||||||
|
<BaseModal :show="showBatchEditModal" size="lg" @close="closeBatchEditModal()">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center space-x-3">
|
||||||
|
<div class="p-2 rounded-lg" :class="[isDarkMode ? 'bg-indigo-500/10' : 'bg-indigo-50']">
|
||||||
|
<ClockIcon
|
||||||
|
class="w-5 h-5"
|
||||||
|
:class="[isDarkMode ? 'text-indigo-400' : 'text-indigo-600']"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<h3
|
||||||
|
class="text-xl font-semibold leading-6"
|
||||||
|
:class="[isDarkMode ? 'text-white' : 'text-gray-900']"
|
||||||
|
>
|
||||||
|
{{ t('fileManage.batchEditTitle') }}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="space-y-5">
|
||||||
|
<div
|
||||||
|
class="rounded-lg border px-4 py-3 text-sm"
|
||||||
|
:class="[
|
||||||
|
isDarkMode
|
||||||
|
? 'border-indigo-500/20 bg-indigo-500/10 text-indigo-200'
|
||||||
|
: 'border-indigo-100 bg-indigo-50 text-indigo-700'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
{{ t('fileManage.batchEditSelected', { count: selectedCount }) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-3">
|
||||||
|
<p class="text-sm font-medium" :class="[primaryTextClass]">
|
||||||
|
{{ t('fileManage.batchEditMode') }}
|
||||||
|
</p>
|
||||||
|
<div class="grid gap-2 sm:grid-cols-3">
|
||||||
|
<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="getBatchEditModeClass(option.value)"
|
||||||
|
:disabled="isBatchUpdating"
|
||||||
|
@click="batchEditForm.mode = option.value"
|
||||||
|
>
|
||||||
|
<component :is="option.icon" class="mr-2 h-4 w-4" />
|
||||||
|
<span>{{ option.label }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FileEditField
|
||||||
|
v-if="batchEditForm.mode === 'expiresAt'"
|
||||||
|
v-model="batchEditForm.expired_at"
|
||||||
|
type="datetime-local"
|
||||||
|
:label="t('fileManage.batchEditExpiresAt')"
|
||||||
|
/>
|
||||||
|
<FileEditField
|
||||||
|
v-else-if="batchEditForm.mode === 'downloadLimit'"
|
||||||
|
v-model="batchEditForm.expired_count"
|
||||||
|
type="number"
|
||||||
|
:label="t('fileManage.batchEditDownloadLimit')"
|
||||||
|
:placeholder="t('fileManage.form.downloadLimitPlaceholder')"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="rounded-lg border px-4 py-3 text-sm"
|
||||||
|
:class="[
|
||||||
|
isDarkMode
|
||||||
|
? 'border-gray-700 bg-gray-700/50 text-gray-300'
|
||||||
|
: 'border-gray-200 bg-gray-50 text-gray-600'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
{{ t('fileManage.batchEditForeverHint') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<BaseButton variant="secondary" :disabled="isBatchUpdating" @click="closeBatchEditModal()">
|
||||||
|
{{ t('common.cancel') }}
|
||||||
|
</BaseButton>
|
||||||
|
<BaseButton :loading="isBatchUpdating" @click="handleBatchUpdate">
|
||||||
|
<template #icon>
|
||||||
|
<CheckIcon class="w-4 h-4 mr-2" />
|
||||||
|
</template>
|
||||||
|
{{ t('fileManage.applyBatchEdit') }}
|
||||||
|
</BaseButton>
|
||||||
|
</template>
|
||||||
|
</BaseModal>
|
||||||
|
|
||||||
<BaseModal :show="showTextPreview" size="lg" @close="closeTextPreview">
|
<BaseModal :show="showTextPreview" size="lg" @close="closeTextPreview">
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="flex items-center space-x-3">
|
<div class="flex items-center space-x-3">
|
||||||
@@ -538,9 +639,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted } from 'vue'
|
import { computed, onMounted, type Component } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import type {
|
import type {
|
||||||
|
AdminBatchEditMode,
|
||||||
AdminFileSortBy,
|
AdminFileSortBy,
|
||||||
AdminFileSortOrder,
|
AdminFileSortOrder,
|
||||||
AdminFileStatusFilter,
|
AdminFileStatusFilter,
|
||||||
@@ -593,10 +695,13 @@ const {
|
|||||||
hasLoadError,
|
hasLoadError,
|
||||||
isLoading,
|
isLoading,
|
||||||
isAllCurrentPageSelected,
|
isAllCurrentPageSelected,
|
||||||
|
isBatchActionRunning,
|
||||||
isBatchDeleting,
|
isBatchDeleting,
|
||||||
|
isBatchUpdating,
|
||||||
isCurrentPagePartiallySelected,
|
isCurrentPagePartiallySelected,
|
||||||
isPreviewLoading,
|
isPreviewLoading,
|
||||||
isSaving,
|
isSaving,
|
||||||
|
batchEditForm,
|
||||||
downloadingFileId,
|
downloadingFileId,
|
||||||
hasSelectedFiles,
|
hasSelectedFiles,
|
||||||
params,
|
params,
|
||||||
@@ -606,10 +711,12 @@ const {
|
|||||||
selectedFileIds,
|
selectedFileIds,
|
||||||
storageUsedText,
|
storageUsedText,
|
||||||
summary,
|
summary,
|
||||||
|
showBatchEditModal,
|
||||||
showEditModal,
|
showEditModal,
|
||||||
editForm,
|
editForm,
|
||||||
showTextPreview,
|
showTextPreview,
|
||||||
previewText,
|
previewText,
|
||||||
|
closeBatchEditModal,
|
||||||
closeEditModal,
|
closeEditModal,
|
||||||
closeTextPreview,
|
closeTextPreview,
|
||||||
copyText,
|
copyText,
|
||||||
@@ -620,8 +727,10 @@ const {
|
|||||||
exportPreviewText,
|
exportPreviewText,
|
||||||
handlePageChange,
|
handlePageChange,
|
||||||
handleSearch,
|
handleSearch,
|
||||||
|
handleBatchUpdate,
|
||||||
handleUpdate,
|
handleUpdate,
|
||||||
loadFiles,
|
loadFiles,
|
||||||
|
openBatchEditModal,
|
||||||
openEditModal,
|
openEditModal,
|
||||||
openTextPreview,
|
openTextPreview,
|
||||||
refreshFiles,
|
refreshFiles,
|
||||||
@@ -701,6 +810,26 @@ const sortOrderOptions = computed<{ value: AdminFileSortOrder; label: string }[]
|
|||||||
{ value: 'asc', label: t('fileManage.sort.asc') }
|
{ value: 'asc', label: t('fileManage.sort.asc') }
|
||||||
])
|
])
|
||||||
|
|
||||||
|
const batchEditModeOptions = computed<
|
||||||
|
{ value: AdminBatchEditMode; label: string; icon: Component }[]
|
||||||
|
>(() => [
|
||||||
|
{
|
||||||
|
value: 'expiresAt',
|
||||||
|
label: t('fileManage.batchEditExpiresAt'),
|
||||||
|
icon: ClockIcon
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'downloadLimit',
|
||||||
|
label: t('fileManage.batchEditDownloadLimit'),
|
||||||
|
icon: DownloadIcon
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'forever',
|
||||||
|
label: t('fileManage.batchEditForever'),
|
||||||
|
icon: CheckIcon
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
const getPillClass = (active: boolean) => {
|
const getPillClass = (active: boolean) => {
|
||||||
if (active) return 'bg-indigo-600 text-white'
|
if (active) return 'bg-indigo-600 text-white'
|
||||||
return isDarkMode.value
|
return isDarkMode.value
|
||||||
@@ -713,6 +842,18 @@ const getStatusFilterClass = (value: AdminFileStatusFilter) =>
|
|||||||
|
|
||||||
const getTypeFilterClass = (value: AdminFileTypeFilter) => getPillClass(params.value.type === value)
|
const getTypeFilterClass = (value: AdminFileTypeFilter) => getPillClass(params.value.type === value)
|
||||||
|
|
||||||
|
const getBatchEditModeClass = (value: AdminBatchEditMode) => {
|
||||||
|
if (batchEditForm.value.mode === value) {
|
||||||
|
return isDarkMode.value
|
||||||
|
? 'border-indigo-500 bg-indigo-500/20 text-indigo-200'
|
||||||
|
: 'border-indigo-500 bg-indigo-50 text-indigo-700'
|
||||||
|
}
|
||||||
|
|
||||||
|
return isDarkMode.value
|
||||||
|
? 'border-gray-700 bg-gray-700/50 text-gray-300 hover:border-gray-600 hover:bg-gray-700'
|
||||||
|
: 'border-gray-200 bg-white text-gray-700 hover:border-gray-300 hover:bg-gray-50'
|
||||||
|
}
|
||||||
|
|
||||||
const getTypeLabel = (file: AdminFileViewItem) => {
|
const getTypeLabel = (file: AdminFileViewItem) => {
|
||||||
if (file.isChunkedFile) return t('fileManage.chunkedType')
|
if (file.isChunkedFile) return t('fileManage.chunkedType')
|
||||||
return file.isTextFile ? t('fileManage.textType') : t('fileManage.fileType')
|
return file.isTextFile ? t('fileManage.textType') : t('fileManage.fileType')
|
||||||
|
|||||||
Reference in New Issue
Block a user