基本功能完成

This commit is contained in:
2026-06-20 21:43:09 +08:00
parent 84590d5477
commit a89d23b43c
45 changed files with 3508 additions and 1261 deletions
+69 -5
View File
@@ -1,6 +1,7 @@
import * as SQLite from 'expo-sqlite';
import * as FileSystem from 'expo-file-system/legacy';
import type { MeasurementFrame, MeasurementMeta, SetupConfig, GateConfig } from '../protocol/types';
import { AMP_GAIN } from '../protocol/constants';
import * as BinLoader from './BinLoader';
import * as TemBundle from './TemBundle';
@@ -70,10 +71,37 @@ async function getDb(): Promise<SQLite.SQLiteDatabase> {
CREATE INDEX IF NOT EXISTS idx_fs ON frames(session_id);
`);
await runMigrations(_db);
}
return _db;
}
async function runMigrations(db: SQLite.SQLiteDatabase) {
const row = await db.getFirstAsync<{ user_version: number }>('PRAGMA user_version');
let version = row?.user_version ?? 0;
if (version < 1) {
try { await db.runAsync('ALTER TABLE sessions ADD COLUMN project_id TEXT'); } catch {}
await db.runAsync('PRAGMA user_version = 1');
version = 1;
}
if (version < 2) {
const orphans = await db.getAllAsync<{ session_id: string }>(
'SELECT session_id FROM sessions WHERE project_id IS NULL',
);
if (orphans.length > 0) {
const defId = generateProjectId();
await db.runAsync(
'INSERT INTO projects (project_id, name, created_at) VALUES (?,?,?)',
[defId, '默认工程', Date.now()],
);
await db.runAsync('UPDATE sessions SET project_id = ? WHERE project_id IS NULL', [defId]);
}
await db.runAsync('PRAGMA user_version = 2');
version = 2;
}
}
async function ensureDataDir() {
const info = await FileSystem.getInfoAsync(DATA_DIR);
if (!info.exists) await FileSystem.makeDirectoryAsync(DATA_DIR, { intermediates: true });
@@ -84,23 +112,44 @@ export async function initStorage(): Promise<void> {
await ensureDataDir();
}
export async function hasAnyProject(): Promise<boolean> {
const d = await getDb();
const row = await d.getFirstAsync<{ cnt: number }>('SELECT COUNT(*) AS cnt FROM projects');
return (row?.cnt ?? 0) > 0;
}
export async function getFirstProject(): Promise<ProjectInfo | null> {
const d = await getDb();
const r = await d.getFirstAsync<{ project_id: string; name: string; created_at: number }>(
'SELECT project_id, name, created_at FROM projects ORDER BY created_at DESC LIMIT 1',
);
if (!r) return null;
return { projectId: r.project_id, name: r.name, createdAt: r.created_at, lineCount: 0 };
}
// ── Project CRUD ────────────────────────────────────────────────────────────
function generateProjectId(): string {
const now = new Date();
const pad = (n: number, d = 2) => String(n).padStart(d, '0');
const rand = Math.random().toString(36).slice(2, 6);
return (
`P${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}` +
`${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`
`${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}` +
`${pad(now.getMilliseconds(), 3)}_${rand}`
);
}
function sanitizeName(name: string): string {
return name.trim().replace(/[\/\\:*?"<>|.\x00]/g, '_').slice(0, 50);
}
export async function createProject(name: string): Promise<string> {
const d = await getDb();
const id = generateProjectId();
await d.runAsync(
'INSERT INTO projects (project_id, name, created_at) VALUES (?,?,?)',
[id, name.trim(), Date.now()],
[id, sanitizeName(name), Date.now()],
);
return id;
}
@@ -132,6 +181,19 @@ export async function renameProject(projectId: string, name: string): Promise<vo
export async function deleteProject(projectId: string): Promise<void> {
const d = await getDb();
const sessions = await d.getAllAsync<{ session_id: string }>(
'SELECT session_id FROM sessions WHERE project_id = ?', [projectId],
);
for (const s of sessions) {
const frames = await d.getAllAsync<{ bin_path: string | null }>(
'SELECT bin_path FROM frames WHERE session_id = ?', [s.session_id],
);
for (const f of frames) {
if (f.bin_path) { try { await FileSystem.deleteAsync(f.bin_path, { idempotent: true }); } catch {} }
}
await d.runAsync('DELETE FROM frames WHERE session_id = ?', [s.session_id]);
}
await d.runAsync('DELETE FROM sessions WHERE project_id = ?', [projectId]);
await d.runAsync('DELETE FROM projects WHERE project_id = ?', [projectId]);
}
@@ -215,7 +277,7 @@ export async function persistFrame(frame: MeasurementFrame, sessionId: string):
} catch { binPath = null; }
await d.runAsync(
`INSERT OR REPLACE INTO frames (
`INSERT OR IGNORE INTO frames (
frame_id, session_id, ts, utc, lon, lat, alt,
ch_num, acc_num, gain, gps_st, sd_st, amp_ratio,
current, temp, batt, roll, pitch, yaw, src_mode, bin_path
@@ -381,7 +443,9 @@ export async function exportProject(
const bundleBytes = await TemBundle.packBundle(manifest, bins);
await ensureDataDir();
const outPath = DATA_DIR + `TEM_${proj.name}_${Date.now()}.tem`;
const safeName = sanitizeName(proj.name);
const ts = new Date().toISOString().slice(0, 10).replace(/-/g, '');
const outPath = DATA_DIR + `TEM_${safeName}_${ts}.tem`;
await BinLoader.writeFileBytes(outPath, bundleBytes);
return outPath;
}
@@ -473,7 +537,7 @@ export async function importProject(
[
frameId, newSessionId, meta.timestamp,
meta.utc, meta.longitude, meta.latitude, meta.altitude,
meta.channelNum, meta.accNum, meta.ampRatio,
meta.channelNum, meta.accNum, AMP_GAIN[meta.ampRatio] ?? 1,
meta.gpsStatus, meta.sdStatus, meta.ampRatio,
meta.current, meta.temperature, meta.batteryVolt,
meta.roll, meta.pitch, meta.yaw, meta.srcMode, binPath,