From 7e81a8ca59259d4f1a2d597e314a942c42d65c83 Mon Sep 17 00:00:00 2001 From: Ironsp <1522278303@qq.com> Date: Mon, 7 Sep 2026 16:35:13 +0800 Subject: [PATCH] =?UTF-8?q?feat=20=E9=85=8D=E7=BD=AESQlite?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- manifest.config.ts | 4 +- src/auto-imports.d.ts | 14 ++ src/components.d.ts | 1 - src/uni-pages.d.ts | 1 + src/utils/sqlite.ts | 469 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 487 insertions(+), 2 deletions(-) create mode 100644 src/utils/sqlite.ts diff --git a/manifest.config.ts b/manifest.config.ts index 2e88633..84a5d58 100644 --- a/manifest.config.ts +++ b/manifest.config.ts @@ -20,7 +20,9 @@ export default defineManifestConfig({ }, screenOrientation: ['landscape-primary'], /* 模块配置 */ - modules: {}, + modules: { + SQLite: {}, + }, /* 应用发布信息 */ distribute: { /* android打包配置 */ diff --git a/src/auto-imports.d.ts b/src/auto-imports.d.ts index 8ac7335..3875e62 100644 --- a/src/auto-imports.d.ts +++ b/src/auto-imports.d.ts @@ -9,6 +9,7 @@ declare global { const EffectScope: typeof import('vue')['EffectScope'] const asyncComputed: typeof import('@vueuse/core')['asyncComputed'] const autoResetRef: typeof import('@vueuse/core')['autoResetRef'] + const closeDatabase: typeof import('./utils/sqlite')['closeDatabase'] const computed: typeof import('vue')['computed'] const computedAsync: typeof import('@vueuse/core')['computedAsync'] const computedEager: typeof import('@vueuse/core')['computedEager'] @@ -33,11 +34,13 @@ declare global { const defineComponent: typeof import('vue')['defineComponent'] const eagerComputed: typeof import('@vueuse/core')['eagerComputed'] const effectScope: typeof import('vue')['effectScope'] + const executeSql: typeof import('./utils/sqlite')['executeSql'] const extendRef: typeof import('@vueuse/core')['extendRef'] const getCurrentInstance: typeof import('vue')['getCurrentInstance'] const getCurrentScope: typeof import('vue')['getCurrentScope'] const h: typeof import('vue')['h'] const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch'] + const initTables: typeof import('./utils/sqlite')['initTables'] const inject: typeof import('vue')['inject'] const injectLocal: typeof import('@vueuse/core')['injectLocal'] const isDefined: typeof import('@vueuse/core')['isDefined'] @@ -91,6 +94,7 @@ declare global { const onUnmounted: typeof import('vue')['onUnmounted'] const onUpdated: typeof import('vue')['onUpdated'] const onWatcherCleanup: typeof import('vue')['onWatcherCleanup'] + const openDatabase: typeof import('./utils/sqlite')['openDatabase'] const pausableWatch: typeof import('@vueuse/core')['pausableWatch'] const provide: typeof import('vue')['provide'] const provideLocal: typeof import('@vueuse/core')['provideLocal'] @@ -110,9 +114,11 @@ declare global { const resolveComponent: typeof import('vue')['resolveComponent'] const resolveRef: typeof import('@vueuse/core')['resolveRef'] const resolveUnref: typeof import('@vueuse/core')['resolveUnref'] + const selectSql: typeof import('./utils/sqlite')['selectSql'] const shallowReactive: typeof import('vue')['shallowReactive'] const shallowReadonly: typeof import('vue')['shallowReadonly'] const shallowRef: typeof import('vue')['shallowRef'] + const sqlite: typeof import('./utils/sqlite')['default'] const storage: typeof import('./utils/storage')['default'] const syncRef: typeof import('@vueuse/core')['syncRef'] const syncRefs: typeof import('@vueuse/core')['syncRefs'] @@ -124,6 +130,7 @@ declare global { const toRef: typeof import('vue')['toRef'] const toRefs: typeof import('vue')['toRefs'] const toValue: typeof import('vue')['toValue'] + const transaction: typeof import('./utils/sqlite')['transaction'] const triggerRef: typeof import('vue')['triggerRef'] const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount'] const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount'] @@ -331,6 +338,7 @@ declare module 'vue' { readonly EffectScope: UnwrapRef readonly asyncComputed: UnwrapRef readonly autoResetRef: UnwrapRef + readonly closeDatabase: UnwrapRef readonly computed: UnwrapRef readonly computedAsync: UnwrapRef readonly computedEager: UnwrapRef @@ -355,11 +363,13 @@ declare module 'vue' { readonly defineComponent: UnwrapRef readonly eagerComputed: UnwrapRef readonly effectScope: UnwrapRef + readonly executeSql: UnwrapRef readonly extendRef: UnwrapRef readonly getCurrentInstance: UnwrapRef readonly getCurrentScope: UnwrapRef readonly h: UnwrapRef readonly ignorableWatch: UnwrapRef + readonly initTables: UnwrapRef readonly inject: UnwrapRef readonly injectLocal: UnwrapRef readonly isDefined: UnwrapRef @@ -413,6 +423,7 @@ declare module 'vue' { readonly onUnmounted: UnwrapRef readonly onUpdated: UnwrapRef readonly onWatcherCleanup: UnwrapRef + readonly openDatabase: UnwrapRef readonly pausableWatch: UnwrapRef readonly provide: UnwrapRef readonly provideLocal: UnwrapRef @@ -432,9 +443,11 @@ declare module 'vue' { readonly resolveComponent: UnwrapRef readonly resolveRef: UnwrapRef readonly resolveUnref: UnwrapRef + readonly selectSql: UnwrapRef readonly shallowReactive: UnwrapRef readonly shallowReadonly: UnwrapRef readonly shallowRef: UnwrapRef + readonly sqlite: UnwrapRef readonly storage: UnwrapRef readonly syncRef: UnwrapRef readonly syncRefs: UnwrapRef @@ -446,6 +459,7 @@ declare module 'vue' { readonly toRef: UnwrapRef readonly toRefs: UnwrapRef readonly toValue: UnwrapRef + readonly transaction: UnwrapRef readonly triggerRef: UnwrapRef readonly tryOnBeforeMount: UnwrapRef readonly tryOnBeforeUnmount: UnwrapRef diff --git a/src/components.d.ts b/src/components.d.ts index a0bb22d..18b953c 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -35,7 +35,6 @@ declare module 'vue' { SettingsSensorConfig: typeof import('./components/settings/sensorConfig.vue')['default'] SettingsWifiNetwork: typeof import('./components/settings/wifiNetwork.vue')['default'] SettingsWifiSettings: typeof import('./components/settings/wifiSettings.vue')['default'] - ToggleFullscreen: typeof import('./components/toggleFullscreen.vue')['default'] WarningFilter: typeof import('./components/warning/filter.vue')['default'] WarningItem: typeof import('./components/warning/item.vue')['default'] WorkDashboard: typeof import('./components/work/dashboard.vue')['default'] diff --git a/src/uni-pages.d.ts b/src/uni-pages.d.ts index da6a0e6..266b6cd 100644 --- a/src/uni-pages.d.ts +++ b/src/uni-pages.d.ts @@ -6,6 +6,7 @@ type _LocationUrl = "/pages/Ahome/index" | "/pages/bluetooth/index" | + "/pages/demo/sqlite" | "/pages/heatmap/index" | "/pages/message/index" | "/pages/project/index" | diff --git a/src/utils/sqlite.ts b/src/utils/sqlite.ts new file mode 100644 index 0000000..e3a9e91 --- /dev/null +++ b/src/utils/sqlite.ts @@ -0,0 +1,469 @@ +/** + * SQLite 数据库工具类 + * 用于 uni-app Android/iOS 本地数据库操作 + * + * ======================== 使用说明 ======================== + * + * ## 1. 导入 + * + * ```typescript + * import DB, { initTables } from '@/utils/sqlite' + * ``` + * + * ## 2. 初始化数据库表 + * + * ```typescript + * // 首次使用或表结构变更时调用,仅需执行一次 + * await initTables() + * ``` + * + * ## 3. 插入数据(增) + * + * ```typescript + * // 方式一:使用 insert 方法(推荐,自动拼接 SQL) + * await DB.insert('device', { + * device_name: '徐工XS65H #01', + * device_type: '压路机', + * status: 'online', + * }) + * + * // 方式二:使用原生 SQL + * await DB.executeSql( + * `INSERT INTO device (device_name, device_type, status) + * VALUES ('徐工XS65H #01', '压路机', 'online')` + * ) + * ``` + * + * ## 4. 删除数据(删) + * + * ```typescript + * // 按条件删除 + * await DB.delete('device', "id = 1") + * // 删除所有 + * await DB.delete('device') + * ``` + * + * ## 5. 更新数据(改) + * + * ```typescript + * await DB.update('device', + * { status: 'offline', updated_at: new Date().toISOString() }, + * "id = 1" + * ) + * ``` + * + * ## 6. 查询数据(查) + * + * ### 6.1 查询全部 + * ```typescript + * const devices = await DB.select('device') + * ``` + * + * ### 6.2 详细查询 —— 按 ID 查询单条 + * ```typescript + * const device = await DB.selectById('device', 1) + * // 返回: { id: 1, device_name: '...', ... } 或 null + * ``` + * + * ### 6.3 详细查询 —— 按字段精确匹配 + * ```typescript + * const devices = await DB.selectByField('device', 'status', 'online') + * ``` + * + * ### 6.4 模糊查询 —— LIKE 模糊匹配 + * ```typescript + * // 按设备名称模糊查询 + * const devices = await DB.selectLike('device', 'device_name', '徐工') + * + * // 指定返回字段的模糊查询 + * const devices = await DB.selectLike('device', 'device_name', '徐工', 'id, device_name, status') + * ``` + * + * ### 6.5 条件查询 —— 带排序和分页 + * ```typescript + * const devices = await DB.select('device', '*', "status = 'online'", 'id DESC', 10) + * ``` + * + * ### 6.6 计数查询 + * ```typescript + * const count = await DB.count('device', "status = 'online'") + * ``` + * + * ### 6.7 原生 SQL 查询(复杂查询时使用) + * ```typescript + * const result = await DB.selectSql('SELECT * FROM device WHERE status = "online"') + * ``` + * + * ## 7. 事务操作 + * + * ```typescript + * await DB.transaction([ + * "INSERT INTO device (device_name, device_type) VALUES ('设备A', '压路机')", + * "INSERT INTO work_record (device_id, speed) VALUES (1, 5.5)", + * ]) + * ``` + * + * ## 8. 其他 + * + * ```typescript + * // 手动打开数据库 + * await DB.open() + * // 手动关闭数据库 + * await DB.close() + * ``` + */ + +const DB_NAME = 'road_roller' +const DB_PATH = '_doc/road_roller.db' + +let isOpen = false + +/** + * 转义 SQL 字符串值,防止单引号注入 + */ +function escapeValue(value: any): string { + if (value === null || value === undefined) + return 'NULL' + if (typeof value === 'number') + return String(value) + if (typeof value === 'boolean') + return value ? '1' : '0' + return `'${String(value).replace(/'/g, '\'\'')}'` +} + +/** + * 数据库操作类 + */ +class DB { + /** + * 打开数据库 + */ + static open(): Promise { + return new Promise((resolve, reject) => { + if (isOpen) { + resolve() + return + } + + plus.sqlite.openDatabase({ + name: DB_NAME, + path: DB_PATH, + success() { + console.log('数据库打开成功') + isOpen = true + resolve() + }, + fail(err: any) { + console.error('数据库打开失败:', err) + reject(err) + }, + }) + }) + } + + /** + * 关闭数据库 + */ + static close(): Promise { + return new Promise((resolve, reject) => { + if (!isOpen) { + resolve() + return + } + + plus.sqlite.closeDatabase({ + name: DB_NAME, + success() { + console.log('数据库关闭成功') + isOpen = false + resolve() + }, + fail(err: any) { + console.error('数据库关闭失败:', err) + reject(err) + }, + }) + }) + } + + /** + * 执行 SQL 语句(增、删、改) + */ + static executeSql(sql: string | string[]): Promise { + return DB.open().then(() => { + return new Promise((resolve, reject) => { + plus.sqlite.executeSql({ + name: DB_NAME, + sql, + success(res: any) { + resolve(res) + }, + fail(err: any) { + console.error('执行SQL失败:', err) + reject(err) + }, + }) + }) + }) + } + + /** + * 查询 SQL 语句 + */ + static selectSql(sql: string): Promise { + return DB.open().then(() => { + return new Promise((resolve, reject) => { + plus.sqlite.selectSql({ + name: DB_NAME, + sql, + success(res: any) { + resolve(res) + }, + fail(err: any) { + console.error('查询SQL失败:', err) + reject(err) + }, + }) + }) + }) + } + + /** + * 执行事务 + */ + static transaction(sqls: string[]): Promise { + return DB.open().then(() => { + return new Promise((resolve, reject) => { + plus.sqlite.transaction({ + name: DB_NAME, + sql: sqls, + success() { + console.log('事务执行成功') + resolve() + }, + fail(err: any) { + console.error('事务执行失败:', err) + reject(err) + }, + }) + }) + }) + } + + // ======================== CRUD 封装方法 ======================== + + /** + * 插入一条记录 + * @param table 表名 + * @param data 键值对数据对象 + * @example + * await DB.insert('device', { device_name: '设备A', device_type: '压路机', status: 'online' }) + */ + static async insert(table: string, data: Record): Promise { + const keys = Object.keys(data) + const values = keys.map(k => escapeValue(data[k])) + const sql = `INSERT INTO ${table} (${keys.join(', ')}) VALUES (${values.join(', ')})` + return DB.executeSql(sql) + } + + /** + * 更新记录 + * @param table 表名 + * @param data 要更新的键值对 + * @param where 条件子句(不含 WHERE 关键字),为空则更新全部 + * @example + * await DB.update('device', { status: 'offline' }, "id = 1") + */ + static async update(table: string, data: Record, where?: string): Promise { + const sets = Object.keys(data).map(k => `${k} = ${escapeValue(data[k])}`) + let sql = `UPDATE ${table} SET ${sets.join(', ')}` + if (where) + sql += ` WHERE ${where}` + return DB.executeSql(sql) + } + + /** + * 删除记录 + * @param table 表名 + * @param where 条件子句(不含 WHERE 关键字),为空则删除全部 + * @example + * await DB.delete('device', "id = 1") + */ + static async delete(table: string, where?: string): Promise { + let sql = `DELETE FROM ${table}` + if (where) + sql += ` WHERE ${where}` + return DB.executeSql(sql) + } + + /** + * 查询记录 + * @param table 表名 + * @param columns 返回字段,默认 '*' + * @param where 条件子句(不含 WHERE 关键字),可选 + * @param orderBy 排序子句(不含 ORDER BY 关键字),可选 + * @param limit 返回条数限制,可选 + * @example + * const list = await DB.select('device', '*', "status = 'online'", 'id DESC', 10) + */ + static async select( + table: string, + columns: string = '*', + where?: string, + orderBy?: string, + limit?: number, + ): Promise { + let sql = `SELECT ${columns} FROM ${table}` + if (where) + sql += ` WHERE ${where}` + if (orderBy) + sql += ` ORDER BY ${orderBy}` + if (limit) + sql += ` LIMIT ${limit}` + return DB.selectSql(sql) + } + + /** + * 按 ID 查询单条记录(详细查询) + * @param table 表名 + * @param id 主键值 + * @returns 记录对象或 null + * @example + * const device = await DB.selectById('device', 1) + */ + static async selectById(table: string, id: number | string): Promise { + const result = await DB.select( + table, + '*', + `id = ${typeof id === 'string' ? escapeValue(id) : id}`, + ) + return result?.[0] || null + } + + /** + * 按字段精确匹配查询(详细查询) + * @param table 表名 + * @param field 字段名 + * @param value 匹配值 + * @param columns 返回字段,默认 '*' + * @example + * const devices = await DB.selectByField('device', 'status', 'online') + */ + static async selectByField( + table: string, + field: string, + value: any, + columns: string = '*', + ): Promise { + return DB.select(table, columns, `${field} = ${escapeValue(value)}`) + } + + /** + * 模糊查询(LIKE %keyword%) + * @param table 表名 + * @param field 要模糊匹配的字段名 + * @param keyword 关键词 + * @param columns 返回字段,默认 '*' + * @example + * // 按设备名称模糊查询 + * const devices = await DB.selectLike('device', 'device_name', '徐工') + * // 指定返回字段 + * const devices = await DB.selectLike('device', 'device_name', '徐工', 'id, device_name') + */ + static async selectLike( + table: string, + field: string, + keyword: string, + columns: string = '*', + ): Promise { + const escapedKeyword = keyword.replace(/'/g, '\'\'') + return DB.select(table, columns, `${field} LIKE '%${escapedKeyword}%'`) + } + + /** + * 计数查询 + * @param table 表名 + * @param where 条件子句(不含 WHERE 关键字),可选 + * @returns 记录数量 + * @example + * const count = await DB.count('device', "status = 'online'") + */ + static async count(table: string, where?: string): Promise { + const result = await DB.select(table, 'COUNT(*) as total', where) + return result?.[0]?.total || 0 + } +} + +/** + * 初始化数据库表 + */ +export async function initTables() { + const sqls = [ + // 设备信息表 + `CREATE TABLE IF NOT EXISTS device ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + device_name TEXT NOT NULL, + device_type TEXT, + status TEXT DEFAULT 'offline', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + )`, + // 作业记录表 + `CREATE TABLE IF NOT EXISTS work_record ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + device_id INTEGER, + start_time DATETIME, + end_time DATETIME, + speed REAL, + frequency REAL, + amplitude REAL, + compaction_value REAL, + gps_lat REAL, + gps_lng REAL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (device_id) REFERENCES device(id) + )`, + // RTK 轨迹数据表 + `CREATE TABLE IF NOT EXISTS rtk_track ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + work_record_id INTEGER, + timestamp DATETIME, + latitude REAL, + longitude REAL, + altitude REAL, + speed REAL, + heading REAL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (work_record_id) REFERENCES work_record(id) + )`, + // 告警记录表 + `CREATE TABLE IF NOT EXISTS warning_record ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + device_id INTEGER, + warning_type TEXT, + warning_level TEXT, + message TEXT, + is_read INTEGER DEFAULT 0, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (device_id) REFERENCES device(id) + )`, + ] + + try { + await DB.transaction(sqls) + console.log('数据库表初始化成功') + } + catch (err) { + console.error('数据库表初始化失败:', err) + throw err + } +} + +// 兼容旧版按需导出 +export const openDatabase = DB.open +export const closeDatabase = DB.close +export const executeSql = DB.executeSql +export const selectSql = DB.selectSql +export const transaction = DB.transaction + +export default DB