同步功能更新

This commit is contained in:
Yeomai committed 2026-10-18 16:50:55 +08:00
1 parent 21f0dc2830
commit 1d2d16e710
11 files changed
+268 -2

No files matched your search

+23
View File
@@ -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<string[]>([])
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"
/>
<UButton
v-if="selectedCount > 0"
icon="i-lucide-cloud-upload"
variant="outline"
color="neutral"
size="sm"
:disabled="!selectedCount || loading"
:label="t('storage.backup.syncSelectedAction')"
@click="openSync"
/>
<div class="flex gap-0.5 rounded-lg border border-default p-0.5">
<UButton
icon="i-lucide-layout-grid"
@@ -700,5 +717,11 @@ defineExpose({ refresh })
:selected-count="selectedCount"
@confirm="confirmBatchTags"
/>
<SiteSyncSelectedDialog
v-model:open="syncOpen"
:keys="syncKeys"
scope="file"
/>
</div>
</template>
+23
View File
@@ -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<LibraryStatsData | null>(null)
const selectedKeys = ref<Set<string>>(new Set())
const deleteModalOpen = ref(false)
const batchTagOpen = ref(false)
const syncOpen = ref(false)
const deleteTargetKeys = ref<string[]>([])
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"
/>
<UButton
v-if="selectedCount > 0"
icon="i-lucide-cloud-upload"
variant="outline"
color="neutral"
size="sm"
:disabled="!selectedCount || loading"
:label="t('storage.backup.syncSelectedAction')"
@click="openSync"
/>
<div class="flex gap-0.5 rounded-lg border border-default p-0.5">
<UButton
icon="i-lucide-layout-grid"
@@ -743,6 +760,12 @@ defineExpose({ refresh })
@confirm="confirmBatchTags"
/>
<SiteSyncSelectedDialog
v-model:open="syncOpen"
:keys="syncKeys"
scope="video"
/>
<VideoPreviewModal
v-model:open="previewOpen"
:item="previewItem"
@@ -14,11 +14,13 @@ const props = withDefaults(defineProps<{
loading?: boolean
showBatchDelete?: boolean
showBatchTags?: boolean
showBatchSync?: boolean
}>(), {
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') }}
</UButton>
<UButton
v-if="showBatchSync"
type="button"
icon="i-lucide-cloud-upload"
size="sm"
variant="outline"
color="neutral"
class="shrink-0"
:disabled="!selectedCount || loading"
@click="emit('batch-sync')"
>
{{ t('storage.backup.syncSelectedAction') }}
</UButton>
<div
class="inline-flex shrink-0 overflow-hidden rounded-md border border-default"
role="group"
+27
View File
@@ -28,6 +28,14 @@ const targetBaseUrl = ref('')
const token = ref('')
const conflictMode = ref<'skip' | 'overwrite'>('skip')
const direction = ref<SiteSyncDirection>('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
}
</script>
<template>
@@ -247,6 +260,20 @@ function setDirection(value: SiteSyncDirection) {
</div>
</div>
<div class="mt-3 flex flex-wrap items-center gap-2">
<span class="text-xs text-muted">{{ t('storage.backup.siteSyncContent') }}</span>
<UButton
v-for="item in scopeItems"
:key="item.value"
size="xs"
:variant="scope === item.value ? 'solid' : 'ghost'"
:color="scope === item.value ? 'primary' : 'neutral'"
:label="item.label"
:disabled="busy || disabled"
@click="setScope(item.value)"
/>
</div>
<UAlert
v-if="activeJob"
class="mt-4"
@@ -0,0 +1,119 @@
<script setup lang="ts">
const props = defineProps<{
open: boolean
keys: string[]
scope: 'image' | 'video' | 'file'
}>()
const emit = defineEmits<{
'update:open': [value: boolean]
}>()
const { t } = useI18n()
const { startSiteSync } = useBackup()
const targetBaseUrl = ref('')
const token = ref('')
const conflictMode = ref<'skip' | 'overwrite'>('skip')
const busy = ref(false)
const canSubmit = computed(() =>
!busy.value
&& props.keys.length > 0
&& Boolean(targetBaseUrl.value.trim())
&& Boolean(token.value.trim())
)
function setConflict(mode: 'skip' | 'overwrite') {
conflictMode.value = mode
}
async function submit() {
if (!canSubmit.value) return
busy.value = true
try {
await startSiteSync({
targetBaseUrl: targetBaseUrl.value.trim(),
token: token.value.trim(),
conflictMode: conflictMode.value,
direction: 'push',
scope: props.scope,
keys: props.keys
})
emit('update:open', false)
} catch {
// useBackup 已提示错误
} finally {
busy.value = false
}
}
</script>
<template>
<UModal
:open="open"
:title="t('storage.backup.syncSelectedTitle')"
:description="t('storage.backup.syncSelectedSubtitle', { n: keys.length })"
@update:open="emit('update:open', $event)"
>
<template #body>
<div class="space-y-3">
<div class="space-y-1.5">
<label class="text-xs text-muted">{{ t('storage.backup.siteSyncTarget') }}</label>
<UInput
v-model="targetBaseUrl"
placeholder="https://target.example.com"
size="sm"
class="w-full"
/>
</div>
<div class="space-y-1.5">
<label class="text-xs text-muted">{{ t('storage.backup.siteSyncToken') }}</label>
<UInput
v-model="token"
type="password"
:placeholder="t('storage.backup.siteSyncTokenPlaceholder')"
size="sm"
class="w-full"
/>
</div>
<div class="flex flex-wrap items-center gap-2">
<span class="text-xs text-muted">{{ t('storage.backup.siteSyncConflict') }}</span>
<UButton
size="xs"
:variant="conflictMode === 'skip' ? 'solid' : 'ghost'"
:color="conflictMode === 'skip' ? 'primary' : 'neutral'"
:label="t('storage.backup.siteSyncSkip')"
@click="setConflict('skip')"
/>
<UButton
size="xs"
:variant="conflictMode === 'overwrite' ? 'solid' : 'ghost'"
:color="conflictMode === 'overwrite' ? 'primary' : 'neutral'"
:label="t('storage.backup.siteSyncOverwrite')"
@click="setConflict('overwrite')"
/>
</div>
</div>
</template>
<template #footer>
<div class="flex justify-end gap-2">
<UButton
:label="t('common.cancel')"
color="neutral"
variant="outline"
@click="emit('update:open', false)"
/>
<UButton
:label="t('storage.backup.syncSelectedStart')"
icon="i-lucide-cloud-upload"
color="primary"
:loading="busy"
:disabled="!canSubmit"
@click="submit"
/>
</div>
</template>
</UModal>
</template>
+15
View File
@@ -1,5 +1,6 @@
<script setup lang="ts">
import type { ImageItem } from '~/types/image'
import SiteSyncSelectedDialog from '~/components/storage/SiteSyncSelectedDialog.vue'
interface StatsResponse {
uploadToday: number
@@ -74,6 +75,7 @@ const previewOpen = ref(false)
const previewImage = ref<ImageItem | null>(null)
const { viewMode } = useLibraryView('image')
const batchTagOpen = ref(false)
const syncOpen = ref(false)
const currentUploadSource = ref('all')
const currentStorageBackend = ref('all')
@@ -93,6 +95,7 @@ const storageBackendItems = computed(() => [
])
const selectedCount = computed(() => selectedKeys.value.size)
const syncKeys = computed(() => [...selectedKeys.value])
function onWindowScroll() {
showScrollTop.value = window.scrollY > 400
@@ -266,6 +269,10 @@ function goToTagSettings() {
void navigateTo({ path: '/settings', query: { tab: 'tags' } })
}
function openSync() {
syncOpen.value = true
}
function updateSelectedKeys(keys: Set<string>) {
selectedKeys.value = keys
}
@@ -495,6 +502,7 @@ watch(isAuthenticated, async (authed, prev) => {
:loading="galleryLoading"
:show-batch-delete="true"
:show-batch-tags="selectedCount > 0"
:show-batch-sync="selectedCount > 0"
@update:storage-backend="handleStorageBackendChange"
@update:upload-source="handleUploadSourceChange"
@update:tag-ids="handleTagIdsChange"
@@ -505,6 +513,7 @@ watch(isAuthenticated, async (authed, prev) => {
@reset="handleResetFilters"
@batch-delete="requestBatchDelete"
@batch-tags="batchTagOpen = true"
@batch-sync="openSync"
@manage-tags="goToTagSettings"
/>
</div>
@@ -591,6 +600,12 @@ watch(isAuthenticated, async (authed, prev) => {
@confirm="confirmBatchTags"
/>
<SiteSyncSelectedDialog
v-model:open="syncOpen"
:keys="syncKeys"
scope="image"
/>
<UButton
v-show="showScrollTop"
icon="i-lucide-arrow-up"
+4
View File
@@ -60,6 +60,10 @@ export interface SiteSyncRequest {
token: string
conflictMode: 'skip' | 'overwrite'
direction: SiteSyncDirection
/** 同步内容:全站或某一类媒体 */
scope?: 'all' | 'image' | 'video' | 'file'
/** 指定资源(同一类型) */
keys?: string[]
resumeJobId?: string
abandonJobId?: string
}