From 1d2d16e710703bd02bb561518eb89e227372b570 Mon Sep 17 00:00:00 2001 From: YeomaiPC Date: Sun, 18 Oct 2026 16:50:55 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=8C=E6=AD=A5=E5=8A=9F=E8=83=BD=E6=9B=B4?= =?UTF-8?q?=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/FileLibraryPanel.vue | 23 ++++ app/components/VideoLibraryPanel.vue | 23 ++++ app/components/gallery/GalleryFilterBar.vue | 17 +++ app/components/storage/SiteSyncCard.vue | 27 ++++ .../storage/SiteSyncSelectedDialog.vue | 119 ++++++++++++++++++ app/pages/gallery.vue | 15 +++ app/types/backup.ts | 4 + i18n/locales/en.json | 9 ++ i18n/locales/zh-CN.json | 9 ++ server/api/admin/site-sync.post.ts | 10 +- server/utils/site-sync/runner.ts | 14 ++- 11 files changed, 268 insertions(+), 2 deletions(-) create mode 100644 app/components/storage/SiteSyncSelectedDialog.vue 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 +}