init: init proj

This commit is contained in:
2025-10-19 12:32:16 +08:00
commit 21c23d9400
52 changed files with 2924 additions and 0 deletions

12
.gitignore vendored Normal file
View File

@@ -0,0 +1,12 @@
/node_modules
/oh_modules
/local.properties
/.idea
**/build
/.hvigor
.cxx
/.clangd
/.clang-format
/.clang-tidy
**/.test
/.appanalyzer

10
AppScope/app.json5 Normal file
View File

@@ -0,0 +1,10 @@
{
"app": {
"bundleName": "com.njcqit.authenticator",
"vendor": "Nanjing Changqing Information Technology Co., Ltd.",
"versionCode": 1000000,
"versionName": "1.0.0",
"icon": "$media:layered_image",
"label": "$string:app_name"
}
}

View File

@@ -0,0 +1,8 @@
{
"string": [
{
"name": "app_name",
"value": "Authenticator"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 874 KiB

View File

@@ -0,0 +1,7 @@
{
"layered-image":
{
"background" : "$media:background",
"foreground" : "$media:foreground"
}
}

160
CLAUDE.md Normal file
View File

@@ -0,0 +1,160 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## 项目概述
这是一个 **MFA Authenticator (多因素身份验证器)** HarmonyOS 应用程序,使用 ArkTS (扩展的 TypeScript) 和 ArkUI 框架开发。应用实现了基于时间的一次性密码(TOTP)算法,用于生成双因素认证验证码。
### 核心功能
- TOTP 验证码生成(30秒自动刷新)
- 账户管理(添加、编辑、删除)
- 本地加密存储
- 搜索功能
- 暗色模式支持
- Apple Design 风格的简洁 UI
## 构建系统
项目使用 Hvigor 作为构建工具:
- **构建命令**: 通过 DevEco Studio 或 `hvigorw` 脚本构建
- **清理**: `hvigorw clean`
- **构建**: `hvigorw assembleHap`
- **调试构建**: `hvigorw --mode debug assembleHap`
- **发布构建**: `hvigorw --mode release assembleHap`
## 测试
项目包含两种测试类型:
- **本地单元测试**: `entry/src/test/` - 使用 Hypium 框架
- **设备测试**: `entry/src/ohosTest/` - 需要在设备或模拟器上运行
- **运行测试**: 通过 DevEco Studio 的测试配置运行
依赖的测试框架:
- `@ohos/hypium`: 单元测试框架
- `@ohos/hamock`: Mock 工具
## 代码架构
### 模块结构
```
entry/src/main/ets/
├── common/
│ ├── constants/
│ │ └── AppConstants.ets # 应用常量(TOTP周期、UI常量等)
│ └── utils/
│ ├── Base32.ets # Base32 编解码工具
│ ├── TOTPGenerator.ets # TOTP 算法实现
│ └── StorageUtil.ets # 本地存储工具(Preferences)
├── model/
│ └── Account.ets # 账户数据模型
├── viewmodel/
│ └── AccountViewModel.ets # 账户业务逻辑
├── view/
│ └── components/
│ ├── AccountCard.ets # 账户卡片组件
│ └── ProgressRing.ets # 圆形进度指示器
├── pages/
│ ├── AccountListPage.ets # 主页面(账户列表)
│ ├── AddAccountPage.ets # 添加账户页面
│ └── AccountDetailPage.ets # 账户详情/编辑页面
├── entryability/
│ └── EntryAbility.ets # 应用入口
└── entrybackupability/
└── EntryBackupAbility.ets # 备份恢复功能
```
### 核心模块说明
#### 1. TOTP 算法实现
- **TOTPGenerator.ets**: 基于 RFC 6238 标准实现
- 使用 HarmonyOS 的 `cryptoFramework` 生成 HMAC-SHA1
- 支持自定义时间步长和验证码位数
- 提供进度计算方法
#### 2. 数据存储
- **StorageUtil.ets**: 使用 Preferences API 进行持久化
- **Account Model**: JSON 序列化存储账户信息
- 支持加密存储(未来可扩展)
#### 3. UI 组件
- **AccountCard**: 显示账户信息、验证码、进度环
- 点击复制验证码
- 长按显示操作菜单
- **ProgressRing**: 圆形进度指示器,显示倒计时
#### 4. 页面结构
1. **AccountListPage** (主页)
- 账户列表展示
- 搜索功能
- 定时器自动刷新验证码
2. **AddAccountPage** (添加账户)
- Tab 切换:扫码/手动输入
- 表单验证
3. **AccountDetailPage** (编辑账户)
- 编辑发行者和账户名
- 查看密钥(可切换显示/隐藏)
### ArkTS 开发要点
- **文件扩展名**: `.ets` (ArkTS 文件)
- **UI 组件**: 使用声明式语法,如 `Text`, `RelativeContainer`, `Button`
- **状态管理**: 使用装饰器 `@State`, `@Prop`, `@Link`
- **日志输出**: 使用 `hilog` API,格式: `hilog.info(DOMAIN, tag, message)`
- **资源引用**: 使用 `$r('app.type.name')` 格式,如 `$r('app.string.EntryAbility_label')`
### 配置文件
- **build-profile.json5**: 应用构建配置,包含签名、目标 SDK 版本等
- **module.json5**: 模块配置,定义 abilities、页面、权限等
- **oh-package.json5**: 依赖包管理
- **code-linter.json5**: 代码检查规则
## SDK 版本
- **目标 SDK**: HarmonyOS 6.0.0(20)
- **兼容 SDK**: HarmonyOS 6.0.0(20)
- **运行时**: HarmonyOS
## UI 设计规范
### 配色方案(Apple Design 风格)
- **主色**: `#007AFF` (浅色) / `#0A84FF` (深色)
- **背景色**: `#F2F2F7` (浅色) / `#000000` (深色)
- **卡片背景**: `#FFFFFF` (浅色) / `#1C1C1E` (深色)
- **文字颜色**: 主要 / 次要 / 三级 (响应式暗色模式)
### 设计元素
- **圆角**: 12-16vp
- **间距**: 8/12/16/24vp 系统
- **字体大小**:
- 标题: 28-32fp
- 正文: 15-17fp
- 验证码: 36fp (等宽字体)
- 辅助文字: 13fp
### 交互规范
- 卡片点击: 复制验证码
- 长按: 显示操作菜单
- 进度环: 30秒倒计时动画
## 注意事项
- 所有源代码文件使用 `.ets` 扩展名
- 页面路径在 `entry/src/main/resources/base/profile/main_pages.json` 中注册
- 使用 `@kit.*` 导入系统能力,如 `@kit.AbilityKit`, `@kit.ArkUI`
- 日志使用统一的 `AppConstants.LOG_DOMAIN``AppConstants.LOG_TAG`
- TOTP 密钥必须是 Base32 格式
- 应用支持自动暗色模式切换
## 开发建议
- 在 DevEco Studio 中打开项目进行开发和调试
- 使用 HarmonyOS 模拟器或真机进行测试
- 验证码刷新使用定时器,注意在页面销毁时清理定时器
- 添加新页面时记得更新 `main_pages.json`

119
README.md Normal file
View File

@@ -0,0 +1,119 @@
# MFA Authenticator
一个基于 HarmonyOS 的多因素身份验证器应用,使用 Apple Design 风格设计。
## 功能特性
-**TOTP 验证码生成**: 基于 RFC 6238 标准的时间一次性密码算法
-**30秒自动刷新**: 实时显示剩余时间和进度
-**账户管理**: 添加、编辑、删除账户
-**搜索功能**: 快速查找账户
-**一键复制**: 点击卡片即可复制验证码
-**本地存储**: 使用 Preferences API 安全存储
-**暗色模式**: 自动适配系统主题
-**简洁 UI**: Apple Design 风格,清爽高效
## 技术栈
- **开发框架**: ArkTS + ArkUI
- **平台**: HarmonyOS 6.0.0
- **架构模式**: MVVM
- **加密算法**: HMAC-SHA1/SHA256
- **存储方案**: Preferences API
## 项目结构
```
entry/src/main/ets/
├── common/
│ ├── constants/ # 常量定义
│ └── utils/ # 工具类(TOTP、Base32、存储)
├── model/ # 数据模型
├── viewmodel/ # 业务逻辑
├── view/
│ └── components/ # 可复用组件
└── pages/ # 页面
├── AccountListPage.ets # 主页面
├── AddAccountPage.ets # 添加账户
└── AccountDetailPage.ets # 账户详情
```
## 快速开始
### 环境要求
- DevEco Studio 5.0+
- HarmonyOS SDK 6.0.0+
### 安装步骤
1. 使用 DevEco Studio 打开项目
2. 等待依赖安装完成
3. 连接 HarmonyOS 设备或启动模拟器
4. 点击运行按钮
### 使用说明
1. **添加账户**
- 点击右上角 "+" 按钮
- 选择"手动输入"标签
- 填写发行者、账户名和密钥(Base32 格式)
- 点击"保存"
2. **查看验证码**
- 主页面显示所有账户和对应的验证码
- 验证码每 30 秒自动刷新
- 右侧圆环显示倒计时进度
3. **复制验证码**
- 点击账户卡片即可复制验证码到剪贴板
4. **管理账户**
- 长按账户卡片显示操作菜单
- 可选择"编辑"或"删除"
## 测试账户
可以使用以下测试密钥进行测试:
```
发行者: Test
账户: test@example.com
密钥: JBSWY3DPEHPK3PXP
```
## 设计规范
### 配色方案
- 主色: SF Blue (#007AFF)
- 背景: 浅灰 (#F2F2F7) / 深色 (#000000)
- 卡片: 白色 (#FFFFFF) / 深色卡片 (#1C1C1E)
### UI 元素
- 圆角: 12-16vp
- 验证码字体: 36fp 等宽字体
- 间距系统: 8/12/16/24vp
## 安全说明
- 所有账户数据仅存储在本地设备
- 使用 Preferences API 进行持久化存储
- 密钥采用 Base32 编码存储
- 不会上传任何数据到云端
## 后续计划
- [ ] 扫码功能实现
- [ ] 数据加密存储
- [ ] 应用锁功能
- [ ] 备份与恢复
- [ ] 更多算法支持(SHA256, SHA512)
- [ ] 批量导入导出
## 许可证
MIT License
## 贡献
欢迎提交 Issue 和 Pull Request!

56
build-profile.json5 Normal file
View File

@@ -0,0 +1,56 @@
{
"app": {
"signingConfigs": [
{
"name": "default",
"type": "HarmonyOS",
"material": {
"storeFile": "/Users/kid/Development/HarmonyOS/hm.p12",
"storePassword": "0000001B30243D260315C67D6B170F40AD52B72CE7FA0F8712F80BA535A6B127D9950927D0C1048EBD21F3",
"keyAlias": "default",
"keyPassword": "0000001BDE4BC6A4AC112DDF9966DAD89C84D0BF5244796A206B8E682A0F72CE4168D11BF41917DF95B227",
"signAlg": "SHA256withECDSA",
"profile": "/Users/kid/Downloads/ReleaseRelease.p7b",
"certpath": "/Users/kid/Downloads/Release.cer"
}
}
],
"products": [
{
"name": "default",
"signingConfig": "default",
"targetSdkVersion": "6.0.0(20)",
"compatibleSdkVersion": "6.0.0(20)",
"runtimeOS": "HarmonyOS",
"buildOption": {
"strictMode": {
"caseSensitiveCheck": true,
"useNormalizedOHMUrl": true
}
}
}
],
"buildModeSet": [
{
"name": "debug",
},
{
"name": "release"
}
]
},
"modules": [
{
"name": "entry",
"srcPath": "./entry",
"targets": [
{
"name": "default",
"applyToProducts": [
"default"
]
}
]
}
]
}

32
code-linter.json5 Normal file
View File

@@ -0,0 +1,32 @@
{
"files": [
"**/*.ets"
],
"ignore": [
"**/src/ohosTest/**/*",
"**/src/test/**/*",
"**/src/mock/**/*",
"**/node_modules/**/*",
"**/oh_modules/**/*",
"**/build/**/*",
"**/.preview/**/*"
],
"ruleSet": [
"plugin:@performance/recommended",
"plugin:@typescript-eslint/recommended"
],
"rules": {
"@security/no-unsafe-aes": "error",
"@security/no-unsafe-hash": "error",
"@security/no-unsafe-mac": "warn",
"@security/no-unsafe-dh": "error",
"@security/no-unsafe-dsa": "error",
"@security/no-unsafe-ecdsa": "error",
"@security/no-unsafe-rsa-encrypt": "error",
"@security/no-unsafe-rsa-sign": "error",
"@security/no-unsafe-rsa-key": "error",
"@security/no-unsafe-dsa-key": "error",
"@security/no-unsafe-dh-key": "error",
"@security/no-unsafe-3des": "error"
}
}

6
entry/.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
/node_modules
/oh_modules
/.preview
/build
/.cxx
/.test

33
entry/build-profile.json5 Normal file
View File

@@ -0,0 +1,33 @@
{
"apiType": "stageMode",
"buildOption": {
"resOptions": {
"copyCodeResource": {
"enable": false
}
}
},
"buildOptionSet": [
{
"name": "release",
"arkOptions": {
"obfuscation": {
"ruleOptions": {
"enable": false,
"files": [
"./obfuscation-rules.txt"
]
}
}
}
},
],
"targets": [
{
"name": "default"
},
{
"name": "ohosTest",
}
]
}

6
entry/hvigorfile.ts Normal file
View File

@@ -0,0 +1,6 @@
import { hapTasks } from '@ohos/hvigor-ohos-plugin';
export default {
system: hapTasks, /* Built-in plugin of Hvigor. It cannot be modified. */
plugins: [] /* Custom plugin to extend the functionality of Hvigor. */
}

View File

@@ -0,0 +1,23 @@
# Define project specific obfuscation rules here.
# You can include the obfuscation configuration files in the current module's build-profile.json5.
#
# For more details, see
# https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/source-obfuscation-V5
# Obfuscation options:
# -disable-obfuscation: disable all obfuscations
# -enable-property-obfuscation: obfuscate the property names
# -enable-toplevel-obfuscation: obfuscate the names in the global scope
# -compact: remove unnecessary blank spaces and all line feeds
# -remove-log: remove all console.* statements
# -print-namecache: print the name cache that contains the mapping from the old names to new names
# -apply-namecache: reuse the given cache file
# Keep options:
# -keep-property-name: specifies property names that you want to keep
# -keep-global-name: specifies names that you want to keep in the global scope
-enable-property-obfuscation
-enable-toplevel-obfuscation
-enable-filename-obfuscation
-enable-export-obfuscation

10
entry/oh-package.json5 Normal file
View File

@@ -0,0 +1,10 @@
{
"name": "authenticator",
"version": "1.0.0",
"description": "MFA/2FA Authenticator App",
"main": "",
"author": "",
"license": "",
"dependencies": {}
}

View File

@@ -0,0 +1,27 @@
/**
* 应用常量定义
*/
export class AppConstants {
// TOTP 配置
static readonly TOTP_PERIOD: number = 30; // 30秒刷新周期
static readonly TOTP_DIGITS: number = 6; // 6位验证码
static readonly TOTP_ALGORITHM: string = 'SHA1'; // 默认算法
// 存储键名
static readonly PREFERENCES_NAME: string = 'authenticator_prefs';
static readonly KEY_ACCOUNTS: string = 'accounts';
static readonly KEY_ENCRYPTION_KEY: string = 'encryption_key';
// 日志标签
static readonly LOG_TAG: string = 'Authenticator';
static readonly LOG_DOMAIN: number = 0x0000;
// UI 常量
static readonly CARD_BORDER_RADIUS: number = 16;
static readonly CARD_PADDING: number = 16;
static readonly LIST_SPACING: number = 12;
static readonly PAGE_PADDING: number = 16;
// 动画时长
static readonly ANIMATION_DURATION: number = 300;
}

View File

@@ -0,0 +1,93 @@
/**
* Base32 编解码工具类
* 用于处理 TOTP 密钥的编解码
*/
class Base32Class {
private readonly CHARSET: string = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
/**
* Base32 解码
* @param encoded Base32 编码的字符串
* @returns 解码后的字节数组
*/
decode(encoded: string): Uint8Array {
// 移除空格和转换为大写
encoded = encoded.replace(/\s/g, '').toUpperCase();
if (encoded.length === 0) {
return new Uint8Array(0);
}
// 计算输出长度
const outputLength = Math.floor(encoded.length * 5 / 8);
const result = new Uint8Array(outputLength);
let buffer = 0;
let bitsLeft = 0;
let index = 0;
for (let i = 0; i < encoded.length; i++) {
const char = encoded.charAt(i);
// 忽略填充字符
if (char === '=') {
break;
}
const value = this.CHARSET.indexOf(char);
if (value === -1) {
throw new Error(`Invalid Base32 character: ${char}`);
}
buffer = (buffer << 5) | value;
bitsLeft += 5;
if (bitsLeft >= 8) {
result[index++] = (buffer >> (bitsLeft - 8)) & 0xFF;
bitsLeft -= 8;
}
}
return result;
}
/**
* Base32 编码
* @param data 字节数组
* @returns Base32 编码的字符串
*/
encode(data: Uint8Array): string {
if (data.length === 0) {
return '';
}
let result = '';
let buffer = 0;
let bitsLeft = 0;
for (let i = 0; i < data.length; i++) {
buffer = (buffer << 8) | data[i];
bitsLeft += 8;
while (bitsLeft >= 5) {
const index = (buffer >> (bitsLeft - 5)) & 0x1F;
result += this.CHARSET.charAt(index);
bitsLeft -= 5;
}
}
if (bitsLeft > 0) {
const index = (buffer << (5 - bitsLeft)) & 0x1F;
result += this.CHARSET.charAt(index);
}
// 添加填充
while (result.length % 8 !== 0) {
result += '=';
}
return result;
}
}
export const Base32 = new Base32Class();

View File

@@ -0,0 +1,118 @@
import { preferences } from '@kit.ArkData';
import { AppConstants } from '../constants/AppConstants';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { BusinessError } from '@kit.BasicServicesKit';
/**
* 数据存储工具类
* 使用 Preferences 进行持久化存储
*/
class StorageUtilClass {
private dataPreferences: preferences.Preferences | null = null;
private static instance: StorageUtilClass | null = null;
private constructor() {
}
static getInstance(): StorageUtilClass {
if (StorageUtilClass.instance === null) {
StorageUtilClass.instance = new StorageUtilClass();
}
return StorageUtilClass.instance;
}
/**
* 初始化存储
* @param context 应用上下文
*/
async init(context: Context): Promise<void> {
try {
this.dataPreferences = await preferences.getPreferences(context, AppConstants.PREFERENCES_NAME);
hilog.info(AppConstants.LOG_DOMAIN, AppConstants.LOG_TAG, 'Storage initialized successfully');
} catch (err) {
const error = err as BusinessError;
hilog.error(AppConstants.LOG_DOMAIN, AppConstants.LOG_TAG, 'Failed to init storage: %{public}s', JSON.stringify(error));
throw new Error(`Failed to init storage: ${error.message}`);
}
}
/**
* 保存字符串数据
* @param key 键
* @param value 值
*/
async putString(key: string, value: string): Promise<void> {
if (!this.dataPreferences) {
throw new Error('Storage not initialized');
}
try {
await this.dataPreferences.put(key, value);
await this.dataPreferences.flush();
} catch (err) {
const error = err as BusinessError;
hilog.error(AppConstants.LOG_DOMAIN, AppConstants.LOG_TAG, 'Failed to save data: %{public}s', JSON.stringify(error));
throw new Error(`Failed to save data: ${error.message}`);
}
}
/**
* 获取字符串数据
* @param key 键
* @param defaultValue 默认值
* @returns 数据值
*/
async getString(key: string, defaultValue: string = ''): Promise<string> {
if (!this.dataPreferences) {
throw new Error('Storage not initialized');
}
try {
const value = await this.dataPreferences.get(key, defaultValue);
return value as string;
} catch (err) {
const error = err as BusinessError;
hilog.error(AppConstants.LOG_DOMAIN, AppConstants.LOG_TAG, 'Failed to get data: %{public}s', JSON.stringify(error));
return defaultValue;
}
}
/**
* 删除数据
* @param key 键
*/
async remove(key: string): Promise<void> {
if (!this.dataPreferences) {
throw new Error('Storage not initialized');
}
try {
await this.dataPreferences.delete(key);
await this.dataPreferences.flush();
} catch (err) {
const error = err as BusinessError;
hilog.error(AppConstants.LOG_DOMAIN, AppConstants.LOG_TAG, 'Failed to remove data: %{public}s', JSON.stringify(error));
throw new Error(`Failed to remove data: ${error.message}`);
}
}
/**
* 清空所有数据
*/
async clear(): Promise<void> {
if (!this.dataPreferences) {
throw new Error('Storage not initialized');
}
try {
await this.dataPreferences.clear();
await this.dataPreferences.flush();
} catch (err) {
const error = err as BusinessError;
hilog.error(AppConstants.LOG_DOMAIN, AppConstants.LOG_TAG, 'Failed to clear data: %{public}s', JSON.stringify(error));
throw new Error(`Failed to clear data: ${error.message}`);
}
}
}
export const StorageUtil = StorageUtilClass.getInstance();

View File

@@ -0,0 +1,127 @@
import { cryptoFramework } from '@kit.CryptoArchitectureKit';
import { Base32 } from './Base32';
import { AppConstants } from '../constants/AppConstants';
import { BusinessError } from '@kit.BasicServicesKit';
/**
* TOTP (Time-based One-Time Password) 生成器
* 基于 RFC 6238 标准实现
*/
class TOTPGeneratorClass {
/**
* 生成 TOTP 验证码
* @param secret Base32 编码的密钥
* @param timestamp 时间戳(秒),默认为当前时间
* @param period 时间步长(秒),默认30秒
* @param digits 验证码位数,默认6位
* @returns TOTP 验证码字符串
*/
async generate(
secret: string,
timestamp?: number,
period: number = AppConstants.TOTP_PERIOD,
digits: number = AppConstants.TOTP_DIGITS
): Promise<string> {
// 使用当前时间或指定时间
const time = timestamp || Math.floor(Date.now() / 1000);
// 计算时间步数
const timeStep = Math.floor(time / period);
// 解码 Base32 密钥
const keyBytes = Base32.decode(secret);
// 生成 HMAC
const hmac = await this.generateHMAC(keyBytes, timeStep);
// 动态截断
const code = this.truncate(hmac, digits);
// 填充前导零
return code.toString().padStart(digits, '0');
}
/**
* 生成 HMAC-SHA1
* @param key 密钥字节数组
* @param counter 计数器值
* @returns HMAC 字节数组
*/
private async generateHMAC(key: Uint8Array, counter: number): Promise<Uint8Array> {
// 将计数器转换为 8 字节大端序
const counterBytes = new Uint8Array(8);
for (let i = 7; i >= 0; i--) {
counterBytes[i] = counter & 0xFF;
counter >>= 8;
}
try {
// 创建 HMAC 生成器
const macAlg = 'SHA1';
const mac = cryptoFramework.createMac(macAlg);
// 创建对称密钥
const keyBlob: cryptoFramework.DataBlob = { data: key };
const symKeyGenerator = cryptoFramework.createSymKeyGenerator('HMAC');
const symKey = await symKeyGenerator.convertKey(keyBlob);
// 初始化 MAC
await mac.init(symKey);
// 更新数据并生成 MAC
const dataBlob: cryptoFramework.DataBlob = { data: counterBytes };
await mac.update(dataBlob);
const result = await mac.doFinal();
return result.data;
} catch (err) {
const error = err as BusinessError;
console.error(`${AppConstants.LOG_TAG}: HMAC generation failed`, JSON.stringify(error));
throw new Error(`HMAC generation failed: ${error.message}`);
}
}
/**
* 动态截断
* @param hmac HMAC 字节数组
* @param digits 验证码位数
* @returns 截断后的数字验证码
*/
private truncate(hmac: Uint8Array, digits: number): number {
// 获取偏移量(最后一个字节的低4位)
const offset = hmac[hmac.length - 1] & 0x0F;
// 提取 4 字节并转换为 31 位整数
const binary =
((hmac[offset] & 0x7F) << 24) |
((hmac[offset + 1] & 0xFF) << 16) |
((hmac[offset + 2] & 0xFF) << 8) |
(hmac[offset + 3] & 0xFF);
// 取模得到验证码
const divisor = Math.pow(10, digits);
return binary % divisor;
}
/**
* 获取当前时间步的剩余秒数
* @param period 时间步长(秒)
* @returns 剩余秒数
*/
getRemainingSeconds(period: number = AppConstants.TOTP_PERIOD): number {
const now = Math.floor(Date.now() / 1000);
return period - (now % period);
}
/**
* 获取当前时间步的进度(0-1)
* @param period 时间步长(秒)
* @returns 进度值 (从满到空: 1.0 → 0.0)
*/
getProgress(period: number = AppConstants.TOTP_PERIOD): number {
const remaining = this.getRemainingSeconds(period);
return remaining / period;
}
}
export const TOTPGenerator = new TOTPGeneratorClass();

View File

@@ -0,0 +1,114 @@
/**
* TOTP URI 解析工具
* 解析标准 TOTP URI 格式: otpauth://totp/Label?secret=SECRET&issuer=ISSUER
*/
export interface TOTPUriData {
issuer: string;
accountName: string;
secret: string;
algorithm?: string;
digits?: number;
period?: number;
}
class TOTPUriParserClass {
/**
* 解析 TOTP URI
* @param uri TOTP URI 字符串
* @returns 解析后的数据对象,解析失败返回 null
*/
parse(uri: string): TOTPUriData | null {
try {
// 检查URI格式
if (!uri.startsWith('otpauth://totp/')) {
return null;
}
// 移除协议前缀
const uriWithoutProtocol = uri.substring('otpauth://totp/'.length);
// 分割标签和参数
const parts = uriWithoutProtocol.split('?');
if (parts.length < 2) {
return null;
}
// 解析标签(Label)
const label = decodeURIComponent(parts[0]);
let issuer = '';
let accountName = '';
// 标签格式: "Issuer:Account" 或 "Account"
if (label.includes(':')) {
const labelParts = label.split(':');
issuer = labelParts[0].trim();
accountName = labelParts.slice(1).join(':').trim();
} else {
accountName = label.trim();
}
// 解析查询参数
const params = this.parseQueryParams(parts[1]);
// 获取必需参数
const secret = params['secret'];
if (!secret) {
return null;
}
// 获取可选参数
if (params['issuer'] && !issuer) {
issuer = params['issuer'];
}
const algorithm = params['algorithm'] || 'SHA1';
const digits = params['digits'] ? parseInt(params['digits']) : 6;
const period = params['period'] ? parseInt(params['period']) : 30;
return {
issuer,
accountName,
secret,
algorithm,
digits,
period
};
} catch (error) {
console.error('Failed to parse TOTP URI:', error);
return null;
}
}
/**
* 解析查询参数
* @param queryString 查询字符串
* @returns 参数对象
*/
private parseQueryParams(queryString: string): Record<string, string> {
const params: Record<string, string> = {};
const pairs = queryString.split('&');
for (const pair of pairs) {
const parts = pair.split('=');
const key = parts[0];
const value = parts[1];
if (key && value) {
params[decodeURIComponent(key)] = decodeURIComponent(value);
}
}
return params;
}
/**
* 验证 URI 格式
* @param uri URI 字符串
* @returns 是否为有效的 TOTP URI
*/
isValidTOTPUri(uri: string): boolean {
return uri.startsWith('otpauth://totp/') && uri.includes('secret=');
}
}
export const TOTPUriParser = new TOTPUriParserClass();

View File

@@ -0,0 +1,48 @@
import { AbilityConstant, ConfigurationConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';
const DOMAIN = 0x0000;
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
try {
this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET);
} catch (err) {
hilog.error(DOMAIN, 'testTag', 'Failed to set colorMode. Cause: %{public}s', JSON.stringify(err));
}
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onCreate');
}
onDestroy(): void {
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onDestroy');
}
onWindowStageCreate(windowStage: window.WindowStage): void {
// Main window is created, set main page for this ability
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageCreate');
windowStage.loadContent('pages/AccountListPage', (err) => {
if (err.code) {
hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
return;
}
hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.');
});
}
onWindowStageDestroy(): void {
// Main window is destroyed, release UI related resources
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageDestroy');
}
onForeground(): void {
// Ability has brought to foreground
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onForeground');
}
onBackground(): void {
// Ability has back to background
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onBackground');
}
}

View File

@@ -0,0 +1,16 @@
import { hilog } from '@kit.PerformanceAnalysisKit';
import { BackupExtensionAbility, BundleVersion } from '@kit.CoreFileKit';
const DOMAIN = 0x0000;
export default class EntryBackupAbility extends BackupExtensionAbility {
async onBackup() {
hilog.info(DOMAIN, 'testTag', 'onBackup ok');
await Promise.resolve();
}
async onRestore(bundleVersion: BundleVersion) {
hilog.info(DOMAIN, 'testTag', 'onRestore ok %{public}s', JSON.stringify(bundleVersion));
await Promise.resolve();
}
}

View File

@@ -0,0 +1,119 @@
/**
* 账户 JSON 接口
*/
export interface AccountJson {
id: string;
issuer: string;
accountName: string;
secret: string;
algorithm: string;
digits: number;
period: number;
icon?: string;
createdAt: number;
updatedAt: number;
}
/**
* 账户数据模型
*/
export class Account {
id: string; // 唯一标识
issuer: string; // 发行者(如 Google, GitHub)
accountName: string; // 账户名称(如邮箱或用户名)
secret: string; // Base32 编码的密钥
algorithm: string; // 算法类型: SHA1, SHA256, SHA512
digits: number; // 验证码位数: 6 或 8
period: number; // 时间步长(秒): 通常为 30
icon?: string; // 图标(可选)
createdAt: number; // 创建时间戳
updatedAt: number; // 更新时间戳
constructor(
issuer: string,
accountName: string,
secret: string,
algorithm: string = 'SHA1',
digits: number = 6,
period: number = 30
) {
this.id = this.generateId();
this.issuer = issuer;
this.accountName = accountName;
this.secret = secret;
this.algorithm = algorithm;
this.digits = digits;
this.period = period;
this.createdAt = Date.now();
this.updatedAt = Date.now();
}
/**
* 生成唯一 ID
*/
private generateId(): string {
return `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
}
/**
* 获取显示名称
*/
getDisplayName(): string {
if (this.issuer && this.accountName) {
return `${this.issuer} (${this.accountName})`;
}
return this.issuer || this.accountName || 'Unknown';
}
/**
* 从 JSON 对象创建 Account 实例
*/
static fromJson(json: AccountJson): Account {
const account = new Account(
json.issuer,
json.accountName,
json.secret,
json.algorithm,
json.digits,
json.period
);
account.id = json.id;
if (json.icon !== undefined) {
account.icon = json.icon;
}
account.createdAt = json.createdAt;
account.updatedAt = json.updatedAt;
return account;
}
/**
* 转换为 JSON 对象
*/
toJson(): AccountJson {
const json: AccountJson = {
id: this.id,
issuer: this.issuer,
accountName: this.accountName,
secret: this.secret,
algorithm: this.algorithm,
digits: this.digits,
period: this.period,
createdAt: this.createdAt,
updatedAt: this.updatedAt
};
if (this.icon !== undefined) {
json.icon = this.icon;
}
return json;
}
/**
* 更新账户信息
*/
update(issuer?: string, accountName?: string, secret?: string): void {
if (issuer !== undefined) this.issuer = issuer;
if (accountName !== undefined) this.accountName = accountName;
if (secret !== undefined) this.secret = secret;
this.updatedAt = Date.now();
}
}

View File

@@ -0,0 +1,248 @@
import { router } from '@kit.ArkUI';
import { AccountViewModel } from '../viewmodel/AccountViewModel';
import { Account } from '../model/Account';
import { StorageUtil } from '../common/utils/StorageUtil';
import { promptAction } from '@kit.ArkUI';
/**
* 账户详情/编辑页面
*/
@Entry
@Component
struct AccountDetailPage {
@State issuer: string = '';
@State accountName: string = '';
@State secretKey: string = '';
@State issuerError: string = '';
@State accountNameError: string = '';
@State secretKeyError: string = '';
@State showSecretKey: boolean = false;
private viewModel: AccountViewModel = new AccountViewModel();
private accountId: string = '';
private account?: Account;
async aboutToAppear(): Promise<void> {
// 初始化存储
await StorageUtil.init(getContext(this));
// 加载账户数据
await this.viewModel.loadAccounts();
// 获取路由参数
const params = router.getParams() as Record<string, string>;
this.accountId = params['accountId'] || '';
// 加载账户信息
if (this.accountId) {
this.account = this.viewModel.getAccountById(this.accountId);
if (this.account) {
this.issuer = this.account.issuer;
this.accountName = this.account.accountName;
this.secretKey = this.account.secret;
}
}
}
/**
* 验证输入
*/
private validate(): boolean {
let isValid = true;
// 验证发行者
if (!this.issuer.trim()) {
this.issuerError = '此字段不能为空';
isValid = false;
} else {
this.issuerError = '';
}
// 验证账户名
if (!this.accountName.trim()) {
this.accountNameError = '此字段不能为空';
isValid = false;
} else {
this.accountNameError = '';
}
return isValid;
}
/**
* 保存更改
*/
private async saveChanges(): Promise<void> {
if (!this.validate()) {
return;
}
try {
await this.viewModel.updateAccount(
this.accountId,
this.issuer.trim(),
this.accountName.trim()
);
promptAction.showToast({
message: '保存成功',
duration: 1500
});
// 返回上一页
router.back();
} catch (error) {
promptAction.showToast({
message: '保存失败',
duration: 1500
});
}
}
build() {
Column() {
// 导航栏
Row() {
Button({ type: ButtonType.Normal }) {
Text($r('app.string.cancel'))
.fontSize($r('app.float.body_large'))
.fontColor($r('app.color.primary_color'))
}
.backgroundColor(Color.Transparent)
.onClick(() => {
router.back();
})
Blank()
Text($r('app.string.edit_account'))
.fontSize($r('app.float.body_large'))
.fontColor($r('app.color.text_primary'))
.fontWeight(FontWeight.Medium)
Blank()
Button({ type: ButtonType.Normal }) {
Text($r('app.string.save'))
.fontSize($r('app.float.body_large'))
.fontColor($r('app.color.primary_color'))
.fontWeight(FontWeight.Medium)
}
.backgroundColor(Color.Transparent)
.onClick(() => {
this.saveChanges();
})
}
.width('100%')
.padding($r('app.float.spacing_large'))
// 表单内容
Column({ space: 24 }) {
// 发行者输入
Column({ space: 8 }) {
Text($r('app.string.issuer'))
.fontSize($r('app.float.body_medium'))
.fontColor($r('app.color.text_primary'))
.width('100%')
TextInput({ text: this.issuer })
.width('100%')
.backgroundColor($r('app.color.card_background'))
.borderRadius($r('app.float.border_radius_small'))
.onChange((value: string) => {
this.issuer = value;
this.issuerError = '';
})
if (this.issuerError) {
Text(this.issuerError)
.fontSize($r('app.float.body_small'))
.fontColor($r('app.color.danger_color'))
.width('100%')
}
}
// 账户名输入
Column({ space: 8 }) {
Text($r('app.string.account_name'))
.fontSize($r('app.float.body_medium'))
.fontColor($r('app.color.text_primary'))
.width('100%')
TextInput({ text: this.accountName })
.width('100%')
.backgroundColor($r('app.color.card_background'))
.borderRadius($r('app.float.border_radius_small'))
.onChange((value: string) => {
this.accountName = value;
this.accountNameError = '';
})
if (this.accountNameError) {
Text(this.accountNameError)
.fontSize($r('app.float.body_small'))
.fontColor($r('app.color.danger_color'))
.width('100%')
}
}
// 密钥显示(只读)
Column({ space: 8 }) {
Row() {
Text($r('app.string.secret_key'))
.fontSize($r('app.float.body_medium'))
.fontColor($r('app.color.text_primary'))
Blank()
Button({ type: ButtonType.Normal }) {
Text(this.showSecretKey ? '隐藏' : '显示')
.fontSize($r('app.float.body_small'))
.fontColor($r('app.color.primary_color'))
}
.backgroundColor(Color.Transparent)
.padding(0)
.onClick(() => {
this.showSecretKey = !this.showSecretKey;
})
}
.width('100%')
TextInput({
text: this.showSecretKey ? this.secretKey : '••••••••••••••••'
})
.width('100%')
.backgroundColor($r('app.color.card_background'))
.borderRadius($r('app.float.border_radius_small'))
.enabled(false)
.fontFamily('monospace')
.fontColor($r('app.color.text_secondary'))
Text('出于安全考虑,密钥不支持修改')
.fontSize($r('app.float.body_small'))
.fontColor($r('app.color.text_tertiary'))
.width('100%')
}
// 创建时间
if (this.account) {
Column({ space: 8 }) {
Text('创建时间')
.fontSize($r('app.float.body_medium'))
.fontColor($r('app.color.text_primary'))
.width('100%')
Text(new Date(this.account.createdAt).toLocaleString('zh-CN'))
.fontSize($r('app.float.body_medium'))
.fontColor($r('app.color.text_secondary'))
.width('100%')
}
}
}
.width('100%')
.padding($r('app.float.spacing_large'))
.layoutWeight(1)
}
.width('100%')
.height('100%')
.backgroundColor($r('app.color.background_color'))
}
}

View File

@@ -0,0 +1,262 @@
import { router } from '@kit.ArkUI';
import { AccountViewModel } from '../viewmodel/AccountViewModel';
import { Account } from '../model/Account';
import { AccountCard } from '../view/components/AccountCard';
import { TOTPGenerator } from '../common/utils/TOTPGenerator';
import { StorageUtil } from '../common/utils/StorageUtil';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { AppConstants } from '../common/constants/AppConstants';
import { pasteboard } from '@kit.BasicServicesKit';
import { promptAction } from '@kit.ArkUI';
/**
* 账户列表主页面
*/
@Entry
@Component
struct AccountListPage {
@State accounts: Account[] = [];
@State codes: Map<string, string> = new Map();
@State progress: number = 0;
@State remainingSeconds: number = 30;
@State searchText: string = '';
private viewModel: AccountViewModel = new AccountViewModel();
private timer: number = -1;
async aboutToAppear(): Promise<void> {
// 初始化存储
try {
await StorageUtil.init(getContext(this));
hilog.info(AppConstants.LOG_DOMAIN, AppConstants.LOG_TAG, 'Storage initialized');
} catch (error) {
hilog.error(AppConstants.LOG_DOMAIN, AppConstants.LOG_TAG, 'Failed to init storage: %{public}s', JSON.stringify(error));
}
// 设置更新回调
this.viewModel.setUpdateCallback(() => {
this.updateAccounts();
});
// 加载账户
await this.viewModel.loadAccounts();
this.updateAccounts();
// 启动定时器更新验证码
this.startTimer();
}
aboutToDisappear(): void {
this.stopTimer();
}
/**
* 页面显示时重新加载账户
* 处理从添加/编辑页面返回的情况
*/
onPageShow(): void {
// 重新加载账户数据
this.viewModel.loadAccounts();
}
/**
* 更新账户列表
*/
private updateAccounts(): void {
if (this.searchText) {
this.accounts = this.viewModel.searchAccounts(this.searchText);
} else {
this.accounts = this.viewModel.getAccounts();
}
}
/**
* 启动定时器
*/
private startTimer(): void {
// 立即生成一次验证码
this.generateAllCodes();
// 每秒更新一次
this.timer = setInterval(() => {
this.updateProgress();
this.generateAllCodes();
}, 1000);
}
/**
* 停止定时器
*/
private stopTimer(): void {
if (this.timer !== -1) {
clearInterval(this.timer);
this.timer = -1;
}
}
/**
* 更新进度
*/
private updateProgress(): void {
this.progress = TOTPGenerator.getProgress();
this.remainingSeconds = TOTPGenerator.getRemainingSeconds();
}
/**
* 生成所有账户的验证码
*/
private async generateAllCodes(): Promise<void> {
for (const account of this.accounts) {
try {
const code = await this.viewModel.generateCode(account);
this.codes.set(account.id, code);
} catch (error) {
hilog.error(AppConstants.LOG_DOMAIN, AppConstants.LOG_TAG, 'Failed to generate code: %{public}s', JSON.stringify(error));
}
}
}
/**
* 复制验证码到剪贴板
*/
private copyToClipboard(account: Account): void {
const code = this.codes.get(account.id) || '------';
if (code !== '------') {
const pasteData = pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN, code);
const systemPasteboard = pasteboard.getSystemPasteboard();
systemPasteboard.setData(pasteData);
}
}
/**
* 删除账户
*/
private async deleteAccount(account: Account): Promise<void> {
promptAction.showDialog({
title: $r('app.string.delete_confirm'),
buttons: [
{ text: $r('app.string.cancel'), color: $r('app.color.text_secondary') },
{ text: $r('app.string.delete'), color: $r('app.color.danger_color') }
]
}).then(async (result) => {
if (result.index === 1) {
await this.viewModel.deleteAccount(account.id);
this.updateAccounts();
}
});
}
/**
* 编辑账户
*/
private editAccount(account: Account): void {
router.pushUrl({
url: 'pages/AccountDetailPage',
params: { accountId: account.id }
});
}
/**
* 添加账户
*/
private addAccount(): void {
router.pushUrl({
url: 'pages/AddAccountPage'
});
}
build() {
Column() {
// 顶部导航栏
Row() {
Text($r('app.string.app_name'))
.fontSize($r('app.float.title_large'))
.fontColor($r('app.color.text_primary'))
.fontWeight(FontWeight.Bold)
Blank()
// 添加按钮
Button({ type: ButtonType.Circle }) {
Text('+')
.fontSize(28)
.fontColor($r('app.color.card_background'))
}
.width(40)
.height(40)
.backgroundColor($r('app.color.primary_color'))
.onClick(() => {
this.addAccount();
})
}
.width('100%')
.padding({
left: $r('app.float.spacing_large'),
right: $r('app.float.spacing_large'),
top: $r('app.float.spacing_large'),
bottom: $r('app.float.spacing_medium')
})
// 搜索框
if (this.accounts.length > 0) {
Search({ placeholder: $r('app.string.search') })
.margin({
left: $r('app.float.spacing_large'),
right: $r('app.float.spacing_large'),
bottom: $r('app.float.spacing_medium')
})
.onChange((value: string) => {
this.searchText = value;
this.updateAccounts();
})
}
// 账户列表
if (this.accounts.length === 0) {
// 空状态
Column({ space: 12 }) {
Text($r('app.string.no_accounts'))
.fontSize($r('app.float.body_large'))
.fontColor($r('app.color.text_secondary'))
Text($r('app.string.no_accounts_tip'))
.fontSize($r('app.float.body_medium'))
.fontColor($r('app.color.text_tertiary'))
}
.width('100%')
.layoutWeight(1)
.justifyContent(FlexAlign.Center)
} else {
// 账户列表
List({ space: 12 }) {
ForEach(this.accounts, (account: Account) => {
ListItem() {
AccountCard({
account: account,
code: this.codes.get(account.id) || '------',
progress: this.progress,
remainingSeconds: this.remainingSeconds,
onCopy: (acc: Account) => {
this.copyToClipboard(acc);
},
onEdit: (acc: Account) => {
this.editAccount(acc);
},
onDelete: (acc: Account) => {
this.deleteAccount(acc);
}
})
}
}, (account: Account) => account.id)
}
.width('100%')
.layoutWeight(1)
.padding({
left: $r('app.float.spacing_large'),
right: $r('app.float.spacing_large')
})
}
}
.width('100%')
.height('100%')
.backgroundColor($r('app.color.background_color'))
}
}

View File

@@ -0,0 +1,365 @@
import { router } from '@kit.ArkUI';
import { AccountViewModel } from '../viewmodel/AccountViewModel';
import { Account } from '../model/Account';
import { StorageUtil } from '../common/utils/StorageUtil';
import { promptAction } from '@kit.ArkUI';
import { scanBarcode, scanCore } from '@kit.ScanKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { TOTPUriParser, TOTPUriData } from '../common/utils/TOTPUriParser';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { AppConstants } from '../common/constants/AppConstants';
/**
* 添加账户页面
*/
@Entry
@Component
struct AddAccountPage {
@State currentTab: number = 0; // 0: 扫码, 1: 手动输入 (默认显示扫码)
@State issuer: string = '';
@State accountName: string = '';
@State secretKey: string = '';
@State issuerError: string = '';
@State accountNameError: string = '';
@State secretKeyError: string = '';
private viewModel: AccountViewModel = new AccountViewModel();
async aboutToAppear(): Promise<void> {
// 初始化存储
await StorageUtil.init(getContext(this));
// 加载账户数据
await this.viewModel.loadAccounts();
}
/**
* 验证输入
*/
private validate(): boolean {
let isValid = true;
// 验证发行者
if (!this.issuer.trim()) {
this.issuerError = '此字段不能为空';
isValid = false;
} else {
this.issuerError = '';
}
// 验证账户名
if (!this.accountName.trim()) {
this.accountNameError = '此字段不能为空';
isValid = false;
} else {
this.accountNameError = '';
}
// 验证密钥
if (!this.secretKey.trim()) {
this.secretKeyError = '此字段不能为空';
isValid = false;
} else {
// 验证 Base32 格式
const base32Regex = /^[A-Z2-7]+=*$/;
if (!base32Regex.test(this.secretKey.toUpperCase().replace(/\s/g, ''))) {
this.secretKeyError = '密钥格式无效';
isValid = false;
} else {
this.secretKeyError = '';
}
}
return isValid;
}
/**
* 开始扫码
*/
private async startScan(): Promise<void> {
try {
// 配置扫码参数
const options: scanBarcode.ScanOptions = {
scanTypes: [scanCore.ScanType.QR_CODE], // 只扫描二维码
enableMultiMode: false, // 单码模式
enableAlbum: true // 支持从相册选择
};
// 调用扫码服务
hilog.info(AppConstants.LOG_DOMAIN, AppConstants.LOG_TAG, 'Starting QR code scan...');
const result: scanBarcode.ScanResult = await scanBarcode.startScanForResult(getContext(this), options);
hilog.info(AppConstants.LOG_DOMAIN, AppConstants.LOG_TAG, `Scan result: ${result.originalValue}`);
// 解析扫码结果
this.handleScanResult(result.originalValue);
} catch (err) {
const error = err as BusinessError;
hilog.error(AppConstants.LOG_DOMAIN, AppConstants.LOG_TAG, `Scan failed: ${JSON.stringify(error)}`);
// 如果用户取消扫码,不显示错误提示
if (error.code !== 1000500002) { // 1000500002 是用户取消的错误码
promptAction.showToast({
message: $r('app.string.scan_failed'),
duration: 1500
});
}
}
}
/**
* 处理扫码结果
*/
private handleScanResult(scanValue: string): void {
hilog.info(AppConstants.LOG_DOMAIN, AppConstants.LOG_TAG, `Handling scan result: ${scanValue}`);
// 验证是否为有效的 TOTP URI
if (!TOTPUriParser.isValidTOTPUri(scanValue)) {
promptAction.showToast({
message: $r('app.string.invalid_qr_format'),
duration: 2000
});
return;
}
// 解析 TOTP URI
const uriData: TOTPUriData | null = TOTPUriParser.parse(scanValue);
if (!uriData) {
promptAction.showToast({
message: $r('app.string.invalid_qr_format'),
duration: 2000
});
return;
}
// 填充表单
this.issuer = uriData.issuer;
this.accountName = uriData.accountName;
this.secretKey = uriData.secret;
// 清除错误提示
this.issuerError = '';
this.accountNameError = '';
this.secretKeyError = '';
// 切换到手动输入标签页,显示解析结果
this.currentTab = 1;
promptAction.showToast({
message: $r('app.string.scan_success'),
duration: 1500
});
}
/**
* 保存账户
*/
private async saveAccount(): Promise<void> {
if (!this.validate()) {
return;
}
try {
const account = new Account(
this.issuer.trim(),
this.accountName.trim(),
this.secretKey.toUpperCase().replace(/\s/g, '')
);
await this.viewModel.addAccount(account);
promptAction.showToast({
message: '账户添加成功',
duration: 1500
});
// 返回上一页
router.back();
} catch (error) {
promptAction.showToast({
message: '添加账户失败',
duration: 1500
});
}
}
build() {
Column() {
// 导航栏
Row() {
Button({ type: ButtonType.Normal }) {
Text($r('app.string.cancel'))
.fontSize($r('app.float.body_large'))
.fontColor($r('app.color.primary_color'))
}
.backgroundColor(Color.Transparent)
.onClick(() => {
router.back();
})
Blank()
Text($r('app.string.add_account'))
.fontSize($r('app.float.body_large'))
.fontColor($r('app.color.text_primary'))
.fontWeight(FontWeight.Medium)
Blank()
Button({ type: ButtonType.Normal }) {
Text($r('app.string.save'))
.fontSize($r('app.float.body_large'))
.fontColor($r('app.color.primary_color'))
.fontWeight(FontWeight.Medium)
}
.backgroundColor(Color.Transparent)
.onClick(() => {
this.saveAccount();
})
}
.width('100%')
.padding($r('app.float.spacing_large'))
// Tab 切换
Tabs({ index: this.currentTab }) {
// 扫码标签
TabContent() {
Column({ space: 32 }) {
// 扫码图标
Column({ space: 12 }) {
Text('📱')
.fontSize(80)
Text($r('app.string.scan_tip'))
.fontSize($r('app.float.body_medium'))
.fontColor($r('app.color.text_secondary'))
.textAlign(TextAlign.Center)
}
.margin({ top: 100 })
// 扫码按钮
Button($r('app.string.scan_button'))
.width('80%')
.height(48)
.fontSize($r('app.float.body_large'))
.backgroundColor($r('app.color.primary_color'))
.borderRadius($r('app.float.border_radius_small'))
.onClick(() => {
this.startScan();
})
// 提示文字
Text('或切换到"手动输入"标签手动添加账户')
.fontSize($r('app.float.body_small'))
.fontColor($r('app.color.text_tertiary'))
.textAlign(TextAlign.Center)
.margin({ top: 24 })
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Start)
.alignItems(HorizontalAlign.Center)
.padding($r('app.float.spacing_large'))
}
.tabBar($r('app.string.scan_qr_code'))
// 手动输入标签
TabContent() {
Column({ space: 24 }) {
// 发行者输入
Column({ space: 8 }) {
Text($r('app.string.issuer'))
.fontSize($r('app.float.body_medium'))
.fontColor($r('app.color.text_primary'))
.width('100%')
TextInput({ placeholder: $r('app.string.input_issuer_hint') })
.width('100%')
.backgroundColor($r('app.color.card_background'))
.borderRadius($r('app.float.border_radius_small'))
.onChange((value: string) => {
this.issuer = value;
this.issuerError = '';
})
if (this.issuerError) {
Text(this.issuerError)
.fontSize($r('app.float.body_small'))
.fontColor($r('app.color.danger_color'))
.width('100%')
}
}
// 账户名输入
Column({ space: 8 }) {
Text($r('app.string.account_name'))
.fontSize($r('app.float.body_medium'))
.fontColor($r('app.color.text_primary'))
.width('100%')
TextInput({ placeholder: $r('app.string.input_account_hint') })
.width('100%')
.backgroundColor($r('app.color.card_background'))
.borderRadius($r('app.float.border_radius_small'))
.onChange((value: string) => {
this.accountName = value;
this.accountNameError = '';
})
if (this.accountNameError) {
Text(this.accountNameError)
.fontSize($r('app.float.body_small'))
.fontColor($r('app.color.danger_color'))
.width('100%')
}
}
// 密钥输入
Column({ space: 8 }) {
Text($r('app.string.secret_key'))
.fontSize($r('app.float.body_medium'))
.fontColor($r('app.color.text_primary'))
.width('100%')
TextInput({ placeholder: $r('app.string.input_secret_hint') })
.width('100%')
.backgroundColor($r('app.color.card_background'))
.borderRadius($r('app.float.border_radius_small'))
.type(InputType.Normal)
.fontFamily('monospace')
.onChange((value: string) => {
this.secretKey = value;
this.secretKeyError = '';
})
if (this.secretKeyError) {
Text(this.secretKeyError)
.fontSize($r('app.float.body_small'))
.fontColor($r('app.color.danger_color'))
.width('100%')
}
}
// 提示信息
Text('密钥通常由服务提供商以 Base32 格式提供,如: JBSWY3DPEHPK3PXP')
.fontSize($r('app.float.body_small'))
.fontColor($r('app.color.text_tertiary'))
.width('100%')
.margin({ top: 8 })
}
.width('100%')
.padding($r('app.float.spacing_large'))
}
.tabBar($r('app.string.manual_entry'))
}
.width('100%')
.layoutWeight(1)
.barMode(BarMode.Fixed)
.onChange((index: number) => {
this.currentTab = index;
})
}
.width('100%')
.height('100%')
.backgroundColor($r('app.color.background_color'))
}
}

View File

@@ -0,0 +1,23 @@
@Entry
@Component
struct Index {
@State message: string = 'Hello World';
build() {
RelativeContainer() {
Text(this.message)
.id('HelloWorld')
.fontSize($r('app.float.page_text_font_size'))
.fontWeight(FontWeight.Bold)
.alignRules({
center: { anchor: '__container__', align: VerticalAlign.Center },
middle: { anchor: '__container__', align: HorizontalAlign.Center }
})
.onClick(() => {
this.message = 'Welcome';
})
}
.height('100%')
.width('100%')
}
}

View File

@@ -0,0 +1,150 @@
import { Account } from '../../model/Account';
import { ProgressRing } from './ProgressRing';
import { promptAction } from '@kit.ArkUI';
/**
* 账户卡片组件
* 显示账户信息和验证码
*/
@Component
export struct AccountCard {
@Prop account: Account = new Account('', '', '');
@Prop code: string = '------';
@Prop progress: number = 0;
@Prop remainingSeconds: number = 30;
onCopy?: (account: Account) => void;
onEdit?: (account: Account) => void;
onDelete?: (account: Account) => void;
build() {
Column() {
Row() {
// 左侧内容区
Column({ space: 8 }) {
// 账户信息
Row({ space: 4 }) {
if (this.account.issuer) {
Text(this.account.issuer)
.fontSize($r('app.float.body_large'))
.fontColor($r('app.color.text_primary'))
.fontWeight(FontWeight.Medium)
}
if (this.account.issuer && this.account.accountName) {
Text('·')
.fontSize($r('app.float.body_medium'))
.fontColor($r('app.color.text_secondary'))
}
if (this.account.accountName) {
Text(this.account.accountName)
.fontSize($r('app.float.body_medium'))
.fontColor($r('app.color.text_secondary'))
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.layoutWeight(1)
}
}
.width('100%')
// 验证码
Text(this.formatCode(this.code))
.fontSize($r('app.float.code_size'))
.fontColor(this.remainingSeconds <= 5 ? $r('app.color.warning_color') : $r('app.color.text_primary'))
.fontWeight(FontWeight.Bold)
.fontFamily('monospace')
.letterSpacing(4)
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Blank()
// 右侧进度环
Column({ space: 8 }) {
ProgressRing({
progress: this.progress,
ringSize: 44,
strokeWidth: 6
})
Text(`${this.remainingSeconds}s`)
.fontSize($r('app.float.body_small'))
.fontColor($r('app.color.text_secondary'))
}
.justifyContent(FlexAlign.Center)
}
.width('100%')
.padding($r('app.float.spacing_large'))
}
.width('100%')
.backgroundColor($r('app.color.card_background'))
.borderRadius($r('app.float.border_radius'))
.shadow({
radius: 8,
color: '#08000000',
offsetX: 0,
offsetY: 2
})
.onClick(() => {
// 点击复制验证码
this.copyCode();
})
.gesture(
// 长按显示操作菜单
LongPressGesture()
.onAction(() => {
this.showActionSheet();
})
)
}
/**
* 格式化验证码,添加空格分隔
*/
private formatCode(code: string): string {
if (code.length === 6) {
return `${code.substring(0, 3)} ${code.substring(3)}`;
}
return code;
}
/**
* 复制验证码
*/
private copyCode(): void {
if (this.onCopy) {
this.onCopy(this.account);
}
promptAction.showToast({
message: $r('app.string.copied'),
duration: 1500
});
}
/**
* 显示操作菜单
*/
private showActionSheet(): void {
// 使用 AlertDialog 显示操作菜单
AlertDialog.show({
title: this.account.getDisplayName(),
message: '',
autoCancel: true,
alignment: DialogAlignment.Bottom,
gridCount: 4,
primaryButton: {
value: '复制',
action: () => {
this.copyCode();
}
},
secondaryButton: {
value: '编辑',
action: () => {
if (this.onEdit) {
this.onEdit(this.account);
}
}
}
});
}
}

View File

@@ -0,0 +1,36 @@
/**
* 圆形进度指示器组件
* 用于显示 TOTP 倒计时进度
*/
@Component
export struct ProgressRing {
@Prop progress: number = 0; // 进度值 0-1
@Prop ringSize: number = 40; // 尺寸
@Prop strokeWidth: number = 3; // 线宽
@Prop ringColor: ResourceColor = $r('app.color.primary_color'); // 颜色
build() {
Stack() {
// 背景圆环
Circle()
.width(this.ringSize)
.height(this.ringSize)
.fillOpacity(0)
.stroke($r('app.color.separator_color'))
.strokeWidth(this.strokeWidth)
// 进度圆环
Progress({
value: this.progress * 100,
total: 100,
type: ProgressType.Ring
})
.width(this.ringSize)
.height(this.ringSize)
.color(this.ringColor)
.style({
strokeWidth: this.strokeWidth
})
}
}
}

View File

@@ -0,0 +1,143 @@
import { Account, AccountJson } from '../model/Account';
import { StorageUtil } from '../common/utils/StorageUtil';
import { TOTPGenerator } from '../common/utils/TOTPGenerator';
import { AppConstants } from '../common/constants/AppConstants';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { BusinessError } from '@kit.BasicServicesKit';
/**
* 账户业务逻辑类
*/
export class AccountViewModel {
private accounts: Account[] = [];
private updateCallback?: () => void;
/**
* 设置更新回调
*/
setUpdateCallback(callback: () => void): void {
this.updateCallback = callback;
}
/**
* 通知数据更新
*/
private notifyUpdate(): void {
if (this.updateCallback) {
this.updateCallback();
}
}
/**
* 加载所有账户
*/
async loadAccounts(): Promise<void> {
try {
const jsonStr = await StorageUtil.getString(AppConstants.KEY_ACCOUNTS, '[]');
const jsonArray = JSON.parse(jsonStr) as Array<AccountJson>;
this.accounts = jsonArray.map(json => Account.fromJson(json));
hilog.info(AppConstants.LOG_DOMAIN, AppConstants.LOG_TAG, `Loaded ${this.accounts.length} accounts`);
this.notifyUpdate();
} catch (err) {
const error = err as BusinessError;
hilog.error(AppConstants.LOG_DOMAIN, AppConstants.LOG_TAG, 'Failed to load accounts: %{public}s', JSON.stringify(error));
this.accounts = [];
}
}
/**
* 保存所有账户
*/
private async saveAccounts(): Promise<void> {
try {
const jsonArray = this.accounts.map(account => account.toJson());
const jsonStr = JSON.stringify(jsonArray);
await StorageUtil.putString(AppConstants.KEY_ACCOUNTS, jsonStr);
hilog.info(AppConstants.LOG_DOMAIN, AppConstants.LOG_TAG, 'Accounts saved successfully');
} catch (err) {
const error = err as BusinessError;
hilog.error(AppConstants.LOG_DOMAIN, AppConstants.LOG_TAG, 'Failed to save accounts: %{public}s', JSON.stringify(error));
throw new Error(`Failed to save accounts: ${error.message}`);
}
}
/**
* 添加账户
*/
async addAccount(account: Account): Promise<void> {
this.accounts.push(account);
await this.saveAccounts();
this.notifyUpdate();
}
/**
* 更新账户
*/
async updateAccount(accountId: string, issuer: string, accountName: string, secret?: string): Promise<void> {
const account = this.accounts.find(acc => acc.id === accountId);
if (account) {
account.update(issuer, accountName, secret);
await this.saveAccounts();
this.notifyUpdate();
}
}
/**
* 删除账户
*/
async deleteAccount(accountId: string): Promise<void> {
const index = this.accounts.findIndex(acc => acc.id === accountId);
if (index !== -1) {
this.accounts.splice(index, 1);
await this.saveAccounts();
this.notifyUpdate();
}
}
/**
* 获取所有账户
*/
getAccounts(): Account[] {
return this.accounts;
}
/**
* 根据 ID 获取账户
*/
getAccountById(accountId: string): Account | undefined {
return this.accounts.find(acc => acc.id === accountId);
}
/**
* 生成验证码
*/
async generateCode(account: Account): Promise<string> {
try {
return await TOTPGenerator.generate(account.secret, undefined, account.period, account.digits);
} catch (error) {
hilog.error(AppConstants.LOG_DOMAIN, AppConstants.LOG_TAG, 'Failed to generate code: %{public}s', JSON.stringify(error));
return '------';
}
}
/**
* 搜索账户
*/
searchAccounts(keyword: string): Account[] {
if (!keyword) {
return this.accounts;
}
const lowerKeyword = keyword.toLowerCase();
return this.accounts.filter(account =>
account.issuer.toLowerCase().includes(lowerKeyword) ||
account.accountName.toLowerCase().includes(lowerKeyword)
);
}
/**
* 获取账户数量
*/
getAccountCount(): number {
return this.accounts.length;
}
}

View File

@@ -0,0 +1,62 @@
{
"module": {
"name": "entry",
"type": "entry",
"description": "$string:module_desc",
"mainElement": "EntryAbility",
"deviceTypes": [
"phone"
],
"deliveryWithInstall": true,
"installationFree": false,
"pages": "$profile:main_pages",
"requestPermissions": [
{
"name": "ohos.permission.CAMERA",
"reason": "$string:camera_permission_reason",
"usedScene": {
"abilities": [
"EntryAbility"
],
"when": "inuse"
}
}
],
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"description": "$string:EntryAbility_desc",
"icon": "$media:icon",
"label": "$string:EntryAbility_label",
"startWindowIcon": "$media:icon",
"startWindowBackground": "$color:start_window_background",
"exported": true,
"skills": [
{
"entities": [
"entity.system.home"
],
"actions": [
"ohos.want.action.home"
]
}
]
}
],
"extensionAbilities": [
{
"name": "EntryBackupAbility",
"srcEntry": "./ets/entrybackupability/EntryBackupAbility.ets",
"type": "backup",
"exported": false,
"metadata": [
{
"name": "ohos.extension.backup",
"resource": "$profile:backup_config"
}
],
}
]
}
}

View File

@@ -0,0 +1,48 @@
{
"color": [
{
"name": "start_window_background",
"value": "#F2F2F7"
},
{
"name": "primary_color",
"value": "#007AFF"
},
{
"name": "background_color",
"value": "#F2F2F7"
},
{
"name": "card_background",
"value": "#FFFFFF"
},
{
"name": "text_primary",
"value": "#000000"
},
{
"name": "text_secondary",
"value": "#8E8E93"
},
{
"name": "text_tertiary",
"value": "#C7C7CC"
},
{
"name": "separator_color",
"value": "#E5E5EA"
},
{
"name": "success_color",
"value": "#34C759"
},
{
"name": "danger_color",
"value": "#FF3B30"
},
{
"name": "warning_color",
"value": "#FF9500"
}
]
}

View File

@@ -0,0 +1,72 @@
{
"float": [
{
"name": "page_text_font_size",
"value": "50fp"
},
{
"name": "title_large",
"value": "32fp"
},
{
"name": "title_medium",
"value": "28fp"
},
{
"name": "title_small",
"value": "24fp"
},
{
"name": "body_large",
"value": "17fp"
},
{
"name": "body_medium",
"value": "15fp"
},
{
"name": "body_small",
"value": "13fp"
},
{
"name": "code_size",
"value": "36fp"
},
{
"name": "border_radius",
"value": "16vp"
},
{
"name": "border_radius_small",
"value": "12vp"
},
{
"name": "border_radius_medium",
"value": "16vp"
},
{
"name": "border_radius_large",
"value": "20vp"
},
{
"name": "spacing_xs",
"value": "4vp"
},
{
"name": "spacing_small",
"value": "8vp"
},
{
"name": "spacing_medium",
"value": "12vp"
},
{
"name": "spacing_large",
"value": "16vp"
},
{
"name": "spacing_xl",
"value": "24vp"
}
]
}

View File

@@ -0,0 +1,148 @@
{
"string": [
{
"name": "module_desc",
"value": "MFA Authenticator"
},
{
"name": "EntryAbility_desc",
"value": "安全的多因素身份验证器"
},
{
"name": "EntryAbility_label",
"value": "身份验证器"
},
{
"name": "app_name",
"value": "身份验证器"
},
{
"name": "accounts",
"value": "账户"
},
{
"name": "add_account",
"value": "添加账户"
},
{
"name": "edit_account",
"value": "编辑账户"
},
{
"name": "delete_account",
"value": "删除账户"
},
{
"name": "search",
"value": "搜索"
},
{
"name": "scan_qr_code",
"value": "扫描二维码"
},
{
"name": "manual_entry",
"value": "手动输入"
},
{
"name": "issuer",
"value": "发行者"
},
{
"name": "account_name",
"value": "账户名称"
},
{
"name": "secret_key",
"value": "密钥"
},
{
"name": "save",
"value": "保存"
},
{
"name": "cancel",
"value": "取消"
},
{
"name": "delete",
"value": "删除"
},
{
"name": "edit",
"value": "编辑"
},
{
"name": "copy",
"value": "复制"
},
{
"name": "copied",
"value": "已复制"
},
{
"name": "settings",
"value": "设置"
},
{
"name": "no_accounts",
"value": "暂无账户"
},
{
"name": "no_accounts_tip",
"value": "点击右上角添加您的第一个账户"
},
{
"name": "delete_confirm",
"value": "确定要删除此账户吗?"
},
{
"name": "confirm",
"value": "确定"
},
{
"name": "input_issuer_hint",
"value": "如: Google, GitHub"
},
{
"name": "input_account_hint",
"value": "如: example@email.com"
},
{
"name": "input_secret_hint",
"value": "输入 Base32 格式的密钥"
},
{
"name": "invalid_secret",
"value": "密钥格式无效"
},
{
"name": "field_required",
"value": "此字段不能为空"
},
{
"name": "scan_button",
"value": "开始扫码"
},
{
"name": "scan_success",
"value": "扫码成功"
},
{
"name": "scan_failed",
"value": "扫码失败"
},
{
"name": "invalid_qr_format",
"value": "无效的二维码格式"
},
{
"name": "camera_permission_reason",
"value": "用于扫描二维码添加账户"
},
{
"name": "scan_tip",
"value": "请将二维码放入扫描框内"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

View File

@@ -0,0 +1,7 @@
{
"layered-image":
{
"background" : "$media:background",
"foreground" : "$media:foreground"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -0,0 +1,3 @@
{
"allowToBackupRestore": true
}

View File

@@ -0,0 +1,7 @@
{
"src": [
"pages/AccountListPage",
"pages/AddAccountPage",
"pages/AccountDetailPage"
]
}

View File

@@ -0,0 +1,48 @@
{
"color": [
{
"name": "start_window_background",
"value": "#000000"
},
{
"name": "primary_color",
"value": "#0A84FF"
},
{
"name": "background_color",
"value": "#000000"
},
{
"name": "card_background",
"value": "#1C1C1E"
},
{
"name": "text_primary",
"value": "#FFFFFF"
},
{
"name": "text_secondary",
"value": "#8E8E93"
},
{
"name": "text_tertiary",
"value": "#48484A"
},
{
"name": "separator_color",
"value": "#38383A"
},
{
"name": "success_color",
"value": "#32D74B"
},
{
"name": "danger_color",
"value": "#FF453A"
},
{
"name": "warning_color",
"value": "#FF9F0A"
}
]
}

View File

@@ -0,0 +1,2 @@
{
}

View File

@@ -0,0 +1,35 @@
import { hilog } from '@kit.PerformanceAnalysisKit';
import { describe, beforeAll, beforeEach, afterEach, afterAll, it, expect } from '@ohos/hypium';
export default function abilityTest() {
describe('ActsAbilityTest', () => {
// Defines a test suite. Two parameters are supported: test suite name and test suite function.
beforeAll(() => {
// Presets an action, which is performed only once before all test cases of the test suite start.
// This API supports only one parameter: preset action function.
})
beforeEach(() => {
// Presets an action, which is performed before each unit test case starts.
// The number of execution times is the same as the number of test cases defined by **it**.
// This API supports only one parameter: preset action function.
})
afterEach(() => {
// Presets a clear action, which is performed after each unit test case ends.
// The number of execution times is the same as the number of test cases defined by **it**.
// This API supports only one parameter: clear action function.
})
afterAll(() => {
// Presets a clear action, which is performed after all test cases of the test suite end.
// This API supports only one parameter: clear action function.
})
it('assertContain', 0, () => {
// Defines a test case. This API supports three parameters: test case name, filter parameter, and test case function.
hilog.info(0x0000, 'testTag', '%{public}s', 'it begin');
let a = 'abc';
let b = 'b';
// Defines a variety of assertion methods, which are used to declare expected boolean conditions.
expect(a).assertContain(b);
expect(a).assertEqual(a);
})
})
}

View File

@@ -0,0 +1,5 @@
import abilityTest from './Ability.test';
export default function testsuite() {
abilityTest();
}

View File

@@ -0,0 +1,11 @@
{
"module": {
"name": "entry_test",
"type": "feature",
"deviceTypes": [
"phone"
],
"deliveryWithInstall": true,
"installationFree": false
}
}

View File

@@ -0,0 +1,5 @@
import localUnitTest from './LocalUnit.test';
export default function testsuite() {
localUnitTest();
}

View File

@@ -0,0 +1,33 @@
import { describe, beforeAll, beforeEach, afterEach, afterAll, it, expect } from '@ohos/hypium';
export default function localUnitTest() {
describe('localUnitTest', () => {
// Defines a test suite. Two parameters are supported: test suite name and test suite function.
beforeAll(() => {
// Presets an action, which is performed only once before all test cases of the test suite start.
// This API supports only one parameter: preset action function.
});
beforeEach(() => {
// Presets an action, which is performed before each unit test case starts.
// The number of execution times is the same as the number of test cases defined by **it**.
// This API supports only one parameter: preset action function.
});
afterEach(() => {
// Presets a clear action, which is performed after each unit test case ends.
// The number of execution times is the same as the number of test cases defined by **it**.
// This API supports only one parameter: clear action function.
});
afterAll(() => {
// Presets a clear action, which is performed after all test cases of the test suite end.
// This API supports only one parameter: clear action function.
});
it('assertContain', 0, () => {
// Defines a test case. This API supports three parameters: test case name, filter parameter, and test case function.
let a = 'abc';
let b = 'b';
// Defines a variety of assertion methods, which are used to declare expected boolean conditions.
expect(a).assertContain(b);
expect(a).assertEqual(a);
});
});
}

View File

@@ -0,0 +1,23 @@
{
"modelVersion": "6.0.0",
"dependencies": {
},
"execution": {
// "analyze": "normal", /* Define the build analyze mode. Value: [ "normal" | "advanced" | "ultrafine" | false ]. Default: "normal" */
// "daemon": true, /* Enable daemon compilation. Value: [ true | false ]. Default: true */
// "incremental": true, /* Enable incremental compilation. Value: [ true | false ]. Default: true */
// "parallel": true, /* Enable parallel compilation. Value: [ true | false ]. Default: true */
// "typeCheck": false, /* Enable typeCheck. Value: [ true | false ]. Default: false */
// "optimizationStrategy": "memory" /* Define the optimization strategy. Value: [ "memory" | "performance" ]. Default: "memory" */
},
"logging": {
// "level": "info" /* Define the log level. Value: [ "debug" | "info" | "warn" | "error" ]. Default: "info" */
},
"debugging": {
// "stacktrace": false /* Disable stacktrace compilation. Value: [ true | false ]. Default: false */
},
"nodeOptions": {
// "maxOldSpaceSize": 8192 /* Enable nodeOptions maxOldSpaceSize compilation. Unit M. Used for the daemon process. Default: 8192*/
// "exposeGC": true /* Enable to trigger garbage collection explicitly. Default: true*/
}
}

6
hvigorfile.ts Normal file
View File

@@ -0,0 +1,6 @@
import { appTasks } from '@ohos/hvigor-ohos-plugin';
export default {
system: appTasks, /* Built-in plugin of Hvigor. It cannot be modified. */
plugins: [] /* Custom plugin to extend the functionality of Hvigor. */
}

28
oh-package-lock.json5 Normal file
View File

@@ -0,0 +1,28 @@
{
"meta": {
"stableOrder": true,
"enableUnifiedLockfile": false
},
"lockfileVersion": 3,
"ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.",
"specifiers": {
"@ohos/hamock@1.0.0": "@ohos/hamock@1.0.0",
"@ohos/hypium@1.0.24": "@ohos/hypium@1.0.24"
},
"packages": {
"@ohos/hamock@1.0.0": {
"name": "",
"version": "1.0.0",
"integrity": "sha512-K6lDPYc6VkKe6ZBNQa9aoG+ZZMiwqfcR/7yAVFSUGIuOAhPvCJAo9+t1fZnpe0dBRBPxj2bxPPbKh69VuyAtDg==",
"resolved": "https://ohpm.openharmony.cn/ohpm/@ohos/hamock/-/hamock-1.0.0.har",
"registryType": "ohpm"
},
"@ohos/hypium@1.0.24": {
"name": "",
"version": "1.0.24",
"integrity": "sha512-3dCqc+BAR5LqEGG2Vtzi8O3r7ci/3fYU+FWjwvUobbfko7DUnXGOccaror0yYuUhJfXzFK0aZNMGSnXaTwEnbw==",
"resolved": "https://ohpm.openharmony.cn/ohpm/@ohos/hypium/-/hypium-1.0.24.har",
"registryType": "ohpm"
}
}
}

10
oh-package.json5 Normal file
View File

@@ -0,0 +1,10 @@
{
"modelVersion": "6.0.0",
"description": "Please describe the basic information.",
"dependencies": {
},
"devDependencies": {
"@ohos/hypium": "1.0.24",
"@ohos/hamock": "1.0.0"
}
}