diff --git a/uniapp/security/src/manifest.json b/uniapp/security/src/manifest.json index 9d4da98..3992f6b 100644 --- a/uniapp/security/src/manifest.json +++ b/uniapp/security/src/manifest.json @@ -1,30 +1,31 @@ { - "name" : "安全小秘书", - "appid" : "__UNI__A3FEFA1", - "description" : "本地 SQLite 密码保险箱,图案密码登录", - "versionName" : "1.0.0", - "versionCode" : "100", - "transformPx" : false, + "name": "安全小秘书", + "appid": "__UNI__A3FEFA1", + "description": "本地 SQLite 密码保险箱,图案密码登录", + "versionName": "1.0.0", + "versionCode": "100", + "transformPx": false, /* 5+App特有相关 */ - "app-plus" : { - "usingComponents" : true, - "nvueStyleCompiler" : "uni-app", - "compilerVersion" : 3, - "splashscreen" : { - "alwaysShowBeforeRender" : true, - "waiting" : true, - "autoclose" : true, - "delay" : 0 + "app-plus": { + "usingComponents": true, + "nvueStyleCompiler": "uni-app", + "compilerVersion": 3, + "splashscreen": { + "alwaysShowBeforeRender": true, + "waiting": true, + "autoclose": true, + "delay": 0 }, /* 模块配置 */ - "modules" : { - "sqlite" : {} + "modules": { + "sqlite": {}, + "Camera": {} }, /* 应用发布信息 */ - "distribute" : { + "distribute": { /* android打包配置 */ - "android" : { - "permissions" : [ + "android": { + "permissions": [ "", "", "", @@ -41,40 +42,77 @@ "", "" ], - "abiFilters" : [ "arm64-v8a", "x86" ] + "abiFilters": [ + "arm64-v8a", + "x86" + ] }, /* ios打包配置 */ - "ios" : { - "dSYMs" : false + "ios": { + "dSYMs": false }, /* SDK配置 */ - "sdkConfigs" : {}, - "splashscreen" : { - "useOriginalMsgbox" : true + "sdkConfigs": { + "share": {} + }, + "splashscreen": { + "useOriginalMsgbox": true + }, + "icons": { + "android": { + "hdpi": "unpackage/res/icons/72x72.png", + "xhdpi": "unpackage/res/icons/96x96.png", + "xxhdpi": "unpackage/res/icons/144x144.png", + "xxxhdpi": "unpackage/res/icons/192x192.png" + }, + "ios": { + "appstore": "unpackage/res/icons/1024x1024.png", + "ipad": { + "app": "unpackage/res/icons/76x76.png", + "app@2x": "unpackage/res/icons/152x152.png", + "notification": "unpackage/res/icons/20x20.png", + "notification@2x": "unpackage/res/icons/40x40.png", + "proapp@2x": "unpackage/res/icons/167x167.png", + "settings": "unpackage/res/icons/29x29.png", + "settings@2x": "unpackage/res/icons/58x58.png", + "spotlight": "unpackage/res/icons/40x40.png", + "spotlight@2x": "unpackage/res/icons/80x80.png" + }, + "iphone": { + "app@2x": "unpackage/res/icons/120x120.png", + "app@3x": "unpackage/res/icons/180x180.png", + "notification@2x": "unpackage/res/icons/40x40.png", + "notification@3x": "unpackage/res/icons/60x60.png", + "settings@2x": "unpackage/res/icons/58x58.png", + "settings@3x": "unpackage/res/icons/87x87.png", + "spotlight@2x": "unpackage/res/icons/80x80.png", + "spotlight@3x": "unpackage/res/icons/120x120.png" + } + } } } }, /* 快应用特有相关 */ - "quickapp" : {}, + "quickapp": {}, /* 小程序特有相关 */ - "mp-weixin" : { - "appid" : "", - "setting" : { - "urlCheck" : false + "mp-weixin": { + "appid": "", + "setting": { + "urlCheck": false }, - "usingComponents" : true + "usingComponents": true }, - "mp-alipay" : { - "usingComponents" : true + "mp-alipay": { + "usingComponents": true }, - "mp-baidu" : { - "usingComponents" : true + "mp-baidu": { + "usingComponents": true }, - "mp-toutiao" : { - "usingComponents" : true + "mp-toutiao": { + "usingComponents": true }, - "uniStatistics" : { - "enable" : false + "uniStatistics": { + "enable": false }, - "vueVersion" : "3" -} + "vueVersion": "3" +} \ No newline at end of file diff --git a/uniapp/security/src/pages/index/index.vue b/uniapp/security/src/pages/index/index.vue index cf2d3bc..b5b7025 100644 --- a/uniapp/security/src/pages/index/index.vue +++ b/uniapp/security/src/pages/index/index.vue @@ -219,7 +219,6 @@ {{ b.name }} - @@ -243,7 +242,7 @@ import { listPasswords, getPassword, savePassword, deletePassword, getSessionKey import { generatePassword } from '@/utils/crypto'; import { pickImageAsBase64 } from '@/utils/image'; // #ifdef APP-PLUS -import { exportBackupFile, shareBackupFile, listBackupFiles, readBackupFile, clearBackupFiles, pickSecFile, readBackupFromUri } from '@/utils/backup'; +import { exportBackupFile, listBackupFiles, readBackupFile, clearBackupFiles, pickSecFile, readBackupFromUri } from '@/utils/backup'; // #endif const ADVANCED = '高级'; @@ -496,9 +495,8 @@ async function onExport() { } try { const dump = await getExportPlainDump(); - const url = await exportBackupFile(dump, pwd); - uni.showToast({ title: '备份已生成', icon: 'none' }); - await shareBackupFile(url); + await exportBackupFile(dump, pwd); + uni.showToast({ title: '备份已生成,可在导入列表查看', icon: 'none' }); } catch (e: any) { uni.showToast({ title: e?.message || '导出失败', icon: 'none' }); } @@ -526,15 +524,15 @@ async function onPickBackup(item: { name: string; localUrl: string }) { } async function onPickFromFiles() { - const uri = await pickSecFile(); - if (!uri) { - uni.showToast({ title: '未选择文件', icon: 'none' }); + const picked = await pickSecFile(); + if (!picked.uri) { + uni.showToast({ title: picked.error || '未选择文件', icon: 'none' }); return; } const pwd = await askPassword('输入所选备份文件的密码'); if (pwd === null) return; try { - const dump = await readBackupFromUri(uri, pwd); + const dump = await readBackupFromUri(picked.uri, pwd); confirmRestore(dump, pwd); } catch (e: any) { uni.showToast({ title: e?.message || '恢复失败', icon: 'none' }); @@ -586,14 +584,10 @@ function onShowPath(item: { name: string; localUrl: string }) { uni.showModal({ title: '文件路径', content: item.localUrl, showCancel: false, confirmText: '知道了' }); } -async function onShareFile(item: { name: string; localUrl: string }) { - await shareBackupFile(item.localUrl); -} - function onClearBackupFiles() { uni.showModal({ title: '清空备份文件', - content: '将删除本机 _doc/backups 下所有 .sec 备份(已分享到外部的副本不受影响)。确定继续?', + content: '将删除本机 _doc/backups 下所有 .sec 备份(已转移到外部的副本不受影响)。确定继续?', confirmText: '清空', confirmColor: '#e53935', success: async (res) => { @@ -1053,7 +1047,6 @@ async function onSave() { .file-acts { display: flex; align-items: center; - gap: 18px; } .ico-path { @@ -1062,12 +1055,6 @@ async function onSave() { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%235a6472'%3E%3Cpath d='M11 7h2v2h-2zm0 4h2v6h-2zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z'/%3E%3C/svg%3E"); } -.ico-share { - width: 20px; - height: 20px; - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%235a6472'%3E%3Cpath d='M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81 1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.16c-.05.21-.08.43-.08.65 0 1.61 1.31 2.92 2.92 2.92 1.61 0 2.92-1.31 2.92-2.92s-1.31-2.92-2.92-2.92z'/%3E%3C/svg%3E"); -} - .list { flex: 1; } diff --git a/uniapp/security/src/utils/backup.ts b/uniapp/security/src/utils/backup.ts index eeef097..d287d43 100644 --- a/uniapp/security/src/utils/backup.ts +++ b/uniapp/security/src/utils/backup.ts @@ -77,23 +77,44 @@ export async function exportBackupFile( w.write(content); }, rej); }); - return fileEntry.toLocalURL ? fileEntry.toLocalURL() : fileEntry.fullPath; + // 返回 _doc 相对路径:可跨设备(真机/模拟器包名不同) + return `_doc/backups/${name}`; } -/** 调起系统分享,把备份文件另存到网盘/微信收藏等(失败静默,不影响已生成文件) */ -export function shareBackupFile(localUrl: string): Promise { - const p = plusAny(); - return new Promise((resolve) => { - try { - p.share.sendWithSystem( - { type: "file", files: [localUrl] }, - () => resolve(), - () => resolve(), - ); - } catch { - resolve(); +/** 用 Java 文件 IO 读取绝对路径文本(不经过 content resolver,真实机型可用) */ +function readTextByNativePath(p: any, path: string): string { + const File = p.android.importClass("java.io.File"); + const FileInputStream = p.android.importClass("java.io.FileInputStream"); + const fis = new FileInputStream(new File(path)); + try { + let out = ""; + let b = fis.read(); + // 备份文件为 JSON 文本且体积很小,逐字节读取避免跨语言 byte[] 桥接问题 + while (b !== -1) { + out += String.fromCharCode(b); + b = fis.read(); } - }); + // Latin-1 逐字节拼接后还原 UTF-8 再解码 + return decodeURIComponent(escape(out)); + } finally { + fis.close(); + } +} + +/** 通过 content resolver 输入流读取文本 */ +function readTextByStream(p: any, is: any): string { + const BufferedReader = p.android.importClass("java.io.BufferedReader"); + const InputStreamReader = p.android.importClass("java.io.InputStreamReader"); + const reader = new BufferedReader(new InputStreamReader(is)); + let out = ""; + let line = reader.readLine(); + while (line != null) { + out += line; + line = reader.readLine(); + if (line != null) out += "\n"; + } + reader.close(); + return out; } /** 列出 _doc/backups 下的备份文件 */ @@ -107,7 +128,8 @@ export async function listBackupFiles(): Promise { .filter((e) => e.name && e.name.endsWith(".sec")) .map((e) => ({ name: e.name, - localUrl: e.toLocalURL ? e.toLocalURL() : e.fullPath, + // 统一用 _doc 相对路径,跨设备(包名不同)保持稳定 + localUrl: `_doc/backups/${e.name}`, })); } @@ -169,10 +191,27 @@ export async function clearBackupFiles(): Promise { return targets.length; } -/** 调起系统文件选择器,返回所选文件的 content URI(取消或失败返回 null) */ -export function pickSecFile(): Promise { +/** 调起系统文件选择器,返回所选文件(content URI 或绝对路径);取消/失败返回 null,异常信息在 error */ +export function pickSecFile(): Promise<{ uri: string | null; error?: string }> { const p = plusAny(); return new Promise((resolve) => { + let settled = false; + const done = (r: { uri: string | null; error?: string }) => { + if (!settled) { + settled = true; + resolve(r); + } + }; + // 优先 HTML5+ 原生选择器:不依赖 activity result 回传,真实机型稳定可用 + if (typeof p.io.chooseFile === "function") { + p.io.chooseFile( + (path: string) => done({ uri: path }), + (err: any) => + done({ uri: null, error: err?.message || "文件选择失败" }), + { extension: ".sec" }, + ); + return; + } try { const main = p.android.runtimeMainActivity(); const Intent = p.android.importClass("android.content.Intent"); @@ -189,9 +228,9 @@ export function pickSecFile(): Promise { if (requestCode === REQUEST) { if (resultCode === -1 && data) { const uri = data.getData(); - resolve(uri ? uri.toString() : null); + done({ uri: uri ? uri.toString() : null }); } else { - resolve(null); + done({ uri: null }); } return; } @@ -201,13 +240,18 @@ export function pickSecFile(): Promise { Intent.createChooser(intent, "选择备份文件"), REQUEST, ); - } catch { - resolve(null); + // 新版运行时结果不回传 JS 钩子,超时兜底解除挂起 + setTimeout( + () => done({ uri: null, error: "文件选择器无响应" }), + 5 * 60 * 1000, + ); + } catch (e: any) { + done({ uri: null, error: e?.message || "调起文件选择器失败" }); } }); } -/** 读取 content URI 文本并用密码解密(供“从文件选择 .sec”恢复) */ +/** 读取所选文件(content URI 或绝对路径)并用密码解密 */ export async function readBackupFromUri( uriStr: string, password: string, @@ -215,25 +259,26 @@ export async function readBackupFromUri( const p = plusAny(); const content: string = await new Promise((resolve, reject) => { try { + if (!uriStr.startsWith("content://")) { + // chooseFile 返回的是本机文件路径:解析为绝对路径后用原生 IO 读取 + let path = uriStr; + try { + path = p.io.convertLocalFileSystemURL(uriStr) || uriStr; + } catch { + // 已是绝对路径,转换失败时沿用原值 + } + path = path.replace(/^file:\/\//, ""); + const File = p.android.importClass("java.io.File"); + if (!new File(path).exists()) throw new Error("找不到所选文件"); + resolve(readTextByNativePath(p, path)); + return; + } const main = p.android.runtimeMainActivity(); const Uri = p.android.importClass("android.net.Uri"); const uri = Uri.parse(uriStr); const resolver = main.getContentResolver(); const is = resolver.openInputStream(uri); - const BufferedReader = p.android.importClass("java.io.BufferedReader"); - const InputStreamReader = p.android.importClass( - "java.io.InputStreamReader", - ); - const reader = new BufferedReader(new InputStreamReader(is)); - let out = ""; - let line = reader.readLine(); - while (line != null) { - out += line; - line = reader.readLine(); - if (line != null) out += "\n"; - } - reader.close(); - resolve(out); + resolve(readTextByStream(p, is)); } catch (e) { reject(e instanceof Error ? e : new Error("读取所选文件失败")); } diff --git a/uniapp/security/unpackage/res/icons/1024x1024.png b/uniapp/security/unpackage/res/icons/1024x1024.png new file mode 100644 index 0000000..bd9f2fe Binary files /dev/null and b/uniapp/security/unpackage/res/icons/1024x1024.png differ diff --git a/uniapp/security/unpackage/res/icons/120x120.png b/uniapp/security/unpackage/res/icons/120x120.png new file mode 100644 index 0000000..de13984 Binary files /dev/null and b/uniapp/security/unpackage/res/icons/120x120.png differ diff --git a/uniapp/security/unpackage/res/icons/144x144.png b/uniapp/security/unpackage/res/icons/144x144.png new file mode 100644 index 0000000..9d86a12 Binary files /dev/null and b/uniapp/security/unpackage/res/icons/144x144.png differ diff --git a/uniapp/security/unpackage/res/icons/152x152.png b/uniapp/security/unpackage/res/icons/152x152.png new file mode 100644 index 0000000..ace6341 Binary files /dev/null and b/uniapp/security/unpackage/res/icons/152x152.png differ diff --git a/uniapp/security/unpackage/res/icons/167x167.png b/uniapp/security/unpackage/res/icons/167x167.png new file mode 100644 index 0000000..33f7af7 Binary files /dev/null and b/uniapp/security/unpackage/res/icons/167x167.png differ diff --git a/uniapp/security/unpackage/res/icons/180x180.png b/uniapp/security/unpackage/res/icons/180x180.png new file mode 100644 index 0000000..a90ecd6 Binary files /dev/null and b/uniapp/security/unpackage/res/icons/180x180.png differ diff --git a/uniapp/security/unpackage/res/icons/192x192.png b/uniapp/security/unpackage/res/icons/192x192.png new file mode 100644 index 0000000..5e20f37 Binary files /dev/null and b/uniapp/security/unpackage/res/icons/192x192.png differ diff --git a/uniapp/security/unpackage/res/icons/20x20.png b/uniapp/security/unpackage/res/icons/20x20.png new file mode 100644 index 0000000..70337fe Binary files /dev/null and b/uniapp/security/unpackage/res/icons/20x20.png differ diff --git a/uniapp/security/unpackage/res/icons/29x29.png b/uniapp/security/unpackage/res/icons/29x29.png new file mode 100644 index 0000000..a3799c5 Binary files /dev/null and b/uniapp/security/unpackage/res/icons/29x29.png differ diff --git a/uniapp/security/unpackage/res/icons/40x40.png b/uniapp/security/unpackage/res/icons/40x40.png new file mode 100644 index 0000000..9ac0f40 Binary files /dev/null and b/uniapp/security/unpackage/res/icons/40x40.png differ diff --git a/uniapp/security/unpackage/res/icons/58x58.png b/uniapp/security/unpackage/res/icons/58x58.png new file mode 100644 index 0000000..0986840 Binary files /dev/null and b/uniapp/security/unpackage/res/icons/58x58.png differ diff --git a/uniapp/security/unpackage/res/icons/60x60.png b/uniapp/security/unpackage/res/icons/60x60.png new file mode 100644 index 0000000..50741b7 Binary files /dev/null and b/uniapp/security/unpackage/res/icons/60x60.png differ diff --git a/uniapp/security/unpackage/res/icons/72x72.png b/uniapp/security/unpackage/res/icons/72x72.png new file mode 100644 index 0000000..25806f5 Binary files /dev/null and b/uniapp/security/unpackage/res/icons/72x72.png differ diff --git a/uniapp/security/unpackage/res/icons/76x76.png b/uniapp/security/unpackage/res/icons/76x76.png new file mode 100644 index 0000000..2747dcf Binary files /dev/null and b/uniapp/security/unpackage/res/icons/76x76.png differ diff --git a/uniapp/security/unpackage/res/icons/80x80.png b/uniapp/security/unpackage/res/icons/80x80.png new file mode 100644 index 0000000..81710c0 Binary files /dev/null and b/uniapp/security/unpackage/res/icons/80x80.png differ diff --git a/uniapp/security/unpackage/res/icons/87x87.png b/uniapp/security/unpackage/res/icons/87x87.png new file mode 100644 index 0000000..897f5ee Binary files /dev/null and b/uniapp/security/unpackage/res/icons/87x87.png differ diff --git a/uniapp/security/unpackage/res/icons/96x96.png b/uniapp/security/unpackage/res/icons/96x96.png new file mode 100644 index 0000000..205aac6 Binary files /dev/null and b/uniapp/security/unpackage/res/icons/96x96.png differ