feat(P21PWA1-4): 移动扫码页面与盘点路由导航联动
这个提交包含在:
@@ -0,0 +1,118 @@
|
||||
import type { InventoryCheckResult } from "../api/inventoryChecks";
|
||||
|
||||
export type QueuedInventoryScanItem = {
|
||||
id: string;
|
||||
inventoryCheckId: string;
|
||||
assetId: string;
|
||||
assetNo: string;
|
||||
result: InventoryCheckResult;
|
||||
note: string | null;
|
||||
createdAt: string;
|
||||
lastError: string | null;
|
||||
};
|
||||
|
||||
const DB_NAME = "hyb-oa-mobile-inventory";
|
||||
const DB_VERSION = 1;
|
||||
const STORE_NAME = "inventory_scan_queue";
|
||||
|
||||
let dbPromise: Promise<IDBDatabase> | null = null;
|
||||
|
||||
function getDatabase() {
|
||||
if (!dbPromise) {
|
||||
dbPromise = new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
||||
|
||||
request.onupgradeneeded = () => {
|
||||
const db = request.result;
|
||||
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
||||
db.createObjectStore(STORE_NAME, { keyPath: "id" });
|
||||
}
|
||||
};
|
||||
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
request.onerror = () => reject(request.error ?? new Error("indexeddb_open_failed"));
|
||||
});
|
||||
}
|
||||
|
||||
return dbPromise;
|
||||
}
|
||||
|
||||
function requestPromise<T>(request: IDBRequest<T>) {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
request.onerror = () => reject(request.error ?? new Error("indexeddb_request_failed"));
|
||||
});
|
||||
}
|
||||
|
||||
async function withStore<T>(mode: IDBTransactionMode, executor: (store: IDBObjectStore) => Promise<T>) {
|
||||
const db = await getDatabase();
|
||||
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
const tx = db.transaction(STORE_NAME, mode);
|
||||
const store = tx.objectStore(STORE_NAME);
|
||||
|
||||
executor(store)
|
||||
.then((result) => {
|
||||
tx.oncomplete = () => resolve(result);
|
||||
tx.onerror = () => reject(tx.error ?? new Error("indexeddb_tx_failed"));
|
||||
})
|
||||
.catch((error) => reject(error));
|
||||
});
|
||||
}
|
||||
|
||||
export async function enqueueInventoryScan(input: {
|
||||
inventoryCheckId: string;
|
||||
assetId: string;
|
||||
assetNo: string;
|
||||
result: InventoryCheckResult;
|
||||
note?: string | null;
|
||||
}) {
|
||||
const id = `${input.inventoryCheckId}:${input.assetId}`;
|
||||
const item: QueuedInventoryScanItem = {
|
||||
id,
|
||||
inventoryCheckId: input.inventoryCheckId,
|
||||
assetId: input.assetId,
|
||||
assetNo: input.assetNo,
|
||||
result: input.result,
|
||||
note: input.note?.trim() ? input.note.trim() : null,
|
||||
createdAt: new Date().toISOString(),
|
||||
lastError: null,
|
||||
};
|
||||
|
||||
await withStore("readwrite", async (store) => {
|
||||
await requestPromise(store.put(item));
|
||||
return true;
|
||||
});
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
export async function listQueuedInventoryScans() {
|
||||
return withStore("readonly", async (store) => {
|
||||
const records = (await requestPromise(store.getAll())) as QueuedInventoryScanItem[];
|
||||
return records.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
||||
});
|
||||
}
|
||||
|
||||
export async function removeQueuedInventoryScan(id: string) {
|
||||
await withStore("readwrite", async (store) => {
|
||||
await requestPromise(store.delete(id));
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateQueuedInventoryScanError(id: string, lastError: string | null) {
|
||||
await withStore("readwrite", async (store) => {
|
||||
const existing = (await requestPromise(store.get(id))) as QueuedInventoryScanItem | undefined;
|
||||
if (!existing) {
|
||||
return false;
|
||||
}
|
||||
await requestPromise(
|
||||
store.put({
|
||||
...existing,
|
||||
lastError,
|
||||
}),
|
||||
);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户