auto-save 2026-05-26 00:13 (~8)
This commit is contained in:
@@ -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 | 导出用于调试
|
||||
|
||||
@@ -415,7 +415,7 @@ const scrollToProjects = () => {
|
||||
}
|
||||
|
||||
// Initialize projects store on mount | 挂载时初始化项目存储
|
||||
onMounted(() => {
|
||||
initProjectsStore()
|
||||
onMounted(async () => {
|
||||
await initProjectsStore()
|
||||
})
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user