fix: 同步应用循环数组解构错误(.for is not iterable)
This commit is contained in:
+10
-10
File diff suppressed because one or more lines are too long
+2
-2
@@ -1,2 +1,2 @@
|
||||
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./web-DNeKtcdd.js","./index-B3Rf3B1G.js","./index-DWsZ3Bb0.css"])))=>i.map(i=>d[i]);
|
||||
import{r as s,_ as i}from"./index-B3Rf3B1G.js";var t;(function(e){e.Documents="DOCUMENTS",e.Data="DATA",e.Library="LIBRARY",e.Cache="CACHE",e.External="EXTERNAL",e.ExternalStorage="EXTERNAL_STORAGE"})(t||(t={}));var a;(function(e){e.UTF8="utf8",e.ASCII="ascii",e.UTF16="utf16"})(a||(a={}));const E=s("Filesystem",{web:()=>i(()=>import("./web-DNeKtcdd.js"),__vite__mapDeps([0,1,2]),import.meta.url).then(e=>new e.FilesystemWeb)});export{t as Directory,a as Encoding,E as Filesystem};
|
||||
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./web-DOIpoVam.js","./index-Cy5zm918.js","./index-DWsZ3Bb0.css"])))=>i.map(i=>d[i]);
|
||||
import{r as s,_ as i}from"./index-Cy5zm918.js";var t;(function(e){e.Documents="DOCUMENTS",e.Data="DATA",e.Library="LIBRARY",e.Cache="CACHE",e.External="EXTERNAL",e.ExternalStorage="EXTERNAL_STORAGE"})(t||(t={}));var a;(function(e){e.UTF8="utf8",e.ASCII="ascii",e.UTF16="utf16"})(a||(a={}));const E=s("Filesystem",{web:()=>i(()=>import("./web-DOIpoVam.js"),__vite__mapDeps([0,1,2]),import.meta.url).then(e=>new e.FilesystemWeb)});export{t as Directory,a as Encoding,E as Filesystem};
|
||||
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -8,7 +8,7 @@
|
||||
<link rel="icon" href="./icons/icon.svg" type="image/svg+xml" />
|
||||
<link rel="apple-touch-icon" href="./icons/icon-192.png" />
|
||||
<title>实习工作日志</title>
|
||||
<script type="module" crossorigin src="./assets/index-B3Rf3B1G.js"></script>
|
||||
<script type="module" crossorigin src="./assets/index-Cy5zm918.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-DWsZ3Bb0.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -12,19 +12,20 @@
|
||||
"cap:sync": "vite build && npx cap sync android"
|
||||
},
|
||||
"dependencies": {
|
||||
"dexie": "^4.0.8",
|
||||
"vue": "^3.5.13",
|
||||
"@capacitor/core": "^6.2.0",
|
||||
"@capacitor/filesystem": "^6.0.2"
|
||||
"@capacitor/filesystem": "^6.0.2",
|
||||
"dexie": "^4.0.8",
|
||||
"vue": "^3.5.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@capacitor/android": "^6.2.0",
|
||||
"@capacitor/cli": "^6.2.0",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"typescript": "~5.6.3",
|
||||
"vite": "^5.4.11",
|
||||
"electron": "^33.2.0",
|
||||
"electron-builder": "^25.1.8",
|
||||
"@capacitor/cli": "^6.2.0",
|
||||
"@capacitor/android": "^6.2.0"
|
||||
"fake-indexeddb": "^6.2.5",
|
||||
"typescript": "~5.6.3",
|
||||
"vite": "^5.4.11"
|
||||
},
|
||||
"description": "实习工作日志:打卡、记录、备忘录与密码库",
|
||||
"main": "electron/main.cjs",
|
||||
|
||||
Generated
+9
@@ -36,6 +36,9 @@ importers:
|
||||
electron-builder:
|
||||
specifier: ^25.1.8
|
||||
version: 25.1.8(bluebird@3.7.2)(electron-builder-squirrel-windows@25.1.8)(supports-color@7.2.0)
|
||||
fake-indexeddb:
|
||||
specifier: ^6.2.5
|
||||
version: 6.2.5
|
||||
typescript:
|
||||
specifier: ~5.6.3
|
||||
version: 5.6.3
|
||||
@@ -1012,6 +1015,10 @@ packages:
|
||||
resolution: {integrity: sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==}
|
||||
engines: {'0': node >=0.6.0}
|
||||
|
||||
fake-indexeddb@6.2.5:
|
||||
resolution: {integrity: sha512-CGnyrvbhPlWYMngksqrSSUT1BAVP49dZocrHuK0SvtR0D5TMs5wP0o3j7jexDJW01KSadjBp1M/71o/KR3nD1w==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
fast-deep-equal@3.1.3:
|
||||
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
||||
|
||||
@@ -3090,6 +3097,8 @@ snapshots:
|
||||
extsprintf@1.4.1:
|
||||
optional: true
|
||||
|
||||
fake-indexeddb@6.2.5: {}
|
||||
|
||||
fast-deep-equal@3.1.3: {}
|
||||
|
||||
fast-json-stable-stringify@2.1.0: {}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,48 @@
|
||||
import 'fake-indexeddb/auto'
|
||||
import { bindDatabase, WorklogDB } from '../src/db/schema'
|
||||
|
||||
async function main() {
|
||||
bindDatabase(new WorklogDB('repro_db'))
|
||||
const mod = await import('../src/db/schema')
|
||||
const db = mod.db
|
||||
|
||||
await db.project.bulkAdd([{ name: 'P1', archived: 0, created_at: '2025-01-01T00:00:00' }])
|
||||
await db.work_log.bulkAdd([{ date: '2025-01-02', created_at: '2025-01-02T00:00:00' }])
|
||||
await db.setting.bulkAdd([
|
||||
{ key: 'work_start', value: '09:00' },
|
||||
{ key: 'sync_base_url', value: 'http://127.0.0.1:3780' }
|
||||
])
|
||||
await db.attendance.bulkAdd([{ date: '2025-01-02', clock_in_at: '2025-01-02T09:00:00', clock_out_at: '2025-01-02T18:00:00', kind: 'normal', note: null, created_at: '2025-01-02T09:00:00' }])
|
||||
await db.table('transaction').bulkAdd([{ type: 'expense', category: '餐饮', amount: 2500, date: '2025-01-02', note: 'lunch', reimbursable: 1, reimbursed: 0, created_at: '2025-01-02T12:00:00' }])
|
||||
|
||||
const engine = await import('../src/sync/engine')
|
||||
engine.enableRealtimeSync()
|
||||
|
||||
await fetch('http://127.0.0.1:3780/api/auth/register', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username: 'rt', password: 'rt123456' })
|
||||
}).catch(() => { /* 已注册则忽略 */ })
|
||||
|
||||
const r1 = await engine.runSync({ baseUrl: 'http://127.0.0.1:3780', username: 'rt', password: 'rt123456' })
|
||||
console.log('SYNC1 OK:', JSON.stringify(r1))
|
||||
|
||||
// 触发本地变更,走实时推送路径
|
||||
await db.table('log_item').add({
|
||||
work_log_id: 1, content: '实时测试条目', project_id: null, category: 'dev',
|
||||
minutes: null, is_milestone: 0, needs_followup: 0, takeaway: null,
|
||||
remark: null, sort_order: 0, created_at: '2025-01-02T10:00:00'
|
||||
})
|
||||
await new Promise(r => setTimeout(r, 4500))
|
||||
console.log('REALTIME PATH DONE')
|
||||
|
||||
// 二次同步幂等验证
|
||||
const r2 = await engine.runSync({ baseUrl: 'http://127.0.0.1:3780', username: 'rt', password: 'rt123456' })
|
||||
console.log('SYNC2 OK:', JSON.stringify(r2))
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
main().catch(e => {
|
||||
console.error('FAILED:', (e && (e.stack || e.message)) || e)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -1,5 +1,6 @@
|
||||
// 多端同步引擎:全量差异对比 + 记录级新者胜 + 软删除墓碑
|
||||
// 实时性:本地变更 3 秒内推送;服务端变更通过 SSE 秒级通知其他端;60 秒轮询兜底
|
||||
import Dexie, { liveQuery } from 'dexie'
|
||||
import { db } from '../db/schema'
|
||||
import { getSetting, setSetting } from '../db/repo'
|
||||
import { getCloudCreds } from '../db/auth'
|
||||
@@ -167,6 +168,7 @@ let changeDebounce: number | null = null
|
||||
let sse: EventSource | null = null
|
||||
let sseKey = ''
|
||||
let realtimeAttached = false
|
||||
let realtimeSub: { unsubscribe: () => void } | null = null
|
||||
|
||||
export async function autoSyncNow(): Promise<void> {
|
||||
if (syncRunning) {
|
||||
@@ -179,20 +181,29 @@ export async function autoSyncNow(): Promise<void> {
|
||||
} catch { /* 自动同步静默失败,等待下次触发 */ }
|
||||
}
|
||||
|
||||
// 实时触发:本地任何数据变更 3 秒后自动推送
|
||||
// 实时触发:本地任何数据变更 3 秒后自动推送(Dexie 4 核心 liveQuery,跨全部业务表)
|
||||
export function enableRealtimeSync(): void {
|
||||
if (realtimeAttached) return
|
||||
realtimeAttached = true
|
||||
;(db as unknown as AnyRow).on('changes', () => {
|
||||
if (changesSuppressed || syncRunning) return
|
||||
if (changeDebounce) window.clearTimeout(changeDebounce)
|
||||
changeDebounce = window.setTimeout(() => { void autoSyncNow() }, 3000)
|
||||
const sub = liveQuery(async () => {
|
||||
const counts: Record<string, number> = {}
|
||||
for (const t of TABLES) counts[t] = await db.table(t).count()
|
||||
return counts
|
||||
})
|
||||
realtimeSub = sub.subscribe({
|
||||
next: () => {
|
||||
if (changesSuppressed || syncRunning) return
|
||||
if (changeDebounce) globalThis.clearTimeout(changeDebounce)
|
||||
changeDebounce = globalThis.setTimeout(() => { void autoSyncNow() }, 3000)
|
||||
},
|
||||
error: () => { /* 忽略监听错误,轮询兜底 */ }
|
||||
})
|
||||
}
|
||||
|
||||
export function stopRealtime(): void {
|
||||
if (realtimeSub) { realtimeSub.unsubscribe(); realtimeSub = null }
|
||||
if (sse) { sse.close(); sse = null; sseKey = '' }
|
||||
if (changeDebounce) { window.clearTimeout(changeDebounce); changeDebounce = null }
|
||||
if (changeDebounce) { globalThis.clearTimeout(changeDebounce); changeDebounce = null }
|
||||
}
|
||||
|
||||
// 服务端 SSE 通知:其他端有变更时立即拉取
|
||||
@@ -220,7 +231,7 @@ export async function runSync(config: SyncConfig): Promise<SyncResult> {
|
||||
const result = await doRunSync(config)
|
||||
if (pendingSync) {
|
||||
pendingSync = false
|
||||
window.setTimeout(() => { void autoSyncNow() }, 800)
|
||||
globalThis.setTimeout(() => { void autoSyncNow() }, 800)
|
||||
}
|
||||
return result
|
||||
} finally {
|
||||
@@ -308,9 +319,9 @@ async function doRunSync(config: SyncConfig): Promise<SyncResult> {
|
||||
removed++
|
||||
}
|
||||
}
|
||||
for (const [u, srow] of serverRows) {
|
||||
if (dead.has(u)) continue
|
||||
const local = byUuid.get(u)
|
||||
for (const srow of serverRows) {
|
||||
if (dead.has(srow.uuid)) continue
|
||||
const local = byUuid.get(srow.uuid)
|
||||
if (!local) {
|
||||
await table.add({ ...srow })
|
||||
pulled++
|
||||
|
||||
Reference in New Issue
Block a user