diff --git a/app/components/FileLibraryPanel.vue b/app/components/FileLibraryPanel.vue index ecc7fdf..cbbccb2 100644 --- a/app/components/FileLibraryPanel.vue +++ b/app/components/FileLibraryPanel.vue @@ -2,6 +2,7 @@ import type { FileItem, FileListResponse } from '~/types/file' import type { LibraryStatsData } from '~/types/media' import { formatFileSize } from '~/composables/useFileSize' +import SiteSyncSelectedDialog from '~/components/storage/SiteSyncSelectedDialog.vue' const props = withDefaults(defineProps<{ /** 文件库(file)与视频库结构一致,保留参数以便复用 */ @@ -32,6 +33,7 @@ const deleteModalOpen = ref(false) const deleteTargetKeys = ref([]) const deleting = ref(false) const batchTagOpen = ref(false) +const syncOpen = ref(false) const currentBackend = ref('all') const currentSource = ref('all') @@ -42,6 +44,7 @@ const activeUntaggedOnly = ref(false) const { viewMode } = useLibraryView(props.kind) const selectedCount = computed(() => selectedKeys.value.size) +const syncKeys = computed(() => [...selectedKeys.value]) const pageText = computed(() => props.kind === 'video' ? { @@ -263,6 +266,10 @@ function openBatchTags() { batchTagOpen.value = true } +function openSync() { + syncOpen.value = true +} + function requestDelete(item: FileItem) { if (!isAuthenticated.value) return deleteTargetKeys.value = [item.key] @@ -471,6 +478,16 @@ defineExpose({ refresh }) :label="t('library.batchDelete')" @click="requestBatchDelete" /> +
+ +
diff --git a/app/components/VideoLibraryPanel.vue b/app/components/VideoLibraryPanel.vue index 0ff0cdb..dcc3e36 100644 --- a/app/components/VideoLibraryPanel.vue +++ b/app/components/VideoLibraryPanel.vue @@ -2,6 +2,7 @@ import type { FileItem, FileListResponse } from '~/types/file' import type { LibraryStatsData } from '~/types/media' import { formatFileSize } from '~/composables/useFileSize' +import SiteSyncSelectedDialog from '~/components/storage/SiteSyncSelectedDialog.vue' const { isAuthenticated, handleAuthError } = useAuth() const toast = useToast() @@ -24,6 +25,7 @@ const stats = ref(null) const selectedKeys = ref>(new Set()) const deleteModalOpen = ref(false) const batchTagOpen = ref(false) +const syncOpen = ref(false) const deleteTargetKeys = ref([]) const deleting = ref(false) const previewOpen = ref(false) @@ -40,6 +42,7 @@ const activeUntaggedOnly = ref(false) const { viewMode } = useLibraryView('video') const selectedCount = computed(() => selectedKeys.value.size) +const syncKeys = computed(() => [...selectedKeys.value]) const backendItems = computed(() => [ { label: t('stats.filterStorageAll'), value: 'all' }, @@ -246,6 +249,10 @@ function openBatchTags() { batchTagOpen.value = true } +function openSync() { + syncOpen.value = true +} + async function confirmBatchTags(tagIds: number[], action: 'add' | 'remove') { const keys = [...selectedKeys.value] if (!keys.length || !tagIds.length) return @@ -470,6 +477,16 @@ defineExpose({ refresh }) :label="t('library.batchDelete')" @click="requestBatchDelete" /> +
+ + (), { selectedCount: 0, loading: false, showBatchDelete: true, showBatchTags: false, + showBatchSync: false, tagIds: () => [], tags: () => [], tagMode: 'or', @@ -37,6 +39,7 @@ const emit = defineEmits<{ 'reset': [] 'batch-delete': [] 'batch-tags': [] + 'batch-sync': [] 'manage-tags': [] }>() @@ -155,6 +158,20 @@ function viewButtonClass(active: boolean) { {{ t('stats.batchDelete') }} + + {{ t('storage.backup.syncSelectedAction') }} + +
('skip') const direction = ref('push') +const scope = ref<'all' | 'image' | 'video' | 'file'>('all') + +const scopeItems = computed(() => [ + { value: 'all' as const, label: t('storage.backup.siteSyncScopeAll') }, + { value: 'image' as const, label: t('storage.backup.siteSyncScopeImage') }, + { value: 'video' as const, label: t('storage.backup.siteSyncScopeVideo') }, + { value: 'file' as const, label: t('storage.backup.siteSyncScopeFile') } +]) watch(() => props.resumableJob, (job) => { if (!job) return @@ -99,6 +107,7 @@ function submit(resume = false) { token: token.value.trim(), conflictMode: conflictMode.value, direction: direction.value, + scope: scope.value, ...(resume && props.resumableJob ? { resumeJobId: props.resumableJob.id } : {}) }) token.value = '' @@ -132,6 +141,10 @@ function setConflictMode(mode: 'skip' | 'overwrite') { function setDirection(value: SiteSyncDirection) { direction.value = value } + +function setScope(value: 'all' | 'image' | 'video' | 'file') { + scope.value = value +}