auto-save 2026-05-26 00:13 (~8)

This commit is contained in:
2026-05-26 00:13:17 +08:00
parent 089a30d970
commit 544087cf9d
8 changed files with 407 additions and 67 deletions

View File

@@ -16,11 +16,93 @@ export const projects = ref([])
// Current project ID | 当前项目ID
export const currentProjectId = ref(null)
export const projectSyncStatus = ref('idle')
export const projectSyncError = ref('')
const API_BASE = import.meta.env.VITE_SKG_API_BASE || '/api'
const apiUrl = (path) => `${API_BASE}${path.startsWith('/') ? '' : '/'}${path}`
const remoteSaveTimers = new Map()
let initPromise = null
let remoteAvailable = false
// Current project | 当前项目
export const currentProject = computed(() => {
return projects.value.find(p => p.id === currentProjectId.value) || null
})
const dateToSeconds = (value) => {
if (value instanceof Date) return value.getTime() / 1000
const parsed = new Date(value)
return Number.isFinite(parsed.getTime()) ? parsed.getTime() / 1000 : Date.now() / 1000
}
const secondsToDate = (value) => {
if (value instanceof Date) return value
const num = Number(value || 0)
return new Date(num > 100000000000 ? num : num * 1000)
}
const projectFromApi = (item) => ({
id: item.id,
name: item.name || '未命名项目',
thumbnail: item.thumbnail || '',
visibility: item.visibility || 'private',
ownerId: item.owner_id || '',
ownerName: item.owner_name || '',
ownerEmail: item.owner_email || '',
ownerProvider: item.owner_provider || '',
version: item.version || 1,
createdAt: secondsToDate(item.created_at),
updatedAt: secondsToDate(item.updated_at),
canvasData: item.canvas_data || {
nodes: [],
edges: [],
viewport: { x: 100, y: 50, zoom: 0.8 }
}
})
const projectToApi = (project) => ({
id: project.id,
name: project.name || '未命名项目',
thumbnail: project.thumbnail || '',
visibility: project.visibility || 'private',
canvas_data: cleanProjectForStorage(project).canvasData || {
nodes: [],
edges: [],
viewport: { x: 100, y: 50, zoom: 0.8 }
},
created_at: dateToSeconds(project.createdAt),
updated_at: dateToSeconds(project.updatedAt),
source: 'canvas'
})
const requestJson = async (path, init = {}) => {
const response = await fetch(apiUrl(path), {
...init,
headers: {
...(init.body instanceof FormData ? {} : { 'Content-Type': 'application/json' }),
...(init.headers || {})
}
})
if (!response.ok) {
const text = await response.text().catch(() => '')
throw new Error(text || `${path} ${response.status}`)
}
return response.json()
}
const mergeProjectLists = (localItems, remoteItems) => {
const byId = new Map()
for (const item of remoteItems) byId.set(item.id, item)
for (const item of localItems) {
const existing = byId.get(item.id)
if (!existing || dateToSeconds(item.updatedAt) > dateToSeconds(existing.updatedAt)) {
byId.set(item.id, item)
}
}
return [...byId.values()].sort((a, b) => dateToSeconds(b.updatedAt) - dateToSeconds(a.updatedAt))
}
/**
* Load projects from localStorage | 从 localStorage 加载项目
*/
@@ -42,6 +124,69 @@ export const loadProjects = () => {
}
}
const saveRemoteProjectNow = async (project) => {
if (!project?.id) return null
const response = await requestJson(`/canvas-projects/${encodeURIComponent(project.id)}`, {
method: 'PUT',
body: JSON.stringify(projectToApi(project))
})
return response.item ? projectFromApi(response.item) : null
}
const scheduleRemoteSave = (project, delay = 800) => {
if (!remoteAvailable || !project?.id) return
if (remoteSaveTimers.has(project.id)) {
clearTimeout(remoteSaveTimers.get(project.id))
}
remoteSaveTimers.set(project.id, setTimeout(async () => {
remoteSaveTimers.delete(project.id)
try {
projectSyncStatus.value = 'syncing'
await saveRemoteProjectNow(project)
projectSyncStatus.value = 'synced'
projectSyncError.value = ''
} catch (err) {
projectSyncStatus.value = 'error'
projectSyncError.value = err.message || '项目同步失败'
console.warn('Failed to sync project:', err)
}
}, delay))
}
const importLocalProjectsToServer = async (localItems) => {
if (!localItems.length) return []
const payload = { projects: localItems.map(projectToApi) }
const response = await requestJson('/canvas-projects/import', {
method: 'POST',
body: JSON.stringify(payload)
})
return (response.items || []).map(projectFromApi)
}
export const loadRemoteProjects = async () => {
try {
projectSyncStatus.value = 'syncing'
const localItems = [...projects.value]
const response = await requestJson('/canvas-projects')
remoteAvailable = true
const remoteItems = (response.items || []).map(projectFromApi)
const missingLocal = localItems.filter(local => !remoteItems.some(remote => remote.id === local.id))
const importedItems = await importLocalProjectsToServer(missingLocal)
const merged = mergeProjectLists(localItems, [...remoteItems, ...importedItems])
projects.value = merged
saveProjects({ remote: false })
projectSyncStatus.value = 'synced'
projectSyncError.value = ''
return merged
} catch (err) {
remoteAvailable = false
projectSyncStatus.value = 'error'
projectSyncError.value = err.message || '项目同步失败'
console.warn('Remote project sync unavailable:', err)
return projects.value
}
}
/**
* Clean node data for storage | 清理节点数据用于存储
* Removes base64 data URLs to reduce storage size | 移除 base64 数据减小存储大小
@@ -89,7 +234,7 @@ const cleanProjectForStorage = (project) => {
* Save projects to localStorage | 保存项目到 localStorage
* Handles QuotaExceededError by compressing data | 通过压缩数据处理配额超限错误
*/
export const saveProjects = () => {
export const saveProjects = ({ remote = false } = {}) => {
// Always clean data before saving | 保存前始终清理数据
const cleanedProjects = projects.value.map(cleanProjectForStorage)
@@ -128,6 +273,10 @@ export const saveProjects = () => {
console.error('Failed to save projects:', err)
}
}
if (remote) {
for (const project of projects.value) scheduleRemoteSave(project)
}
}
/**
@@ -155,6 +304,7 @@ export const createProject = (name = '未命名项目') => {
projects.value = [newProject, ...projects.value]
saveProjects()
scheduleRemoteSave(newProject, 0)
return id
}
@@ -179,6 +329,7 @@ export const updateProject = (id, data) => {
projects.value = [updated, ...projects.value]
saveProjects()
scheduleRemoteSave(updated)
return true
}
@@ -239,6 +390,10 @@ export const getProjectCanvas = (id) => {
export const deleteProject = (id) => {
projects.value = projects.value.filter(p => p.id !== id)
saveProjects()
if (remoteAvailable) {
requestJson(`/canvas-projects/${encodeURIComponent(id)}`, { method: 'DELETE' })
.catch(err => console.warn('Failed to delete remote project:', err))
}
}
/**
@@ -263,6 +418,7 @@ export const duplicateProject = (id) => {
projects.value = [newProject, ...projects.value]
saveProjects()
scheduleRemoteSave(newProject, 0)
return newId
}
@@ -320,51 +476,14 @@ export const getSortedProjects = (sortBy = 'updatedAt', order = 'desc') => {
/**
* Initialize projects store | 初始化项目存储
*/
export const initProjectsStore = () => {
export const initProjectsStore = async () => {
if (initPromise) return initPromise
initPromise = (async () => {
loadProjects()
// Create sample project if empty | 如果为空则创建示例项目
if (projects.value.length === 0) {
const id = createProject('示例项目')
const project = projects.value.find(p => p.id === id)
if (project) {
project.canvasData = {
nodes: [
{
id: 'node_0',
type: 'text',
position: { x: 150, y: 150 },
data: {
content: '一只金毛寻回犬在草地上奔跑,摇着尾巴,脸上带着快乐的表情。它的毛发在阳光下闪耀,眼神充满了对自由的渴望,全身散发着阳光、友善的气息。',
label: '文本输入'
}
},
{
id: 'node_1',
type: 'imageConfig',
position: { x: 500, y: 150 },
data: {
prompt: '',
model: 'auto',
size: '1024x1024',
label: '文生图'
}
}
],
edges: [
{
id: 'edge_node_0_node_1',
source: 'node_0',
target: 'node_1',
sourceHandle: 'right',
targetHandle: 'left'
}
],
viewport: { x: 100, y: 50, zoom: 0.8 }
}
saveProjects()
}
}
await loadRemoteProjects()
return projects.value
})()
return initPromise
}
// Export for debugging | 导出用于调试

View File

@@ -415,7 +415,7 @@ const scrollToProjects = () => {
}
// Initialize projects store on mount | 挂载时初始化项目存储
onMounted(() => {
initProjectsStore()
onMounted(async () => {
await initProjectsStore()
})
</script>