fix: 多选同步弹窗复用站点同步表单缓存
- 抽取站点地址与 Token 的 24 小时缓存工具 - 存储页与多选同步弹窗共用 pichost.site-sync-form - 弹窗打开时读取最新缓存,修改后写回 - 增加缓存读取与过期测试
This commit is contained in:
1 parent
58424fe55d
commit
366b8a4272
4 files changed
+122
-31
No files matched your search
@@ -3,6 +3,7 @@ import type {
|
|||||||
SiteSyncDirection,
|
SiteSyncDirection,
|
||||||
SiteSyncRequest
|
SiteSyncRequest
|
||||||
} from '~/types/backup'
|
} from '~/types/backup'
|
||||||
|
import { loadSiteSyncForm, saveSiteSyncForm } from '~/utils/site-sync-form'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
busy: boolean
|
busy: boolean
|
||||||
@@ -30,42 +31,15 @@ const conflictMode = ref<'skip' | 'overwrite'>('skip')
|
|||||||
const direction = ref<SiteSyncDirection>('push')
|
const direction = ref<SiteSyncDirection>('push')
|
||||||
const scope = ref<'all' | 'image' | 'video' | 'file'>('all')
|
const scope = ref<'all' | 'image' | 'video' | 'file'>('all')
|
||||||
|
|
||||||
/** 目标站点地址与 Token 自动保存 24 小时 */
|
|
||||||
const FORM_STORAGE_KEY = 'pichost.site-sync-form'
|
|
||||||
const FORM_TTL_MS = 24 * 60 * 60 * 1000
|
|
||||||
|
|
||||||
function loadSavedForm() {
|
function loadSavedForm() {
|
||||||
if (!import.meta.client) return
|
if (!import.meta.client) return
|
||||||
try {
|
const saved = loadSiteSyncForm()
|
||||||
const raw = localStorage.getItem(FORM_STORAGE_KEY)
|
if (saved?.targetBaseUrl) targetBaseUrl.value = saved.targetBaseUrl
|
||||||
if (!raw) return
|
if (saved?.token) token.value = saved.token
|
||||||
const data = JSON.parse(raw) as {
|
|
||||||
targetBaseUrl?: string
|
|
||||||
token?: string
|
|
||||||
savedAt?: number
|
|
||||||
}
|
|
||||||
if (!data.savedAt || Date.now() - data.savedAt > FORM_TTL_MS) {
|
|
||||||
localStorage.removeItem(FORM_STORAGE_KEY)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (data.targetBaseUrl) targetBaseUrl.value = data.targetBaseUrl
|
|
||||||
if (data.token) token.value = data.token
|
|
||||||
} catch {
|
|
||||||
localStorage.removeItem(FORM_STORAGE_KEY)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function persistForm() {
|
function persistForm() {
|
||||||
if (!import.meta.client) return
|
saveSiteSyncForm(targetBaseUrl.value, token.value)
|
||||||
try {
|
|
||||||
localStorage.setItem(FORM_STORAGE_KEY, JSON.stringify({
|
|
||||||
targetBaseUrl: targetBaseUrl.value,
|
|
||||||
token: token.value,
|
|
||||||
savedAt: Date.now()
|
|
||||||
}))
|
|
||||||
} catch {
|
|
||||||
// 存储不可用时忽略
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
watch([targetBaseUrl, token], persistForm)
|
watch([targetBaseUrl, token], persistForm)
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { loadSiteSyncForm, saveSiteSyncForm } from '~/utils/site-sync-form'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
open: boolean
|
open: boolean
|
||||||
keys: string[]
|
keys: string[]
|
||||||
@@ -17,6 +19,19 @@ const token = ref('')
|
|||||||
const conflictMode = ref<'skip' | 'overwrite'>('skip')
|
const conflictMode = ref<'skip' | 'overwrite'>('skip')
|
||||||
const busy = ref(false)
|
const busy = ref(false)
|
||||||
|
|
||||||
|
watch(() => props.open, (open) => {
|
||||||
|
if (!open) return
|
||||||
|
const saved = loadSiteSyncForm()
|
||||||
|
if (saved) {
|
||||||
|
targetBaseUrl.value = saved.targetBaseUrl
|
||||||
|
token.value = saved.token
|
||||||
|
}
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
|
watch([targetBaseUrl, token], () => {
|
||||||
|
saveSiteSyncForm(targetBaseUrl.value, token.value)
|
||||||
|
})
|
||||||
|
|
||||||
const canSubmit = computed(() =>
|
const canSubmit = computed(() =>
|
||||||
!busy.value
|
!busy.value
|
||||||
&& props.keys.length > 0
|
&& props.keys.length > 0
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import {
|
||||||
|
loadSiteSyncForm,
|
||||||
|
saveSiteSyncForm,
|
||||||
|
SITE_SYNC_FORM_STORAGE_KEY
|
||||||
|
} from './site-sync-form'
|
||||||
|
|
||||||
|
function createStorage(): Storage {
|
||||||
|
const values = new Map<string, string>()
|
||||||
|
return {
|
||||||
|
get length() {
|
||||||
|
return values.size
|
||||||
|
},
|
||||||
|
clear: () => values.clear(),
|
||||||
|
getItem: key => values.get(key) ?? null,
|
||||||
|
key: index => [...values.keys()][index] ?? null,
|
||||||
|
removeItem: key => values.delete(key),
|
||||||
|
setItem: (key, value) => values.set(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('site sync form cache', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers()
|
||||||
|
vi.unstubAllGlobals()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('reads values saved by another component', () => {
|
||||||
|
vi.stubGlobal('localStorage', createStorage())
|
||||||
|
saveSiteSyncForm('http://127.0.0.1:3010', 'secret-token')
|
||||||
|
|
||||||
|
expect(loadSiteSyncForm()).toEqual({
|
||||||
|
targetBaseUrl: 'http://127.0.0.1:3010',
|
||||||
|
token: 'secret-token'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('drops the cache after 24 hours', () => {
|
||||||
|
const storage = createStorage()
|
||||||
|
vi.stubGlobal('localStorage', storage)
|
||||||
|
vi.useFakeTimers()
|
||||||
|
vi.setSystemTime(new Date('2026-10-19T00:00:00Z'))
|
||||||
|
saveSiteSyncForm('http://target', 'token')
|
||||||
|
|
||||||
|
vi.setSystemTime(new Date('2026-10-20T00:00:01Z'))
|
||||||
|
|
||||||
|
expect(loadSiteSyncForm()).toBeNull()
|
||||||
|
expect(storage.getItem(SITE_SYNC_FORM_STORAGE_KEY)).toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
export const SITE_SYNC_FORM_STORAGE_KEY = 'pichost.site-sync-form'
|
||||||
|
export const SITE_SYNC_FORM_TTL_MS = 24 * 60 * 60 * 1000
|
||||||
|
|
||||||
|
export interface SiteSyncFormCache {
|
||||||
|
targetBaseUrl: string
|
||||||
|
token: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStorage(): Storage | null {
|
||||||
|
return typeof localStorage === 'undefined' ? null : localStorage
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadSiteSyncForm(): SiteSyncFormCache | null {
|
||||||
|
const storage = getStorage()
|
||||||
|
if (!storage) return null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const raw = storage.getItem(SITE_SYNC_FORM_STORAGE_KEY)
|
||||||
|
if (!raw) return null
|
||||||
|
const data = JSON.parse(raw) as {
|
||||||
|
targetBaseUrl?: unknown
|
||||||
|
token?: unknown
|
||||||
|
savedAt?: unknown
|
||||||
|
}
|
||||||
|
if (typeof data.savedAt !== 'number' || Date.now() - data.savedAt > SITE_SYNC_FORM_TTL_MS) {
|
||||||
|
storage.removeItem(SITE_SYNC_FORM_STORAGE_KEY)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
targetBaseUrl: typeof data.targetBaseUrl === 'string' ? data.targetBaseUrl : '',
|
||||||
|
token: typeof data.token === 'string' ? data.token : ''
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
storage.removeItem(SITE_SYNC_FORM_STORAGE_KEY)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveSiteSyncForm(targetBaseUrl: string, token: string): void {
|
||||||
|
const storage = getStorage()
|
||||||
|
if (!storage) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
storage.setItem(SITE_SYNC_FORM_STORAGE_KEY, JSON.stringify({
|
||||||
|
targetBaseUrl,
|
||||||
|
token,
|
||||||
|
savedAt: Date.now()
|
||||||
|
}))
|
||||||
|
} catch {
|
||||||
|
// 存储不可用时忽略
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user