This commit is contained in:
CC-star 2025-03-31 21:16:43 +08:00
commit 900b732663
159 changed files with 124835 additions and 0 deletions

64
.clang-format Normal file
View File

@ -0,0 +1,64 @@
Language: Cpp
# BasedOnStyle: LLVM
ColumnLimit: 120
SortIncludes: CaseSensitive
TabWidth: 4
IndentWidth: 4
UseTab: Never
AccessModifierOffset: -4
ContinuationIndentWidth: 4
IndentCaseBlocks: false
IndentCaseLabels: false
IndentGotoLabels: true
IndentWrappedFunctionNames: false
SortUsingDeclarations: false
NamespaceIndentation: None
SpaceAfterCStyleCast: false
SpaceAfterLogicalNot: false
SpaceAfterTemplateKeyword: true
SpaceBeforeAssignmentOperators: true
SpaceBeforeCaseColon: false
SpaceBeforeCpp11BracedList: false
SpaceBeforeCtorInitializerColon: true
SpaceBeforeInheritanceColon: true
SpaceBeforeRangeBasedForLoopColon: true
SpaceBeforeSquareBrackets: false
SpaceInEmptyBlock: false
SpaceInEmptyParentheses: false
SpacesInAngles: false
SpacesInCStyleCastParentheses: false
SpacesInConditionalStatement: false
SpacesInParentheses: false
SpacesInSquareBrackets: false
AlignTrailingComments: true
AlignAfterOpenBracket: true
AllowShortCaseLabelsOnASingleLine: false
AllowShortEnumsOnASingleLine: true
AllowShortFunctionsOnASingleLine: All
AllowShortIfStatementsOnASingleLine: Never
AllowShortLambdasOnASingleLine: All
AllowShortLoopsOnASingleLine: false
AlwaysBreakTemplateDeclarations: MultiLine
BinPackArguments: true
BinPackParameters: true
BreakBeforeTernaryOperators: true
BreakConstructorInitializers: BeforeColon
BreakInheritanceList: BeforeColon
BreakStringLiterals: true
InsertBraces: false
IndentExternBlock: NoIndent
BreakBeforeBraces: Custom
BraceWrapping:
AfterCaseLabel: false
AfterClass: false
AfterControlStatement: Never
AfterEnum: false
AfterFunction: false
AfterNamespace: false
AfterStruct: false
AfterUnion: false
AfterExternBlock: false
BeforeCatch: false
BeforeElse: false
ReflowComments: true
MaxEmptyLinesToKeep: 2

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
build
.preview
.idea

1
.hvigor/cache/file-cache.json vendored Normal file

File diff suppressed because one or more lines are too long

1
.hvigor/cache/meta.json vendored Normal file
View File

@ -0,0 +1 @@
{"compileSdkVersion":"5.0.2(14)","hvigorVersion":"5.14.3","toolChainsVersion":"5.0.2.126"}

1
.hvigor/cache/task-cache.json vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
{"basePath":"/Users/tian/Documents/DevEco/ClassMG/.hvigor/dependencyMap/dependencyMap.json5","rootDependency":"./oh-package.json5","dependencyMap":{"entry":"./entry/oh-package.json5"},"modules":[{"name":"entry","srcPath":"../../../entry"}]}

View File

@ -0,0 +1 @@
{"name":"entry","version":"1.0.0","description":"Please describe the basic information.","main":"","author":"","license":"","dependencies":{}}

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,37 @@
{
"HVIGOR_OHOS_PLUGIN": {
"MODULES": [
{
"MODULE_NAME": "1043bfc77febe75fafec0c4309faccf1",
"API_TYPE": "stageMode",
"INCREMENTAL_TASKS": {
"COMPILE_ARKTS": true
}
}
],
"BUILD_MODE": "debug"
},
"HVIGOR": {
"IS_INCREMENTAL": true,
"IS_DAEMON": true,
"IS_PARALLEL": true,
"IS_HVIGORFILE_TYPE_CHECK": false,
"TASK_TIME": {
"1043bfc77febe75fafec0c4309faccf1": {
"ConfigureCmake": 191500,
"PreCheckSyscap": 170792,
"ProcessIntegratedHsp": 920208,
"BuildNativeWithCmake": 180209,
"SyscapTransform": 1222042,
"BuildNativeWithNinja": 795834,
"BuildJS": 1263042
}
},
"TOTAL_TIME": 1834683875,
"BUILD_ID": "202503312112520550",
"ERROR_MESSAGE": {
"CODE": "00000",
"TIMESTAMP": "1743426773889"
}
}
}

View File

@ -0,0 +1 @@
{"CACHE_SYNC_FILE_HASH":{"/Users/tian/Documents/DevEco/ClassMG/hvigorfile.ts":"9fcfc9a3e7dcbc1ff9995f87c81451a17a752746978695a53ea315b4b0923f08","/Users/tian/Documents/DevEco/ClassMG/oh-package.json5":"d15b2e6574c835be846438740580fdf0d07913cca0f9f2b81c0cd23fc0e8542f","/Users/tian/Documents/DevEco/ClassMG/.hvigor/outputs/sync/output.json":"2fa5616dec2fad0d8c059dab3fd66b8b0b4c31ea05bcba95226d38634517a0dc","/Users/tian/Documents/DevEco/ClassMG/entry/hvigorfile.ts":"9fcfc9a3e7dcbc1ff9995f87c81451a17a752746978695a53ea315b4b0923f08","/Users/tian/Documents/DevEco/ClassMG/entry/build-profile.json5":"e753bd3a2f1e3b9cd295cdaebd2aa42f48d3df182a195636e9c3f2cb784c2ceb","/Users/tian/Documents/DevEco/ClassMG/hvigor/hvigor-config.json5":"e381cc4e155e767588bdbd4bce1682e23850099dc7b861b35f2a98ac70699378","/Users/tian/Documents/DevEco/ClassMG/entry/oh-package.json5":"e5c96a2b6c40008639c515043adc6905ce28355164f58b419139eeaccaed5d86","/Users/tian/Documents/DevEco/ClassMG/build-profile.json5":"67a095cfb37136aba9f50f1ae51e82f2818dafb19b0011a78d4e0df536a67f42","SDK_LOCATION":"/Applications/DevEco-Studio.app/Contents/sdk"},"OHPM_INSTALL_FILE_HASH":{"/Users/tian/Documents/DevEco/ClassMG/oh-package.json5":"d15b2e6574c835be846438740580fdf0d07913cca0f9f2b81c0cd23fc0e8542f","/Users/tian/Documents/DevEco/ClassMG/oh_modules":true,"/Users/tian/Documents/DevEco/ClassMG/entry/oh_modules":false,"/Users/tian/Documents/DevEco/ClassMG/entry/oh-package.json5":"e5c96a2b6c40008639c515043adc6905ce28355164f58b419139eeaccaed5d86","/Users/tian/Documents/DevEco/ClassMG/oh_modules/.ohpm/lock.json5":"744ae73f8f3a6db0e250f8e9050ebe1d27617adcfeb9d6abded765a2fc13a86c"}}

View File

@ -0,0 +1,174 @@
{
"ohos-module-entry": {
"SELECT_TARGET": "default",
"MODULE_BUILD_DIR": "/Users/tian/Documents/DevEco/ClassMG/entry/build",
"DEPENDENCY_INFO": {},
"TARGETS": {
"default": {
"SOURCE_ROOT": "/Users/tian/Documents/DevEco/ClassMG/entry/src/main",
"RESOURCES_PATH": [
"/Users/tian/Documents/DevEco/ClassMG/entry/src/main/resources"
],
"BUILD_PATH": {
"OUTPUT_PATH": "/Users/tian/Documents/DevEco/ClassMG/entry/build/default/outputs/default",
"INTERMEDIA_PATH": "/Users/tian/Documents/DevEco/ClassMG/entry/build/default/intermediates",
"JS_ASSETS_PATH": "/Users/tian/Documents/DevEco/ClassMG/entry/build/default/intermediates/loader_out/default",
"JS_LITE_ASSETS_PATH": "/Users/tian/Documents/DevEco/ClassMG/entry/build/default/intermediates/loader_out_lite/default",
"RES_PATH": "/Users/tian/Documents/DevEco/ClassMG/entry/build/default/intermediates/res/default",
"RES_PROFILE_PATH": "/Users/tian/Documents/DevEco/ClassMG/entry/build/default/intermediates/res/default/resources/base/profile",
"ETS_SUPER_VISUAL_PATH": "/Users/tian/Documents/DevEco/ClassMG/entry/build/default/cache/default/default@CompileArkTS/esmodule",
"JS_SUPER_VISUAL_PATH": "/Users/tian/Documents/DevEco/ClassMG/entry/build/default/cache/default/default@CompileJS/jsbundle",
"WORKER_LOADER": "/Users/tian/Documents/DevEco/ClassMG/entry/build/default/intermediates/loader/default/loader.json",
"MANIFEST_JSON": "/Users/tian/Documents/DevEco/ClassMG/entry/build/default/intermediates/manifest/default",
"OUTPUT_METADATA_JSON": "/Users/tian/Documents/DevEco/ClassMG/entry/build/default/intermediates/hap_metadata/default/output_metadata.json",
"SOURCE_MAP_DIR": "/Users/tian/Documents/DevEco/ClassMG/entry/build/default/intermediates/source_map/default"
},
"BUILD_OPTION": {
"debuggable": true
}
},
"ohosTest": {
"SOURCE_ROOT": "/Users/tian/Documents/DevEco/ClassMG/entry/src/ohosTest",
"RESOURCES_PATH": [
"/Users/tian/Documents/DevEco/ClassMG/entry/src/ohosTest/resources"
],
"BUILD_PATH": {
"OUTPUT_PATH": "/Users/tian/Documents/DevEco/ClassMG/entry/build/default/outputs/ohosTest",
"INTERMEDIA_PATH": "/Users/tian/Documents/DevEco/ClassMG/entry/build/default/intermediates",
"JS_ASSETS_PATH": "/Users/tian/Documents/DevEco/ClassMG/entry/build/default/intermediates/loader_out/ohosTest",
"JS_LITE_ASSETS_PATH": "/Users/tian/Documents/DevEco/ClassMG/entry/build/default/intermediates/loader_out_lite/ohosTest",
"RES_PATH": "/Users/tian/Documents/DevEco/ClassMG/entry/build/default/intermediates/res/ohosTest",
"RES_PROFILE_PATH": "/Users/tian/Documents/DevEco/ClassMG/entry/build/default/intermediates/res/ohosTest/resources/base/profile",
"ETS_SUPER_VISUAL_PATH": "/Users/tian/Documents/DevEco/ClassMG/entry/build/default/cache/ohosTest/ohosTest@OhosTestCompileArkTS/esmodule",
"JS_SUPER_VISUAL_PATH": "/Users/tian/Documents/DevEco/ClassMG/entry/build/default/cache/ohosTest/ohosTest@OhosTestCompileJS/jsbundle",
"WORKER_LOADER": "/Users/tian/Documents/DevEco/ClassMG/entry/build/default/intermediates/loader/ohosTest/loader.json",
"MANIFEST_JSON": "/Users/tian/Documents/DevEco/ClassMG/entry/build/default/intermediates/manifest/ohosTest",
"OUTPUT_METADATA_JSON": "/Users/tian/Documents/DevEco/ClassMG/entry/build/default/intermediates/hap_metadata/ohosTest/output_metadata.json",
"SOURCE_MAP_DIR": "/Users/tian/Documents/DevEco/ClassMG/entry/build/default/intermediates/source_map/ohosTest"
},
"BUILD_OPTION": {
"debuggable": true
}
}
},
"BUILD_OPTION": {
"default-default": {
"debuggable": true,
"copyFrom": "default",
"strictMode": {
"caseSensitiveCheck": true,
"useNormalizedOHMUrl": true
},
"name": "debug"
}
},
"BUILD_PROFILE_OPT": {
"apiType": "stageMode",
"buildOption": {},
"buildOptionSet": [
{
"name": "release",
"arkOptions": {
"obfuscation": {
"ruleOptions": {
"enable": false,
"files": [
"./obfuscation-rules.txt"
]
}
}
}
},
{
"name": "default"
},
{
"name": "debug"
}
],
"targets": [
{
"name": "default"
},
{
"name": "ohosTest"
}
]
},
"BUILD_CACHE_DIR": ""
},
"ohos-project": {
"SELECT_PRODUCT_NAME": "default",
"MODULE_BUILD_DIR": "/Users/tian/Documents/DevEco/ClassMG/build",
"BUNDLE_NAME": "com.example.myapplication",
"BUILD_PATH": {
"OUTPUT_PATH": "/Users/tian/Documents/DevEco/ClassMG/build/outputs/default"
},
"MODULES": [
{
"name": "entry",
"srcPath": "/Users/tian/Documents/DevEco/ClassMG/entry",
"targets": [
{
"name": "default",
"applyToProducts": [
"default"
]
}
],
"belongProjectPath": "/Users/tian/Documents/DevEco/ClassMG"
}
],
"PROFILE_OPT": {
"app": {
"signingConfigs": [],
"products": [
{
"name": "default",
"signingConfig": "default",
"compatibleSdkVersion": "5.0.2(14)",
"runtimeOS": "HarmonyOS",
"buildOption": {
"strictMode": {
"caseSensitiveCheck": true,
"useNormalizedOHMUrl": true
}
}
}
],
"buildModeSet": [
{
"name": "debug"
},
{
"name": "release"
}
]
},
"modules": [
{
"name": "entry",
"srcPath": "./entry",
"targets": [
{
"name": "default",
"applyToProducts": [
"default"
]
}
]
}
]
},
"CONFIG_PROPERTIES": {
"enableSignTask": true,
"skipNativeIncremental": false,
"hvigor.keepDependency": true
},
"OVERALL_PROJECT_PATHS": [
"/Users/tian/Documents/DevEco/ClassMG"
],
"BUILD_CACHE_DIR": ""
},
"version": 1
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

10
AppScope/app.json5 Normal file
View File

@ -0,0 +1,10 @@
{
"app": {
"bundleName": "com.example.myapplication",
"vendor": "example",
"versionCode": 1000000,
"versionName": "1.0.0",
"icon": "$media:app_icon",
"label": "$string:app_name"
}
}

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

41
build-profile.json5 Normal file
View File

@ -0,0 +1,41 @@
{
"app": {
"signingConfigs": [],
"products": [
{
"name": "default",
"signingConfig": "default",
"compatibleSdkVersion": "5.0.2(14)",
"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

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

@ -0,0 +1,28 @@
{
"apiType": "stageMode",
"buildOption": {
},
"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": "entry",
"version": "1.0.0",
"description": "Please describe the basic information.",
"main": "",
"author": "",
"license": "",
"dependencies": {}
}

View File

@ -0,0 +1,391 @@
// 导入日志管理器 - 仅用于获取版本号
import logManager, { LogCategory, LogEventType } from './logtext';
// 可选的主题颜色
export enum ThemeColor {
BLUE = '#409eff',
GREEN = '#67C23A',
RED = '#F56C6C',
ORANGE = '#E6A23C',
PURPLE = '#909399'
}
// 可选的语言
export enum Language {
CHINESE = '中文',
ENGLISH = 'English'
}
// 用户信息模型
@Observed
export class UserInfoModel {
avatar: string;
name: string;
email: string;
constructor(avatar: string = 'avatar-placeholder.png',
name: string = '张三',
email: string = 'zhangsan@example.com') {
this.avatar = avatar;
this.name = name;
this.email = email;
}
}
// 版本日志项模型
export interface VersionLogItem {
version: string; // 版本号,例如 "1.0.0"
date: string; // 发布日期,例如 "2023-10-01"
changes: string[]; // 更新内容列表
}
// 多语言文本集合
export class TextResources {
// 通用
title: string = '';
// 首页
homeTitle: string = '';
classroomMonitor: string = '';
comprehensiveData: string = '';
// 上课页面
classPageTitle: string = '';
classData: string = '';
teacher: string = '';
classroom: string = '';
// 设置页面
settingsTitle: string = '';
systemSettings: string = '';
themeColor: string = '';
languageText: string = '';
notificationSettings: string = '';
about: string = '';
versionInfo: string = '';
// 版本日志相关
versionLog: string = '';
versionHistory: string = '';
latestVersion: string = '';
noVersionInfo: string = '';
// 颜色名称
blue: string = '';
green: string = '';
red: string = '';
orange: string = '';
purple: string = '';
// 底部导航
home: string = '';
class: string = '';
settings: string = '';
// 通知状态
enabled: string = '';
disabled: string = '';
}
// 设置项模型
export class SettingsModel {
themeColor: ThemeColor = ThemeColor.BLUE;
language: Language = Language.CHINESE;
notificationEnabled: boolean = true;
constructor(themeColor: ThemeColor = ThemeColor.BLUE,
language: Language = Language.CHINESE,
notificationEnabled: boolean = true) {
this.themeColor = themeColor;
this.language = language;
this.notificationEnabled = notificationEnabled;
}
}
// 中文文本资源
const chineseTexts: TextResources = new TextResources();
// 通用
chineseTexts.title = '智慧教室管理系统';
// 首页
chineseTexts.homeTitle = '首页';
chineseTexts.classroomMonitor = '教室监控';
chineseTexts.comprehensiveData = '综合上课数据';
// 上课页面
chineseTexts.classPageTitle = '上课';
chineseTexts.classData = '上课数据';
chineseTexts.teacher = '授课教师';
chineseTexts.classroom = '教室';
// 设置页面
chineseTexts.settingsTitle = '设置';
chineseTexts.systemSettings = '系统设置';
chineseTexts.themeColor = '主题颜色';
chineseTexts.languageText = '语言';
chineseTexts.notificationSettings = '通知设置';
chineseTexts.about = '关于';
chineseTexts.versionInfo = '版本信息';
// 版本日志相关
chineseTexts.versionLog = '版本日志';
chineseTexts.versionHistory = '版本历史';
chineseTexts.latestVersion = '最新版本';
chineseTexts.noVersionInfo = '没有版本信息';
// 颜色名称
chineseTexts.blue = '蓝色';
chineseTexts.green = '绿色';
chineseTexts.red = '红色';
chineseTexts.orange = '橙色';
chineseTexts.purple = '紫色';
// 底部导航
chineseTexts.home = '首页';
chineseTexts.class = '上课';
chineseTexts.settings = '设置';
// 通知状态
chineseTexts.enabled = '开启';
chineseTexts.disabled = '关闭';
// 英文文本资源
const englishTexts: TextResources = new TextResources();
// 通用
englishTexts.title = 'Smart Classroom Management System';
// 首页
englishTexts.homeTitle = 'Home';
englishTexts.classroomMonitor = 'Classroom Monitor';
englishTexts.comprehensiveData = 'Comprehensive Class Data';
// 上课页面
englishTexts.classPageTitle = 'Class';
englishTexts.classData = 'Class Data';
englishTexts.teacher = 'Teacher';
englishTexts.classroom = 'Classroom';
// 设置页面
englishTexts.settingsTitle = 'Settings';
englishTexts.systemSettings = 'System Settings';
englishTexts.themeColor = 'Theme Color';
englishTexts.languageText = 'Language';
englishTexts.notificationSettings = 'Notification Settings';
englishTexts.about = 'About';
englishTexts.versionInfo = 'Version Information';
// 版本日志相关
englishTexts.versionLog = 'Version Log';
englishTexts.versionHistory = 'Version History';
englishTexts.latestVersion = 'Latest Version';
englishTexts.noVersionInfo = 'No Version Information';
// 颜色名称
englishTexts.blue = 'Blue';
englishTexts.green = 'Green';
englishTexts.red = 'Red';
englishTexts.orange = 'Orange';
englishTexts.purple = 'Purple';
// 底部导航
englishTexts.home = 'Home';
englishTexts.class = 'Class';
englishTexts.settings = 'Settings';
// 通知状态
englishTexts.enabled = 'Enabled';
englishTexts.disabled = 'Disabled';
// 全局设置服务
export class SettingsService {
private static instance: SettingsService;
private settings: SettingsModel;
// 添加用户信息存储
private userInfo: UserInfoModel;
// 语言变化的回调函数
private languageChangeCallbacks: (() => void)[] = [];
// 颜色
private colorChangeCallbacks: (() => void)[] = [];
// 添加通知状态回调
private notificationChangeCallbacks: (() => void)[] = [];
// 添加用户信息变化回调
private userInfoChangeCallbacks: (() => void)[] = [];
private constructor() {
// 初始化默认设置
this.settings = new SettingsModel();
// 初始化默认用户信息
this.userInfo = new UserInfoModel();
}
// 获取当前最新版本号 - 与版本日志同步
public getCurrentVersion(): string {
return logManager.getLatestVersion();
}
// 获取所有版本日志
public getVersionLogs(): VersionLogItem[] {
return logManager.getVersionLogs();
}
// 添加新版本日志
public addVersionLog(versionLog: VersionLogItem): void {
logManager.addVersionLog(versionLog);
}
// 单例模式获取实例
public static getInstance(): SettingsService {
if (!SettingsService.instance) {
SettingsService.instance = new SettingsService();
}
return SettingsService.instance;
}
// 获取当前设置
public getSettings(): SettingsModel {
return this.settings;
}
// 获取用户信息
public getUserInfo(): UserInfoModel {
return this.userInfo;
}
// 更新用户邮箱
public updateUserEmail(email: string): void {
if (email && email.trim() !== '') {
const oldEmail = this.userInfo.email;
this.userInfo.email = email.trim();
// 记录用户邮箱变更
logManager.info(LogCategory.USER, LogEventType.DATA_UPDATE, `Email updated: ${email}`);
logManager.debug(LogCategory.USER, LogEventType.SETTING_CHANGE, 'userEmail', oldEmail, email);
this.notifyUserInfoChange();
}
}
// 更新用户名称
public updateUserName(name: string): void {
if (name && name.trim() !== '') {
this.userInfo.name = name.trim();
this.notifyUserInfoChange();
}
}
// 更新用户头像
public updateUserAvatar(avatar: string): void {
if (avatar && avatar.trim() !== '') {
this.userInfo.avatar = avatar.trim();
this.notifyUserInfoChange();
}
}
// 注册用户信息变化的回调
public registerUserInfoChangeCallback(callback: () => void): void {
this.userInfoChangeCallbacks.push(callback);
}
// 通知用户信息变化
private notifyUserInfoChange(): void {
for (const callback of this.userInfoChangeCallbacks) {
callback();
}
}
//注册颜色更新回调函数
public registerColorChangeCallback(callback: () => void): void {
this.colorChangeCallbacks.push(callback);
}
// 更新主题颜色
public setThemeColor(color: ThemeColor): void {
const oldColor = this.settings.themeColor;
this.settings.themeColor = color;
// 记录主题颜色变更
logManager.info(LogCategory.SETTINGS, LogEventType.THEME_CHANGE, color);
logManager.debug(LogCategory.SETTINGS, LogEventType.SETTING_CHANGE, 'themeColor', oldColor, color);
this.notifyColorChange();
}
// 颜色更新后回调
private notifyColorChange(): void {
for (const callback of this.colorChangeCallbacks) {
callback();
}
}
// 更新语言
public setLanguage(language: Language): void {
const oldLanguage = this.settings.language;
this.settings.language = language;
// 记录语言变更
logManager.info(LogCategory.SETTINGS, LogEventType.LANGUAGE_CHANGE, language);
logManager.debug(LogCategory.SETTINGS, LogEventType.SETTING_CHANGE, 'language', oldLanguage, language);
// 更新日志管理器的语言设置
logManager.setLanguage(language);
// 通知所有注册的回调,语言已更改
this.notifyLanguageChange();
}
// 通知语言变化
private notifyLanguageChange(): void {
for (const callback of this.languageChangeCallbacks) {
callback();
}
}
// 更新通知设置
public setNotificationEnabled(enabled: boolean): void {
const oldValue = this.settings.notificationEnabled;
this.settings.notificationEnabled = enabled;
// 记录通知设置变更
logManager.info(LogCategory.SETTINGS, LogEventType.NOTIFICATION_CHANGE, enabled ? 'enabled' : 'disabled');
logManager.debug(LogCategory.SETTINGS, LogEventType.SETTING_CHANGE, 'notificationEnabled', oldValue, enabled);
// 通知所有注册的回调,通知设置已更改
this.notifyNotificationChange();
}
// 注册通知设置变化的回调
public registerNotificationChangeCallback(callback: () => void): void {
this.notificationChangeCallbacks.push(callback);
}
// 通知通知设置变化
private notifyNotificationChange(): void {
for (const callback of this.notificationChangeCallbacks) {
callback();
}
}
// 注册语言变化的回调
public registerLanguageChangeCallback(callback: () => void): void {
this.languageChangeCallbacks.push(callback);
}
// 获取当前语言的文本资源
public getTextResources(): TextResources {
return this.settings.language === Language.CHINESE ? chineseTexts : englishTexts;
}
// 获取主题颜色的友好名称
public getThemeColorName(): string {
const texts = this.getTextResources();
switch (this.settings.themeColor) {
case ThemeColor.BLUE:
return texts.blue;
case ThemeColor.GREEN:
return texts.green;
case ThemeColor.RED:
return texts.red;
case ThemeColor.ORANGE:
return texts.orange;
case ThemeColor.PURPLE:
return texts.purple;
default:
return texts.blue;
}
}
// 获取通知设置的友好名称
public getNotificationStatusName(): string {
const texts = this.getTextResources();
return this.settings.notificationEnabled ? texts.enabled : texts.disabled;
}
}
// 导出默认实例
export default SettingsService.getInstance();

View File

@ -0,0 +1,229 @@
import { hilog } from '@kit.PerformanceAnalysisKit';
// 日志域 - 应用程序的唯一标识符
const LOG_DOMAIN: number = 0;
// 日志标签 - 用于筛选日志
const LOG_TAG: string = 'ClassMG';
/**
* 日志类别枚举
*/
export enum LogCategory {
USER = 'USER',
SETTINGS = 'SETTINGS',
NAVIGATION = 'NAVIGATION',
PERFORMANCE = 'PERFORMANCE',
NETWORK = 'NETWORK',
UI = 'UI',
DATA = 'DATA',
ERROR = 'ERROR',
SYSTEM = 'SYSTEM',
HOME = 'HOME',
CLASS = 'CLASS'
}
/**
* 日志事件类型枚举
*/
export enum LogEventType {
PAGE_VIEW = 'PAGE_VIEW',
BUTTON_CLICK = 'BUTTON_CLICK',
DATA_UPDATE = 'DATA_UPDATE',
SETTING_CHANGE = 'SETTING_CHANGE',
THEME_CHANGE = 'THEME_CHANGE',
LANGUAGE_CHANGE = 'LANGUAGE_CHANGE',
NOTIFICATION_CHANGE = 'NOTIFICATION_CHANGE',
LOGIN = 'LOGIN',
LOGOUT = 'LOGOUT',
ERROR = 'ERROR',
PAGE_APPEAR = 'PAGE_APPEAR',
PAGE_DISAPPEAR = 'PAGE_DISAPPEAR',
PAGE_SHOW = 'PAGE_SHOW',
PAGE_HIDE = 'PAGE_HIDE',
PAGE_BACK = 'PAGE_BACK',
SYSTEM_INFO = 'SYSTEM_INFO'
}
/**
* 版本日志项
*/
export interface VersionLogItem {
version: string; // 版本号
date: string; // 发布日期
changes: string[]; // 更新内容列表
}
/**
* 简单的日志记录
* @param message 日志信息
*/
export function logInfo(message: string): void {
hilog.info(LOG_DOMAIN, LOG_TAG, message);
}
/**
* 日志管理类 - 主要用于管理版本日志
*/
export class LogManager {
private static instance: LogManager;
// 版本日志列表
private versionLogs: VersionLogItem[] = [];
// 当前语言
private currentLanguage: string = '中文';
private constructor() {
this.initVersionLogs();
}
/**
* 获取LogManager实例
*/
public static getInstance(): LogManager {
if (!LogManager.instance) {
LogManager.instance = new LogManager();
}
return LogManager.instance;
}
/**
* 设置当前语言
* @param language 语言设置
*/
public setLanguage(language: string): void {
this.currentLanguage = language;
logInfo(`日志管理器语言已设置为: ${language}`);
}
/**
* 记录信息日志
* @param category 日志类别
* @param type 事件类型
* @param message 日志消息或数据
*/
public info(category: LogCategory, type: LogEventType, message: string | number | boolean): void {
const logMessage = `[${category}][${type}] ${message}`;
hilog.info(LOG_DOMAIN, LOG_TAG, logMessage);
}
/**
* 记录调试日志,带有更多上下文
* @param category 日志类别
* @param type 事件类型
* @param key 相关的键或属性名
* @param oldValue 旧值
* @param newValue 新值
*/
public debug(category: LogCategory, type: LogEventType, key: string, oldValue: string | number | boolean, newValue: string | number | boolean): void {
const logMessage = `[${category}][${type}] ${key}: ${oldValue} -> ${newValue}`;
hilog.debug(LOG_DOMAIN, LOG_TAG, logMessage);
}
/**
* 记录警告日志
* @param category 日志类别
* @param message 警告消息
*/
public warn(category: LogCategory, message: string): void {
const logMessage = `[${category}][WARN] ${message}`;
hilog.warn(LOG_DOMAIN, LOG_TAG, logMessage);
}
/**
* 记录错误日志
* @param category 日志类别
* @param error 错误对象或消息
*/
public error(category: LogCategory, error: string | Error): void {
const errorMessage = error instanceof Error ? `${error.name}: ${error.message}` : error;
const logMessage = `[${category}][ERROR] ${errorMessage}`;
hilog.error(LOG_DOMAIN, LOG_TAG, logMessage);
}
/**
* 初始化版本日志数据
*/
private initVersionLogs(): void {
this.versionLogs = [
{
version: "1.2.0",
date: "2023-12-15",
changes: [
"优化设置页面UI和交互体验",
"增加版本日志功能",
"实现多语言切换优化",
"改进主题颜色切换效果"
]
},
{
version: "1.1.0",
date: "2023-11-10",
changes: [
"添加用户信息编辑功能",
"优化首页数据展示",
"修复已知问题"
]
},
{
version: "1.0.0",
date: "2023-10-15",
changes: [
"初始版本发布",
"实现基本功能界面",
"添加用户信息管理",
"支持主题色切换",
"支持中英文切换"
]
}
];
}
/**
* 获取所有版本日志
*/
public getVersionLogs(): VersionLogItem[] {
return this.versionLogs;
}
/**
* 获取最新版本号
*/
public getLatestVersion(): string {
if (this.versionLogs && this.versionLogs.length > 0) {
return this.versionLogs[0].version;
}
return "1.0.0"; // 默认版本号
}
/**
* 添加新版本日志
* @param versionLog 版本日志项
*/
public addVersionLog(versionLog: VersionLogItem): void {
// 检查版本是否已存在
const existingIndex = this.versionLogs.findIndex(log => log.version === versionLog.version);
if (existingIndex !== -1) {
// 如果已存在,更新该版本信息
this.versionLogs[existingIndex] = versionLog;
logInfo(`版本日志已更新: ${versionLog.version}`);
} else {
// 如果不存在,添加到列表开头(最新版本)
this.versionLogs.unshift(versionLog);
logInfo(`新版本日志已添加: ${versionLog.version}`);
}
// 按版本号排序假设版本号格式为x.y.z
this.versionLogs.sort((a, b) => {
const vA = a.version.split('.').map(Number);
const vB = b.version.split('.').map(Number);
for (let i = 0; i < Math.max(vA.length, vB.length); i++) {
const numA = i < vA.length ? vA[i] : 0;
const numB = i < vB.length ? vB[i] : 0;
if (numB - numA !== 0) return numB - numA;
}
return 0;
});
}
}
// 导出单例实例
export default LogManager.getInstance();

View File

@ -0,0 +1,44 @@
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 {
this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET);
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/WelcomePage', (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,274 @@
import { router } from '@kit.ArkUI';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { MonitorDataType } from './HomePage';
import settingsService, { SettingsModel, TextResources } from '../common/SettingsService';
import logManager, { LogCategory, LogEventType } from '../common/logtext';
@Entry
@Component
struct ClassPage {
@State currentClassInfo: ClassInfo = new ClassInfo('数学', '张老师', 'A101');
@State classData: ClassDataType = {
name: '',
time: '',
content: '',
status: false
}; // Initialize with empty values
// 从设置服务获取设置
@State settings: SettingsModel = settingsService.getSettings();
// 获取文本资源
@State texts: TextResources = settingsService.getTextResources();
aboutToAppear() {
// 注册语言变化的回调
settingsService.registerLanguageChangeCallback(() => {
this.texts = settingsService.getTextResources();
});
// 注册主题颜色变化的回调
settingsService.registerColorChangeCallback(() => {
this.settings = settingsService.getSettings();
});
this.Log_Event('aboutToAppear');
}
build() {
Column() {
// 顶部导航栏
Row() {
Text(this.texts.classPageTitle).fontSize(22).fontColor(Color.White).fontWeight(FontWeight.Bold)
}
.width('100%')
.backgroundColor(this.settings.themeColor)
.height(60)
.justifyContent(FlexAlign.Center)
.padding({ left: 20 })
// 页面内容区
Scroll() {
Column() {
// 当前课程信息
Column() {
Text(this.currentClassInfo.name).fontSize(24).fontWeight(FontWeight.Bold).margin({ bottom: 10 })
Text(`${this.texts.teacher}: ${this.currentClassInfo.teacher}`).fontSize(16).fontColor('#666').margin({ bottom: 8 })
Text(`${this.texts.classroom}: ${this.currentClassInfo.room}`).fontSize(16).fontColor('#666')
}
.width('100%')
.backgroundColor(Color.White)
.borderRadius(12)
.padding(20)
.shadow({ radius: 6, color: '#eeeeee' })
.margin({ top: 15, bottom: 15 })
// 上课数据卡片
Card({
title: this.texts.classData,
data: this.classData,
themeColor: this.settings.themeColor
})
}
.width('100%')
.padding({ left: 16, right: 16 })
}
.layoutWeight(1)
.scrollBar(BarState.Off)
// 底部导航栏
BottomNavigation({
activePage: 'class',
themeColor: this.settings.themeColor,
texts: this.texts
})
}
.width('100%')
.height('100%')
}
// 页面生命周期方法
onPageShow(): void {
this.Log_Event('onPageShow');
}
onPageHide(): void {
this.Log_Event('onPageHide');
}
onBackPress(): void {
this.Log_Event('onBackPress');
}
aboutToDisappear(): void {
this.Log_Event('aboutToDisappear');
}
// 日志记录方法
public Log_Event(eventName: string): void {
// 根据事件名称选择合适的日志事件类型
switch (eventName) {
case 'aboutToAppear':
logManager.info(LogCategory.CLASS, LogEventType.PAGE_APPEAR, 'ClassPage');
break;
case 'aboutToDisappear':
logManager.info(LogCategory.CLASS, LogEventType.PAGE_DISAPPEAR, 'ClassPage');
break;
case 'onPageShow':
logManager.info(LogCategory.CLASS, LogEventType.PAGE_SHOW, 'ClassPage');
break;
case 'onPageHide':
logManager.info(LogCategory.CLASS, LogEventType.PAGE_HIDE, 'ClassPage');
break;
case 'onBackPress':
logManager.info(LogCategory.CLASS, LogEventType.PAGE_BACK, 'ClassPage');
break;
default:
logManager.info(LogCategory.CLASS, LogEventType.SYSTEM_INFO, `ClassPage: ${eventName}`);
}
}
}
// Define a class for currentClassInfo
class ClassInfo {
name: string;
teacher: string;
room: string;
constructor(name: string, teacher: string, room: string) {
this.name = name;
this.teacher = teacher;
this.room = room;
}
}
// Define proper interfaces instead of using any
interface ClassDataType {
name: string;
time: string;
content: string;
status: boolean;
}
// 卡片组件
@Component
struct Card {
title: string = '';
data: ClassDataType | MonitorDataType = {} as ClassDataType;
themeColor: string = '#409eff';
build() {
Column() {
// 卡片标题
Row() {
Text(this.title)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#333')
}
.width('100%')
.padding({ top: 8, bottom: 16 })
// 图表内容
Column() {
Text('图表内容')
.fontSize(16)
.fontColor('#999')
.height(150)
.width('100%')
.textAlign(TextAlign.Center)
}
.backgroundColor('#f5f5f5')
.borderRadius(8)
.width('100%')
}
.width('100%')
.backgroundColor(Color.White)
.borderRadius(12)
.padding(16)
.shadow({ radius: 6, color: '#eeeeee' })
}
}
// 底部导航栏组件
@Component
struct BottomNavigation {
@Prop activePage: string = 'class';
@Prop themeColor: string;
@Prop texts: TextResources = {} as TextResources;
build() {
Row() {
// 首页按钮
Column() {
Image($r('app.media.home'))
.width(24)
.height(24)
.fillColor(this.activePage === 'home' ? this.themeColor : '#999')
Text(this.texts.home)
.fontSize(12)
.fontColor(this.activePage === 'home' ? this.themeColor : '#999')
.margin({ top: 4 })
}
.layoutWeight(1)
.onClick(() => {
if (this.activePage !== 'home') {
router.replaceUrl({
url: 'pages/HomePage',
params: {
direction: 'left' // 从上课页向左切换到首页
}
});
}
})
// 上课按钮
Column() {
Image($r('app.media.class'))
.width(24)
.height(24)
.fillColor(this.activePage === 'class' ? this.themeColor : '#999')
Text(this.texts.class)
.fontSize(12)
.fontColor(this.activePage === 'class' ? this.themeColor : '#999')
.margin({ top: 4 })
}
.layoutWeight(1)
.onClick(() => {
if (this.activePage !== 'class') {
router.replaceUrl({
url: 'pages/ClassPage'
});
}
})
// 设置按钮
Column() {
Image($r('app.media.settings'))
.width(24)
.height(24)
.fillColor(this.activePage === 'settings' ? this.themeColor : '#999')
Text(this.texts.settings)
.fontSize(12)
.fontColor(this.activePage === 'settings' ? this.themeColor : '#999')
.margin({ top: 4 })
}
.layoutWeight(1)
.onClick(() => {
if (this.activePage !== 'settings') {
router.replaceUrl({
url: 'pages/SettingsPage',
params: {
direction: 'right' // 从上课页向右切换到设置页
}
});
}
})
}
.width('100%')
.height(60)
.backgroundColor(Color.White)
.border({ color: '#eeeeee', width: 1, style: BorderStyle.Solid })
}
}

View File

@ -0,0 +1,266 @@
import { router } from '@kit.ArkUI';
import logManager, { LogCategory, LogEventType } from '../common/logtext';
import settingsService, { SettingsModel, TextResources } from '../common/SettingsService';
@Entry
@Component
struct HomePage {
@State message: string = '智慧教室管理系统';
@State classroomMonitorData: MonitorDataType = {
roomId: '',
temperature: 0,
humidity: 0,
occupancy: 0,
status: ''
};
@State comprehensiveClassData: ClassDataType = {
classId: '',
subjectName: '',
teacherName: '',
duration: 0,
studentCount: 0
};
// 从设置服务获取设置
@State settings: SettingsModel = settingsService.getSettings();
// 获取文本资源
@State texts: TextResources = settingsService.getTextResources();
aboutToAppear() {
// 注册语言变化的回调
settingsService.registerLanguageChangeCallback(() => {
this.texts = settingsService.getTextResources();
this.message = this.texts.title;
});
// 初始化文本
this.message = this.texts.title;
}
build() {
Column() {
// 顶部导航栏
Row() {
Text(this.message).fontSize(22).fontColor(Color.White).fontWeight(FontWeight.Bold)
}
.width('100%')
.backgroundColor(this.settings.themeColor)
.height(60)
.justifyContent(FlexAlign.Center)
.padding({ left: 20 })
// 页面内容区
Scroll() {
Column() {
// 教室监控卡片
Card({
title: this.texts.classroomMonitor,
data: this.classroomMonitorData,
themeColor: this.settings.themeColor
})
.margin({ top: 16, bottom: 16 })
// 综合上课数据卡片
Card({
title: this.texts.comprehensiveData,
data: this.comprehensiveClassData,
themeColor: this.settings.themeColor
})
}
.width('100%')
.padding({ left: 16, right: 16 })
}
.layoutWeight(1)
.scrollBar(BarState.Off)
// 底部导航栏
BottomNavigation({
activePage: 'home',
themeColor: this.settings.themeColor,
texts: this.texts
})
}
.width('100%')
.height('100%')
}
// 页面生命周期方法
onPageShow(): void {
this.Log_Event('onPageShow');
}
onPageHide(): void {
this.Log_Event('onPageHide');
}
onBackPress(): void {
this.Log_Event('onBackPress');
}
aboutToDisappear(): void {
this.Log_Event('aboutToDisappear');
}
// 日志记录方法
public Log_Event(eventName: string): void {
// 根据事件名称选择合适的日志事件类型
switch (eventName) {
case 'aboutToAppear':
logManager.info(LogCategory.HOME, LogEventType.PAGE_APPEAR, 'HomePage');
break;
case 'aboutToDisappear':
logManager.info(LogCategory.HOME, LogEventType.PAGE_DISAPPEAR, 'HomePage');
break;
case 'onPageShow':
logManager.info(LogCategory.HOME, LogEventType.PAGE_SHOW, 'HomePage');
break;
case 'onPageHide':
logManager.info(LogCategory.HOME, LogEventType.PAGE_HIDE, 'HomePage');
break;
case 'onBackPress':
logManager.info(LogCategory.HOME, LogEventType.PAGE_BACK, 'HomePage');
break;
default:
logManager.info(LogCategory.HOME, LogEventType.SYSTEM_INFO, `HomePage: ${eventName}`);
}
}
}
// Define proper interfaces
export interface MonitorDataType {
roomId: string;
temperature: number;
humidity: number;
occupancy: number;
status: string;
}
export interface ClassDataType {
classId: string;
subjectName: string;
teacherName: string;
duration: number;
studentCount: number;
}
// 卡片组件
@Component
struct Card {
title: string = '';
data: MonitorDataType | ClassDataType = {} as MonitorDataType;
themeColor: string = '#409eff';
build() {
Column() {
// 卡片标题
Row() {
Text(this.title)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#333')
}
.width('100%')
.padding({ top: 8, bottom: 16 })
// 图表内容
Column() {
Text('图表内容')
.fontSize(16)
.fontColor('#999')
.height(150)
.width('100%')
.textAlign(TextAlign.Center)
}
.backgroundColor('#f5f5f5')
.borderRadius(8)
.width('100%')
}
.width('100%')
.backgroundColor(Color.White)
.borderRadius(12)
.padding(16)
.shadow({ radius: 6, color: '#eeeeee' })
}
}
// 底部导航栏组件
@Component
struct BottomNavigation {
@Prop activePage: string = 'home';
@Prop themeColor: string;
@Prop texts: TextResources = {} as TextResources;
build() {
Row() {
// 首页按钮
Column() {
Image($r('app.media.home'))
.width(24)
.height(24)
.fillColor(this.activePage === 'home' ? this.themeColor : '#999')
Text(this.texts.home)
.fontSize(12)
.fontColor(this.activePage === 'home' ? this.themeColor : '#999')
.margin({ top: 4 })
}
.layoutWeight(1)
.onClick(() => {
if (this.activePage !== 'home') {
router.replaceUrl({
url: 'pages/HomePage'
});
}
})
// 上课按钮
Column() {
Image($r('app.media.class'))
.width(24)
.height(24)
.fillColor(this.activePage === 'class' ? this.themeColor : '#999')
Text(this.texts.class)
.fontSize(12)
.fontColor(this.activePage === 'class' ? this.themeColor : '#999')
.margin({ top: 4 })
}
.layoutWeight(1)
.onClick(() => {
if (this.activePage !== 'class') {
router.replaceUrl({
url: 'pages/ClassPage',
params: {
direction: 'right' // 从首页向右切换到上课页
}
});
}
})
// 设置按钮
Column() {
Image($r('app.media.settings'))
.width(24)
.height(24)
.fillColor(this.activePage === 'settings' ? this.themeColor : '#999')
Text(this.texts.settings)
.fontSize(12)
.fontColor(this.activePage === 'settings' ? this.themeColor : '#999')
.margin({ top: 4 })
}
.layoutWeight(1)
.onClick(() => {
if (this.activePage !== 'settings') {
router.replaceUrl({
url: 'pages/SettingsPage',
params: {
direction: 'right' // 从首页向右切换到设置页
}
});
}
})
}
.width('100%')
.height(60)
.backgroundColor(Color.White)
.border({ color: '#eeeeee', width: 1, style: BorderStyle.Solid })
}
}

View File

@ -0,0 +1,909 @@
import { router } from '@kit.ArkUI';
import { hilog } from '@kit.PerformanceAnalysisKit';
import settingsService, { ThemeColor, Language, SettingsModel, TextResources, UserInfoModel, VersionLogItem } from '../common/SettingsService';
import { promptAction } from '@kit.ArkUI';
@Entry
@Component
struct SettingsPage {
// 使用SettingsService中的用户信息
@State userInfo: UserInfoModel = settingsService.getUserInfo();
// 从设置服务获取设置
@State settings: SettingsModel = settingsService.getSettings();
// 获取文本资源
@State texts: TextResources = settingsService.getTextResources();
@State themeColor: ThemeColor = settingsService.getSettings().themeColor;
// 更新用户邮箱的方法
private updateUserEmail(newEmail: string): void {
if (newEmail && newEmail.trim() !== '') {
// 使用SettingsService更新用户邮箱
settingsService.updateUserEmail(newEmail.trim());
// 显示更新成功的提示
const currentLanguage = settingsService.getSettings().language;
promptAction.showToast({
message: currentLanguage === Language.CHINESE ? '邮箱已更新' : 'Email updated',
duration: 2000
});
}
}
aboutToAppear() {
// 注册语言变化的回调 - 确保更新所有相关状态
settingsService.registerLanguageChangeCallback(() => {
this.texts = settingsService.getTextResources();
this.settings = settingsService.getSettings();
});
//页面颜色变化
settingsService.registerColorChangeCallback(() => {
this.themeColor = settingsService.getSettings().themeColor;
this.settings = settingsService.getSettings(); // 同时更新整个设置对象
});
// 注册用户信息变化的回调
settingsService.registerUserInfoChangeCallback(() => {
this.userInfo = settingsService.getUserInfo();
});
// 页面加载时初始化设置
this.Log_Event('aboutToAppear');
}
build() {
Column() {
// 顶部导航栏 - 美化版
Row() {
Text(this.texts.settingsTitle)
.fontSize(24)
.fontColor(Color.White)
.fontWeight(FontWeight.Bold)
}
.width('100%')
.backgroundColor(this.themeColor)
.height(70)
.padding({ left: 24, right: 24 })
.justifyContent(FlexAlign.Center)
// 添加阴影效果
.shadow({ radius: 8, color: 'rgba(0, 0, 0, 0.1)', offsetY: 2 })
// 为顶部导航栏添加动画效果
.animation({
duration: 300,
curve: Curve.EaseOut
})
// 页面内容区
Scroll() {
Column() {
// 用户信息卡片
UserCard({
user: this.userInfo,
themeColor: this.themeColor,
updateEmail: (newEmail: string) => this.updateUserEmail(newEmail)
})
.margin({ top: 24, bottom: 24 })
.width('100%')
// 为卡片添加转场动画
.transition({
type: TransitionType.Insert,
translate: { x: 0, y: 50, z: 0 },
opacity: 0
})
.animation({
duration: 300,
delay: 100,
curve: Curve.EaseOut
})
.transition({
type: TransitionType.Delete,
translate: { x: 0, y: 50, z: 0 },
opacity: 0
})
// 系统设置选项
SystemSettings({
themeColor: this.settings.themeColor,
language: this.settings.language,
notificationEnabled: this.settings.notificationEnabled,
texts: this.texts
})
.width('100%')
// 为设置卡片添加转场动画
.transition({
type: TransitionType.Insert,
translate: { x: 0, y: 50, z: 0 },
opacity: 0
})
.animation({
duration: 300,
delay: 100,
curve: Curve.EaseOut
})
.transition({
type: TransitionType.Delete,
translate: { x: 0, y: 50, z: 0 },
opacity: 0
})
}
.width('100%')
.padding({ left: 20, right: 20 })
.alignItems(HorizontalAlign.Center)
}
.layoutWeight(1)
.scrollBar(BarState.Off)
.edgeEffect(EdgeEffect.Spring) // 添加弹性滚动效果
// 底部导航栏
BottomNavigation({
activePage: 'settings',
themeColor: this.settings.themeColor,
texts: this.texts
})
// 为底部导航添加阴影和动画
.shadow({ radius: 16, color: 'rgba(0, 0, 0, 0.08)', offsetY: -2 })
.animation({
duration: 300,
curve: Curve.EaseOut
})
}
.width('100%')
.height('100%')
.backgroundColor('#f5f7fa') // 设置页面背景色,提升层次感
}
// 页面生命周期方法
onPageShow(): void {
this.Log_Event('onPageShow');
}
onPageHide(): void {
this.Log_Event('onPageHide');
}
onBackPress(): void {
this.Log_Event('onBackPress');
}
aboutToDisappear(): void {
this.Log_Event('aboutToDisappear');
}
// 日志记录方法
public Log_Event(eventName: string): void {
hilog.info(0, 'ClassMG', `SettingsPage: ${eventName}`);
}
}
// 用户信息卡片组件 - 美化版
@Component
struct UserCard {
@ObjectLink user: UserInfoModel; // 使用@ObjectLink而不是直接值传递确保对象引用更新
@Prop themeColor: string = '#409eff';
// 头像的动画状态
@State avatarScale: number = 1;
// 添加编辑邮箱的状态
@State isEditingEmail: boolean = false;
// 临时存储编辑中的邮箱
@State tempEmail: string = '';
// 编辑按钮的动画状态
@State editScale: number = 1;
// 添加邮箱验证错误状态
@State emailError: boolean = false;
@State errorMessage: string = '';
// 邮箱更新回调
updateEmail: (newEmail: string) => void = () => {};
aboutToAppear() {
// 初始化临时邮箱值
this.tempEmail = this.user.email;
}
// 验证邮箱格式
private validateEmail(email: string): boolean {
// 基本的邮箱格式验证正则表达式
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
return emailRegex.test(email);
}
// 处理保存邮箱
private handleSaveEmail(): void {
// 验证邮箱格式
if (!this.validateEmail(this.tempEmail)) {
this.emailError = true;
this.errorMessage = '邮箱格式无效';
return;
}
// 重置错误状态
this.emailError = false;
this.errorMessage = '';
// 调用更新回调
this.updateEmail(this.tempEmail);
// 退出编辑模式
this.isEditingEmail = false;
}
build() {
Column() {
Row() {
Stack({ alignContent: Alignment.BottomEnd }) {
Image(this.user.avatar)
.width(84)
.height(84)
.borderRadius(42)
.border({ width: 4, color: Color.White, style: BorderStyle.Solid })
.shadow({ radius: 12, color: 'rgba(0, 0, 0, 0.1)', offsetX: 0, offsetY: 4 })
.objectFit(ImageFit.Cover)
.scale({ x: this.avatarScale, y: this.avatarScale })
.animation({
duration: 300,
curve: Curve.EaseOut
})
.onClick(() => {
// 点击头像添加缩放动画效果
this.avatarScale = 0.92;
setTimeout(() => {
this.avatarScale = 1;
}, 150);
})
// 状态指示点
Circle({ width: 20, height: 20 })
.fill('#4CAF50')
.border({ width: 3, color: Color.White, style: BorderStyle.Solid })
.margin({ bottom: 2, right: 2 })
}
Column() {
Text(this.user.name)
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor('#333')
.margin({ bottom: 8 })
// 邮箱信息 - 根据编辑状态显示不同内容
if (this.isEditingEmail) {
Column() {
Row() {
TextInput({ text: this.tempEmail })
.width('70%')
.height(36)
.fontSize(16)
.fontColor('#333')
.backgroundColor('#f5f5f5')
.padding({ left: 8, right: 8 })
.borderRadius(8)
.onChange((value: string) => {
this.tempEmail = value;
// 输入时重置错误状态
if (this.emailError) {
this.emailError = false;
this.errorMessage = '';
}
})
Row({ space: 8 }) {
Button('保存')
.fontSize(14)
.height(32)
.width(60)
.backgroundColor(this.themeColor)
.onClick(() => {
// 保存并退出编辑模式
this.handleSaveEmail();
})
Button('取消')
.fontSize(14)
.height(32)
.width(60)
.backgroundColor('#f0f0f0')
.fontColor('#666')
.onClick(() => {
// 恢复原值并退出编辑模式
this.tempEmail = this.user.email;
this.emailError = false;
this.errorMessage = '';
this.isEditingEmail = false;
})
}
.margin({ left: 8 })
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
// 错误提示信息
if (this.emailError) {
Text(this.errorMessage)
.fontSize(14)
.fontColor('#f56c6c')
.margin({ top: 4 })
}
}
.width('100%')
} else {
Row() {
Text(this.user.email)
.fontSize(16)
.fontColor('#666')
.opacity(0.85)
// 编辑按钮
Image($r('app.media.edit'))
.width(16)
.height(16)
.fillColor(this.themeColor)
.margin({ left: 10 })
.opacity(0.8)
.scale({ x: this.editScale, y: this.editScale })
.animation({
duration: 200,
curve: Curve.EaseOut
})
.onClick(() => {
// 点击编辑按钮的动画效果
this.editScale = 0.8;
setTimeout(() => {
this.editScale = 1;
// 进入编辑模式并初始化临时邮箱
this.tempEmail = this.user.email;
this.isEditingEmail = true;
}, 100);
})
}
}
}
.alignItems(HorizontalAlign.Start)
.margin({ left: 24 })
.layoutWeight(1)
}
.width('100%')
.padding({ top: 18, bottom: 18, left: 20, right: 20 })
.alignItems(VerticalAlign.Center)
}
.width('100%')
.backgroundColor(Color.White)
.borderRadius(16)
.padding(4) // 减小内边距,改用子组件控制
.shadow({ radius: 15, color: 'rgba(0, 0, 0, 0.08)', offsetY: 2 })
}
}
// 系统设置组件 - 美化版
@Component
struct SystemSettings {
@Prop themeColor: string;
@Prop language: string;
@Prop notificationEnabled: boolean;
@Prop texts: TextResources = {} as TextResources;
// 强制UI刷新的控制变量
@State refreshTrigger: number = 0;
// 显示加载动画的状态
@State isLoading: boolean = false;
// 添加本地状态,在组件内部获取最新设置
@State currentTexts: TextResources = settingsService.getTextResources();
// 添加本地状态跟踪当前设置
@State currentLanguage: string = settingsService.getSettings().language;
@State currentThemeColor: string = settingsService.getSettings().themeColor;
@State currentNotificationEnabled: boolean = settingsService.getSettings().notificationEnabled;
aboutToAppear() {
// 注册语言变化的回调,确保组件内文本也会更新
settingsService.registerLanguageChangeCallback(() => {
this.currentTexts = settingsService.getTextResources();
this.currentLanguage = settingsService.getSettings().language;
this.forceRefresh();
});
// 注册颜色变化的回调
settingsService.registerColorChangeCallback(() => {
this.currentThemeColor = settingsService.getSettings().themeColor;
this.forceRefresh();
});
// 注册通知设置变化的回调
settingsService.registerNotificationChangeCallback(() => {
this.currentNotificationEnabled = settingsService.getSettings().notificationEnabled;
this.forceRefresh();
});
}
// 强制刷新UI
private forceRefresh() {
this.refreshTrigger++;
}
// 获取主题颜色显示名称
private getThemeColorDisplayName(): string {
const settings = settingsService.getSettings();
const texts = settingsService.getTextResources();
switch (settings.themeColor) {
case ThemeColor.BLUE:
return texts.blue;
case ThemeColor.GREEN:
return texts.green;
case ThemeColor.RED:
return texts.red;
case ThemeColor.ORANGE:
return texts.orange;
case ThemeColor.PURPLE:
return texts.purple;
default:
return texts.blue;
}
}
// 获取通知状态显示名称
private getNotificationStatusDisplayName(): string {
const settings = settingsService.getSettings();
const texts = settingsService.getTextResources();
return settings.notificationEnabled ? texts.enabled : texts.disabled;
}
// 显示过渡动画并在完成后更新UI
private showTransitionAndUpdate(updateFunc: () => void) {
// 显示全屏过渡动画
this.isLoading = true;
// 立即执行更新函数
updateFunc();
// 1秒后隐藏过渡动画此时所有页面更新已完成
setTimeout(() => {
this.isLoading = false;
this.forceRefresh();
}, 1000);
}
build() {
Stack({ alignContent: Alignment.Center }) {
// 主要内容区(仅在非加载状态显示)
if (!this.isLoading) {
Column() {
// 标题区域
Row() {
Text(this.currentTexts.systemSettings)
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#333')
Blank()
// 小标志
Circle({ width: 8, height: 8 })
.fill(this.currentThemeColor)
.margin({ right: 6 })
.opacity(0.8)
}
.width('100%')
.padding({ top: 20, bottom: 20, left: 24, right: 24 })
// 分隔线
Divider()
.height(1)
.opacity(0.08)
.color('#000')
.width('100%')
// 设置项列表
Column({ space: 'none' }) {
SettingItem({
title: this.currentTexts.themeColor,
value: this.getThemeColorDisplayName(),
refreshTrigger: this.refreshTrigger,
themeColor: this.currentThemeColor,
icon: $r('app.media.theme'),
onItemClick: () => this.showThemeColorDialog()
})
SettingItem({
title: this.currentTexts.languageText,
value: settingsService.getSettings().language,
refreshTrigger: this.refreshTrigger,
themeColor: this.currentThemeColor,
icon: $r('app.media.language'),
onItemClick: () => this.showLanguageDialog()
})
SettingItem({
title: this.currentTexts.notificationSettings,
value: this.getNotificationStatusDisplayName(),
refreshTrigger: this.refreshTrigger,
themeColor: this.currentThemeColor,
icon: $r('app.media.notification'),
onItemClick: () => this.toggleNotification()
})
// 添加日志记录设置项
SettingItem({
title: "日志记录",
value: "查看更新记录",
refreshTrigger: this.refreshTrigger,
themeColor: this.currentThemeColor,
icon: $r('app.media.info'),
onItemClick: () => this.showVersionLogDialog()
})
SettingItem({
title: this.currentTexts.about,
value: this.currentTexts.versionInfo,
refreshTrigger: this.refreshTrigger,
themeColor: this.currentThemeColor,
icon: $r('app.media.info'),
onItemClick: () => this.showAboutInfo()
})
}
.width('100%')
}
.width('100%')
.backgroundColor(Color.White)
.borderRadius(16)
.shadow({ radius: 15, color: 'rgba(0, 0, 0, 0.08)', offsetY: 2 })
}
// 加载动画层 - 美化版
if (this.isLoading) {
Column() {
LoadingProgress()
.color(this.currentThemeColor)
.width(100).height(100)
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.backgroundColor(Color.White)
// 添加淡入淡出效果
.opacity(1)
.animation({
delay: 0,
duration: 300,
curve: Curve.EaseInOut
})
}
}
.width('100%')
.height('100%')
}
// 显示主题颜色选择对话框
private showThemeColorDialog(): void {
// 确保获取最新的文本资源
const texts = settingsService.getTextResources();
const currentLanguage = settingsService.getSettings().language;
AlertDialog.show({
title: texts.themeColor,
message: currentLanguage === Language.CHINESE ? '请选择您喜欢的应用主题颜色' : 'Choose your preferred theme color',
buttons: [
{
value: texts.blue,
action: () => {
// 使用过渡动画更新设置
this.showTransitionAndUpdate(() => {
settingsService.setThemeColor(ThemeColor.BLUE);
});
}
},
{
value: texts.green,
action: () => {
// 使用过渡动画更新设置
this.showTransitionAndUpdate(() => {
settingsService.setThemeColor(ThemeColor.GREEN);
});
}
},
{
value: texts.red,
action: () => {
// 使用过渡动画更新设置
this.showTransitionAndUpdate(() => {
settingsService.setThemeColor(ThemeColor.RED);
});
}
},
{
value: texts.orange,
action: () => {
// 使用过渡动画更新设置
this.showTransitionAndUpdate(() => {
settingsService.setThemeColor(ThemeColor.ORANGE);
});
}
},
{
value: currentLanguage === Language.CHINESE ? '取消' : 'Cancel',
action: () => {}
}
]
})
}
// 显示语言选择对话框
private showLanguageDialog(): void {
// 确保获取最新的文本资源
const texts = settingsService.getTextResources();
const currentLanguage = settingsService.getSettings().language;
AlertDialog.show({
title: texts.languageText,
message: currentLanguage === Language.CHINESE ? '请选择应用语言' : 'Please select a language',
buttons: [
{
value: '中文',
action: () => {
// 使用过渡动画更新设置
this.showTransitionAndUpdate(() => {
settingsService.setLanguage(Language.CHINESE);
// 语言变更需要更新文本资源
setTimeout(() => {
this.currentTexts = settingsService.getTextResources();
}, 500);
});
}
},
{
value: 'English',
action: () => {
// 使用过渡动画更新设置
this.showTransitionAndUpdate(() => {
settingsService.setLanguage(Language.ENGLISH);
// 语言变更需要更新文本资源
setTimeout(() => {
this.currentTexts = settingsService.getTextResources();
}, 500);
});
}
},
{
value: currentLanguage === Language.CHINESE ? '取消' : 'Cancel',
action: () => {}
}
]
})
}
// 切换通知设置
private toggleNotification(): void {
// 获取当前通知状态并切换
const settings = settingsService.getSettings();
const newValue = !settings.notificationEnabled;
const currentLanguage = settings.language;
// 使用过渡动画更新设置
this.showTransitionAndUpdate(() => {
settingsService.setNotificationEnabled(newValue);
// 延迟显示提示信息,等待动画结束后
setTimeout(() => {
const texts = settingsService.getTextResources();
promptAction.showToast({
message: newValue
? (currentLanguage === Language.CHINESE ? '通知已开启' : 'Notifications enabled')
: (currentLanguage === Language.CHINESE ? '通知已关闭' : 'Notifications disabled'),
duration: 2000
});
}, 1100);
});
}
// 显示版本日志对话框
private showVersionLogDialog(): void {
const versionLogs = settingsService.getVersionLogs();
if (versionLogs.length === 0) {
promptAction.showToast({
message: '暂无版本日志',
duration: 2000
});
return;
}
// 构建版本日志内容
let message = '';
for (const log of versionLogs) {
message += `版本 ${log.version}${log.date}\n`;
for (const change of log.changes) {
message += `• ${change}\n`;
}
message += '\n';
}
// 显示对话框
AlertDialog.show({
title: '更新日志',
message: message,
confirm: {
value: this.currentLanguage === Language.CHINESE ? '确定' : 'OK',
action: () => {}
}
});
}
// 显示关于信息
private showAboutInfo(): void {
// 确保使用最新的文本资源
const texts = settingsService.getTextResources();
const currentLanguage = settingsService.getSettings().language;
const currentVersion = settingsService.getCurrentVersion();
AlertDialog.show({
title: texts.about,
message: `v${currentVersion}\n© 第一组 版权所有`,
confirm: {
value: currentLanguage === Language.CHINESE ? '确定' : 'OK',
action: () => {}
}
})
}
}
// 设置项组件 - 美化版
@Component
struct SettingItem {
title: string = '';
value: string = '';
// 添加刷新触发器属性,用于强制组件刷新
refreshTrigger: number = 0;
// 添加主题颜色属性
themeColor: string = '#409eff';
// 添加图标属性
icon: Resource = $r('app.media.settings');
onItemClick: () => void = () => {};
// 动画状态
@State pressed: boolean = false;
build() {
Row() {
// 图标容器
Row() {
Image(this.icon)
.width(22)
.height(22)
.fillColor(this.themeColor)
.opacity(0.9)
}
.width(40)
.height(40)
.borderRadius(20)
.backgroundColor(`${this.themeColor}15`) // 使用主题色的淡色背景
.justifyContent(FlexAlign.Center)
.margin({ right: 16 })
// 标题和值
Column() {
Text(this.title)
.fontSize(16)
.fontColor('#333')
.fontWeight(FontWeight.Medium)
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
// 值和箭头
Row() {
Text(this.value)
.fontSize(16)
.fontColor('#999')
.textAlign(TextAlign.End)
Image($r('app.media.arrow_right'))
.width(18)
.height(18)
.margin({ left: 8 })
.fillColor('#999')
}
.alignItems(VerticalAlign.Center)
}
.width('100%')
.height(76)
.padding({ left: 20, right: 20 })
.backgroundColor(this.pressed ? '#f5f5f5' : Color.White)
.alignItems(VerticalAlign.Center)
.borderRadius(12)
.animation({
duration: 200,
curve: Curve.EaseInOut
})
.onTouch((event) => {
if (event.type === TouchType.Down) {
this.pressed = true;
} else if (event.type === TouchType.Up || event.type === TouchType.Cancel) {
this.pressed = false;
if (event.type === TouchType.Up) {
this.onItemClick();
}
}
})
}
}
// 底部导航栏组件
@Component
struct BottomNavigation {
@Prop activePage: string = 'settings';
@Prop themeColor: string;
@Prop texts: TextResources = {} as TextResources;
build() {
Row() {
// 首页按钮
Column() {
Image($r('app.media.home'))
.width(24)
.height(24)
.fillColor(this.activePage === 'home' ? this.themeColor : '#999')
Text(this.texts.home)
.fontSize(12)
.fontColor(this.activePage === 'home' ? this.themeColor : '#999')
.margin({ top: 4 })
}
.layoutWeight(1)
.onClick(() => {
if (this.activePage !== 'home') {
router.replaceUrl({
url: 'pages/HomePage',
params: {
direction: 'left' // 从设置页向左切换到首页
}
});
}
})
// 上课按钮
Column() {
Image($r('app.media.class'))
.width(24)
.height(24)
.fillColor(this.activePage === 'class' ? this.themeColor : '#999')
Text(this.texts.class)
.fontSize(12)
.fontColor(this.activePage === 'class' ? this.themeColor : '#999')
.margin({ top: 4 })
}
.layoutWeight(1)
.onClick(() => {
if (this.activePage !== 'class') {
router.replaceUrl({
url: 'pages/ClassPage',
params: {
direction: 'left' // 从设置页向左切换到上课页
}
});
}
})
// 设置按钮
Column() {
Image($r('app.media.settings'))
.width(24)
.height(24)
.fillColor(this.activePage === 'settings' ? this.themeColor : '#999')
Text(this.texts.settings)
.fontSize(12)
.fontColor(this.activePage === 'settings' ? this.themeColor : '#999')
.margin({ top: 4 })
}
.layoutWeight(1)
.onClick(() => {
if (this.activePage !== 'settings') {
router.replaceUrl({
url: 'pages/SettingsPage'
});
}
})
}
.width('100%')
.height(60)
.backgroundColor(Color.White)
.border({ color: '#eeeeee', width: 1, style: BorderStyle.Solid })
}
}

View File

@ -0,0 +1,90 @@
import { router } from '@kit.ArkUI';
import logManager, { LogCategory, LogEventType } from '../common/logtext';
@Entry
@Component
struct WelcomePage {
@State message: string = 'Hello World';
@State time: number = 3 //定时器执行总时间
timer: number = -1 //定时器句柄
canGo: boolean = true;
goHome(): void {
if (this.canGo) {
router.pushUrl({ url: 'pages/login' })
this.canGo = false
}
}
aboutToAppear(): void { //页面显示之前显示
this.Log_WelcomeEvent('aboutToAppear');
this.timer = setInterval(() => {
if (this.time <= 1) {
clearInterval(this.timer)
//路由
this.goHome()
}
this.time--
}, 1000)
}
aboutToDisappear(): void {
this.Log_WelcomeEvent('aboutToDisappear');
//清除定时器
clearInterval(this.timer)
}
build() {
Stack({ alignContent: Alignment.TopEnd }) {
Image($r('app.media.welcome'))
Button("跳过" + this.time).onClick(() => {
this.goHome()
})
.backgroundColor('#ccc')
.fontColor('#fff')
}
.height('100%')
.width('100%')
}
// 页面生命周期方法
onPageShow(): void { // 页面显示时触发的生命周期方法
this.Log_WelcomeEvent('onPageShow');
}
onPageHide(): void { // 页面隐藏时触发的生命周期方法
this.Log_WelcomeEvent('onPageHide');
}
onBackPress(): void { // 页面返回按键按下时触发的生命周期方法
this.Log_WelcomeEvent('onBackPress');
}
// 通用日志记录方法
private Log_WelcomeEvent(eventName: string): void {
// 根据事件名称选择合适的日志事件类型
switch (eventName) {
case 'aboutToAppear':
logManager.info(LogCategory.SYSTEM, LogEventType.PAGE_APPEAR, 'WelcomePage');
break;
case 'aboutToDisappear':
logManager.info(LogCategory.SYSTEM, LogEventType.PAGE_DISAPPEAR, 'WelcomePage');
break;
case 'onPageShow':
logManager.info(LogCategory.SYSTEM, LogEventType.PAGE_SHOW, 'WelcomePage');
break;
case 'onPageHide':
logManager.info(LogCategory.SYSTEM, LogEventType.PAGE_HIDE, 'WelcomePage');
break;
case 'onBackPress':
logManager.info(LogCategory.SYSTEM, LogEventType.PAGE_BACK, 'WelcomePage');
break;
default:
logManager.info(LogCategory.SYSTEM, LogEventType.SYSTEM_INFO, `WelcomePage: ${eventName}`);
}
}
}

View File

@ -0,0 +1,133 @@
import { router } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';
import logManager, { LogCategory, LogEventType } from '../common/logtext';
@Entry
@Component
@Preview
struct Login {
@State message: string = 'Hello World';
@State username: string = ''; // 用户名
@State password: string = ''; // 密码
build() {
Column({space:10}){//列布局
Image('http://139.155.155.67:2342/images/tdcat.webp')
.height(150)
//分割线组件
Divider()
Text("唱跳 rap 篮球").fontColor(Color.Green).fontSize(30)
Row(){
Text("用户名:")
TextInput({placeholder:'请输入账号'})//输入提示
.width(250)
.onChange((value: string) => {
this.username = value; // 绑定用户名输入
})
}
Row(){
Text("密码:")
TextInput({placeholder:'请输入密码'})
.type(InputType.Password)//输入模式
.width(270)
.onChange((value: string) => {
this.password = value; // 绑定密码输入
})
}
Button("登录")
.onClick(()=>{
if (this.username === '1' && this.password === '1') {
router.pushUrl({url:'pages/HomePage'})
}else {
AlertDialog.show({message:'输入有误'})
}
})
.width('50%')
Row(){//行布局
Button("忘记密码?")
.fontSize(15)
.backgroundColor(Color.Gray)
.onClick(()=>{
AlertDialog.show({message:"尚未开发"})
})
Blank(150)//间隔
Button("注册")
}
}
.height('100%')
.width('100%')
.padding(20)
.alignItems(HorizontalAlign.Center)
}
// 页面生命周期方法
onPageShow(): void {// 页面显示时触发的生命周期方法
this.Log_LoginEvent('onPageShow');
}
onPageHide(): void {// 页面隐藏时触发的生命周期方法
this.Log_LoginEvent('onPageHide');
}
onBackPress(): void {// 页面返回按键按下时触发的生命周期方法
this.Log_LoginEvent('onBackPress');
}
aboutToAppear(): void {// 页面即将显示时触发的生命周期方法
this.Log_LoginEvent('aboutToAppear');
}
aboutToDisappear(): void {// 页面即将消失时触发的生命周期方法
this.Log_LoginEvent('aboutToDisappear');
}
// 通用日志记录方法
public Log_LoginEvent(eventName: string): void {
// 根据事件名称选择合适的日志事件类型
switch (eventName) {
case 'aboutToAppear':
logManager.info(LogCategory.USER, LogEventType.PAGE_APPEAR, 'LoginPage');
break;
case 'aboutToDisappear':
logManager.info(LogCategory.USER, LogEventType.PAGE_DISAPPEAR, 'LoginPage');
break;
case 'onPageShow':
logManager.info(LogCategory.USER, LogEventType.PAGE_SHOW, 'LoginPage');
break;
case 'onPageHide':
logManager.info(LogCategory.USER, LogEventType.PAGE_HIDE, 'LoginPage');
break;
case 'onBackPress':
logManager.info(LogCategory.USER, LogEventType.PAGE_BACK, 'LoginPage');
break;
default:
logManager.info(LogCategory.USER, LogEventType.SYSTEM_INFO, `LoginPage: ${eventName}`);
}
}
}
/*
interface User{
uname:string
upwd:string
}
class Userinfo{
uname:string
upwd:string
constructor(uname: string, upwd: string) {
this.uname = uname;
this.upwd = upwd;
}
}*/

View File

@ -0,0 +1,58 @@
{
"module": {
"name": "entry",
"type": "entry",
"description": "$string:module_desc",
"mainElement": "EntryAbility",
"deviceTypes": [
"phone",
"tablet",
"2in1"
],
//允许网络连接
"requestPermissions": [
{
"name": "ohos.permission.INTERNET"
}
],
"deliveryWithInstall": true,
"installationFree": false,
"pages": "$profile:main_pages",
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"description": "$string:EntryAbility_desc",
"icon": "$media:layered_image",
"label": "$string:EntryAbility_label",
"startWindowIcon": "$media:startIcon",
"startWindowBackground": "$color:start_window_background",
"exported": true,
"skills": [
{
"entities": [
"entity.system.home"
],
"actions": [
"action.system.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,8 @@
{
"color": [
{
"name": "start_window_background",
"value": "#FFFFFF"
}
]
}

View File

@ -0,0 +1,8 @@
{
"float": [
{
"name": "page_text_font_size",
"value": "50fp"
}
]
}

View File

@ -0,0 +1,16 @@
{
"string": [
{
"name": "module_desc",
"value": "module description"
},
{
"name": "EntryAbility_desc",
"value": "description"
},
{
"name": "EntryAbility_label",
"value": "label"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

View File

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

View File

@ -0,0 +1,10 @@
{
"src": [
"pages/login",
"pages/WelcomePage",
"pages/HomePage",
"pages/SettingsPage",
"pages/ClassPage"
]
}

View File

@ -0,0 +1,8 @@
{
"color": [
{
"name": "start_window_background",
"value": "#000000"
}
]
}

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,13 @@
{
"module": {
"name": "entry_test",
"type": "feature",
"deviceTypes": [
"phone",
"tablet",
"2in1"
],
"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,22 @@
{
"modelVersion": "5.0.2",
"dependencies": {
},
"execution": {
// "analyze": "normal", /* Define the build analyze mode. Value: [ "normal" | "advanced" | 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 */
},
"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. */
}

9
local.properties Normal file
View File

@ -0,0 +1,9 @@
# This file is automatically generated by DevEco Studio.
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
#
# This file should *NOT* be checked into Version Control Systems,
# as it contains information specific to your local configuration.
#
# For customization when using a Version Control System, please read the header note.

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

@ -0,0 +1,27 @@
{
"meta": {
"stableOrder": true
},
"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.21": "@ohos/hypium@1.0.21"
},
"packages": {
"@ohos/hamock@1.0.0": {
"name": "@ohos/hamock",
"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.21": {
"name": "@ohos/hypium",
"version": "1.0.21",
"integrity": "sha512-iyKGMXxE+9PpCkqEwu0VykN/7hNpb+QOeIuHwkmZnxOpI+dFZt6yhPB7k89EgV1MiSK/ieV/hMjr5Z2mWwRfMQ==",
"resolved": "https://ohpm.openharmony.cn/ohpm/@ohos/hypium/-/hypium-1.0.21.har",
"registryType": "ohpm"
}
}
}

10
oh-package.json5 Normal file
View File

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

View File

@ -0,0 +1,4 @@
## 1.0.0
- 修复once断言问题
## 1.0.0-rc
- 提供DevEco Studio预览器场景使能的MockSetup装饰器

View File

@ -0,0 +1,177 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

View File

@ -0,0 +1,82 @@
# Hamock
## 简介
Hamock 是 OpenHarmony 上的模拟框架,提供预览场景的模拟功能。
## 下载安装
```bash
ohpm install @ohos/hamock
```
OpenHarmony ohpm 环境配置等更多内容,请参考[如何安装 OpenHarmony ohpm 包](https://gitee.com/openharmony-tpc/docs/blob/master/OpenHarmony_har_usage.md)
## 使用示例
Hamock 提供了 @MockSetup 用于修饰 Mock 方法,仅支持声明式范式的组件。当开发者预览该组件时,预览运行时将在组件初始化时执行被 @MockSetup 修饰的方法。因此,开发者可以在这个被修饰的方法内重定义组件的方法或重赋值组件的属性,其将在预览时生效。
> 说明:
> @MockSetup 修饰的方法仅在预览场景会自动触发,并先于组件的 aboutToAppear 执行。
### UI组件的方法
在 ArkTS 页面代码中引入 Hamock。在目标组件中定义一个方法并用 @MockSetup 修饰该方法。在这个方法中,使用 MockKit 模拟目标方法。
```typescript
import { MockKit, when, MockSetup } from '@ohos/hamock';
@Entry
@Component
struct Index {
...
@MockSetup
randomName() {
let mocker: MockKit = new MockKit();
let mockfunc: Object = mocker.mockFunc(this, this.method1);
// mock 指定的方法在指定入参的返回值
when(mockfunc)('test').afterReturn(1);
}
...
// 业务场景调用方法
const result: number = this.method1('test'); // in previewer, result = 1
}
```
### UI组件的属性
在 ArkTS 页面代码中引入 Hamock。在目标组件中定义一个方法并用 @MockSetup 修饰该方法。在这个方法中,对于需要 Mock 的属性,可以重新赋值。
```typescript
import { MockSetup } from '@ohos/hamock';
@Component
struct Person {
@Prop species: string;
...
// 在 @MockSetup 片段中,定义对象属性
@MockSetup
randomName() {
this.species = 'primates';
}
...
// 业务场景调用属性(如果从初始化到调用期间,该属性无变化)
const result: string = this.species; // in previewer, result = primates
}
```
## 约束与限制
在下述版本验证通过:
DevEco Studio: 4.1 (4.1.3.400), SDK: API11 (4.1.0.36)
MockSetup 仅在 API11 支持。
## 贡献代码
使用过程中发现任何问题都可以提[Issue](https://gitee.com/openharmony/testfwk_arkxtest/issues) 给我们,当然,我们也非常欢迎你给我们提[PR](https://gitee.com/openharmony/testfwk_arkxtest/pulls) 。
## 开源协议
本项目基于 [Apache License 2.0](https://gitee.com/openharmony/testfwk_arkxtest/blob/master/hamock/LICENSE) ,请自由地享受和参与开源。

View File

@ -0,0 +1,25 @@
/*
* Copyright (c) 2023 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
{
"apiType": "stageMode",
"buildOption": {
},
"targets": [
{
"name": "default"
}
]
}

View File

@ -0,0 +1,17 @@
/*
* Copyright (c) 2023 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Script for compiling build behavior. It is built in the build plug-in and cannot be modified currently.
export { harTasks } from '@ohos/hvigor-ohos-plugin';

View File

@ -0,0 +1,17 @@
/*
* Copyright (c) 2023 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License")
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Script for compiling build behavior. It is built in the build plug-in and cannot be modified currently.
export { harTasks } from '@ohos/hvigor-ohos-plugin';

View File

@ -0,0 +1,58 @@
/*
* Copyright (c) 2023 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License")
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export class ArgumentMatchers {
static any;
static anyString;
static anyBoolean;
static anyNumber;
static anyObj;
static anyFunction;
static matchRegexs(Regex: RegExp): void
}
declare interface when {
afterReturn(value: any): any
afterReturnNothing(): undefined
afterAction(action: any): any
afterThrow(e_msg: string): string
(argMatchers?: any): when;
}
export const when: when;
export interface VerificationMode {
times(count: Number): void
never(): void
once(): void
atLeast(count: Number): void
atMost(count: Number): void
}
export class MockKit {
constructor()
mockFunc(obj: Object, func: Function): Function
mockObject(obj: Object): Object
verify(methodName: String, argsArray: Array<any>): VerificationMode
ignoreMock(obj: Object, func: Function): void
clear(obj: Object): void
clearAll(): void
}
export declare function MockSetup(
target: Object,
propertyName: string | Symbol,
descriptor: TypedPropertyDescriptor<() => void>
): void;

View File

@ -0,0 +1,17 @@
/*
* Copyright (c) 2023 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License")
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { MockSetup, MockKit, when } from './src/main/mock/MockKit';
export { ArgumentMatchers } from './src/main/mock/ArgumentMatchers';

View File

@ -0,0 +1,16 @@
/*
* Copyright (c) 2021-2023 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License")
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { MockSetup, MockKit, when } from './src/main/mock/MockKit.js';
export { ArgumentMatchers } from './src/main/mock/ArgumentMatchers.js';

View File

@ -0,0 +1,17 @@
/*
* Copyright (c) 2021-2023 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License")
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { MockSetup, MockKit, when } from './src/main/mock/MockKit.js';
export { ArgumentMatchers } from './src/main/mock/ArgumentMatchers.js';

View File

@ -0,0 +1,28 @@
/*
* Copyright (c) 2023 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
{
name: '@ohos/hamock',
version: '1.0.0',
description: 'A mock framework for OpenHarmony application.',
main: 'index.ets',
author: 'huawei',
license: 'Apache-2.0',
dependencies: {},
ohos: {
org: 'ohos',
},
types: 'index.d.ts'
}

View File

@ -0,0 +1,97 @@
/*
* Copyright (c) 2022 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export class ArgumentMatchers {
constructor() {
this.ANY = "<any>";
this.ANY_STRING = "<any String>";
this.ANY_BOOLEAN = "<any Boolean>";
this.ANY_NUMBER = "<any Number>";
this.ANY_OBJECT = "<any Object>";
this.ANY_FUNCTION = "<any Function>";
this.MATCH_REGEXS = "<match regexs>";
}
static any() {
}
static anyString() {
}
static anyBoolean() {
}
static anyNumber() {
}
static anyObj() {
}
static anyFunction() {
}
static matchRegexs(regex) {
if (ArgumentMatchers.isRegExp(regex)) {
return regex;
}
throw Error("not a regex");
}
static isRegExp(value) {
return Object.prototype.toString.call(value) === "[object RegExp]";
}
matcheReturnKey(...args) {
let arg = args[0];
let regex = args[1];
let stubSetKey = args[2];
if (stubSetKey && stubSetKey == this.ANY) {
return this.ANY;
}
if (typeof arg === "string" && !regex) {
return this.ANY_STRING;
}
if (typeof arg === "boolean" && !regex) {
return this.ANY_BOOLEAN;
}
if (typeof arg === "number" && !regex) {
return this.ANY_NUMBER;
}
if (typeof arg === "object" && !regex) {
return this.ANY_OBJECT;
}
if (typeof arg === "function" && !regex) {
return this.ANY_FUNCTION;
}
if (typeof arg === "string" && regex) {
return regex.test(arg);
}
return null;
}
matcheStubKey(key) {
if (key === ArgumentMatchers.any) {
return this.ANY;
}
if (key === ArgumentMatchers.anyString) {
return this.ANY_STRING;
}
if (key === ArgumentMatchers.anyBoolean) {
return this.ANY_BOOLEAN;
}
if (key === ArgumentMatchers.anyNumber) {
return this.ANY_NUMBER;
}
if (key === ArgumentMatchers.anyObj) {
return this.ANY_OBJECT;
}
if (key === ArgumentMatchers.anyFunction) {
return this.ANY_FUNCTION;
}
if (ArgumentMatchers.isRegExp(key)) {
return key;
}
return null;
}
}

View File

@ -0,0 +1,118 @@
/*
* Copyright (c) 2022 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export class ArgumentMatchers {
ANY = "<any>";
ANY_STRING = "<any String>";
ANY_BOOLEAN = "<any Boolean>";
ANY_NUMBER = "<any Number>";
ANY_OBJECT = "<any Object>";
ANY_FUNCTION = "<any Function>";
MATCH_REGEXS = "<match regexs>";
static any() {
}
static anyString() {
}
static anyBoolean() {
}
static anyNumber() {
}
static anyObj() {
}
static anyFunction() {
}
static matchRegexs(regex: any) {
if (ArgumentMatchers.isRegExp(regex)) {
return regex;
}
throw Error("not a regex");
}
static isRegExp(value: string) {
return Object.prototype.toString.call(value) === "[object RegExp]";
}
matcheReturnKey(...args: Array<any>) {
let arg = args[0];
let regex = args[1];
let stubSetKey = args[2];
if (stubSetKey && stubSetKey == this.ANY) {
return this.ANY;
}
if (typeof arg === "string" && !regex) {
return this.ANY_STRING;
}
if (typeof arg === "boolean" && !regex) {
return this.ANY_BOOLEAN;
}
if (typeof arg === "number" && !regex) {
return this.ANY_NUMBER;
}
if (typeof arg === "object" && !regex) {
return this.ANY_OBJECT;
}
if (typeof arg === "function" && !regex) {
return this.ANY_FUNCTION;
}
if (typeof arg === "string" && regex) {
return regex.test(arg);
}
return null;
}
matcheStubKey(key: any) {
if (key === ArgumentMatchers.any) {
return this.ANY;
}
if (key === ArgumentMatchers.anyString) {
return this.ANY_STRING;
}
if (key === ArgumentMatchers.anyBoolean) {
return this.ANY_BOOLEAN;
}
if (key === ArgumentMatchers.anyNumber) {
return this.ANY_NUMBER;
}
if (key === ArgumentMatchers.anyObj) {
return this.ANY_OBJECT;
}
if (key === ArgumentMatchers.anyFunction) {
return this.ANY_FUNCTION;
}
if (ArgumentMatchers.isRegExp(key)) {
return key;
}
return null;
}
}

View File

@ -0,0 +1,48 @@
/*
* Copyright (c) 2022 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
class ExtendInterface {
constructor(mocker) {
this.mocker = mocker;
}
stub() {
this.params = arguments;
return this;
}
stubMockedCall(returnInfo) {
this.mocker.stubApply(this, this.params, returnInfo);
}
afterReturn(value) {
this.stubMockedCall(function () {
return value;
});
}
afterReturnNothing() {
this.stubMockedCall(function () {
return undefined;
});
}
afterAction(action) {
this.stubMockedCall(action);
}
afterThrow(msg) {
this.stubMockedCall(function () {
throw msg;
});
}
clear(obj) {
this.mocker.clear(obj);
}
}
export default ExtendInterface;

View File

@ -0,0 +1,63 @@
/*
* Copyright (c) 2022 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { MockKit } from "./MockKit.js";
class ExtendInterface {
private mocker: MockKit
private params: any
constructor(mocker: MockKit) {
this.mocker = mocker;
}
stub() {
this.params = arguments;
return this;
}
stubMockedCall(returnInfo: any) {
this.mocker.stubApply(this, this.params, returnInfo);
}
afterReturn(value: any) {
this.stubMockedCall(function () {
return value;
});
}
afterReturnNothing() {
this.stubMockedCall(function () {
return undefined;
});
}
afterAction(action: Function) {
this.stubMockedCall(action);
}
afterThrow(msg: string) {
this.stubMockedCall(function () {
throw msg;
});
}
clear(obj?: any) {
this.mocker.clear(obj);
}
}
export default ExtendInterface;

View File

@ -0,0 +1,253 @@
/*
* Copyright (c) 2022-2023 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import ExtendInterface from "./ExtendInterface.js";
import VerificationMode from "./VerificationMode.js";
import { ArgumentMatchers } from "./ArgumentMatchers.js";
class MockKit {
constructor() {
this.mFunctions = [];
this.stubs = new Map();
this.recordCalls = new Map();
this.currentSetKey = new Map();
this.mockObj = null;
this.recordMockedMethod = new Map();
this.mFunctions = [];
this.stubs = new Map();
this.recordCalls = new Map();
this.currentSetKey = new Map();
this.mockObj = null;
this.recordMockedMethod = new Map();
}
init() {
this.reset();
}
reset() {
this.mFunctions = [];
this.stubs = new Map();
this.recordCalls = new Map();
this.currentSetKey = new Map();
this.mockObj = null;
this.recordMockedMethod = new Map();
}
clearAll() {
this.reset();
}
clear(obj) {
if (!obj) throw Error("Please enter an object to be cleaned");
if (typeof (obj) !== 'object' && typeof (obj) !== 'function') throw new Error('Not a object or static class');
this.recordMockedMethod.forEach(function (value, key, map) {
if (key) {
obj[key] = value;
}
});
}
ignoreMock(obj, method) {
if (typeof (obj) !== 'object' && typeof (obj) !== 'function') throw new Error('Not a object or static class');
if (typeof (method) !== 'function') throw new Error('Not a function');
let og = this.recordMockedMethod.get(method.propName);
if (og) {
obj[method.propName] = og;
this.recordMockedMethod.set(method.propName, undefined);
}
}
extend(dest, source) {
dest["stub"] = source["stub"];
dest["afterReturn"] = source["afterReturn"];
dest["afterReturnNothing"] = source["afterReturnNothing"];
dest["afterAction"] = source["afterAction"];
dest["afterThrow"] = source["afterThrow"];
dest["stubMockedCall"] = source["stubMockedCall"];
dest["clear"] = source["clear"];
return dest;
}
stubApply(f, params, returnInfo) {
let values = this.stubs.get(f);
if (!values) {
values = new Map();
}
let key = params[0];
if (typeof key === "undefined") {
key = "anonymous-mock-" + f.propName;
}
let matcher = new ArgumentMatchers();
if (matcher.matcheStubKey(key)) {
key = matcher.matcheStubKey(key);
if (key) {
this.currentSetKey.set(f, key);
}
}
values.set(key, returnInfo);
this.stubs.set(f, values);
}
getReturnInfo(f, params) {
let values = this.stubs.get(f);
if (!values) {
return undefined;
}
let retrunKet = params[0];
if (typeof retrunKet === "undefined") {
retrunKet = "anonymous-mock-" + f.propName;
}
let stubSetKey = this.currentSetKey.get(f);
if (stubSetKey && (typeof (retrunKet) !== "undefined")) {
retrunKet = stubSetKey;
}
let matcher = new ArgumentMatchers();
if (matcher.matcheReturnKey(params[0], undefined, stubSetKey) && matcher.matcheReturnKey(params[0], undefined, stubSetKey) !== stubSetKey) {
retrunKet = params[0];
}
values.forEach(function (value, key, map) {
if (ArgumentMatchers.isRegExp(key) && matcher.matcheReturnKey(params[0], key)) {
retrunKet = key;
}
});
return values.get(retrunKet);
}
findName(obj, value) {
let properties = this.findProperties(obj);
let name = '';
properties.filter((item) => (item !== 'caller' && item !== 'arguments')).forEach(function (va1, idx, array) {
if (obj[va1] === value) {
name = va1;
}
});
return name;
}
isFunctionFromPrototype(f, container, propName) {
if (container.constructor !== Object && container.constructor.prototype !== container) {
return container.constructor.prototype[propName] === f;
}
return false;
}
findProperties(obj, ...arg) {
function getProperty(new_obj) {
if (new_obj.__proto__ === null) {
return [];
}
let properties = Object.getOwnPropertyNames(new_obj);
return [...properties, ...getProperty(new_obj.__proto__)];
}
return getProperty(obj);
}
recordMethodCall(originalMethod, args) {
originalMethod['getName'] = function () {
return this.name || this.toString().match(/function\s*([^(]*)\(/)[1];
};
let name = originalMethod.getName();
let arglistString = name + '(' + Array.from(args).toString() + ')';
let records = this.recordCalls.get(arglistString);
if (!records) {
records = 0;
}
records++;
this.recordCalls.set(arglistString, records);
}
mockFunc(originalObject, originalMethod) {
let tmp = this;
this.originalMethod = originalMethod;
const _this = this;
let f = function () {
let args = arguments;
let action = tmp.getReturnInfo(f, args);
if (originalMethod) {
tmp.recordMethodCall(originalMethod, args);
}
if (action) {
return action.apply(_this, args);
}
};
f.container = null || originalObject;
f.original = originalMethod || null;
if (originalObject && originalMethod) {
if (typeof (originalMethod) != 'function')
throw new Error('Not a function');
var name = this.findName(originalObject, originalMethod);
originalObject[name] = f;
this.recordMockedMethod.set(name, originalMethod);
f.propName = name;
f.originalFromPrototype = this.isFunctionFromPrototype(f.original, originalObject, f.propName);
}
f.mocker = this;
this.mFunctions.push(f);
this.extend(f, new ExtendInterface(this));
return f;
}
verify(methodName, argsArray) {
if (!methodName) {
throw Error("not a function name");
}
let a = this.recordCalls.get(methodName + '(' + argsArray.toString() + ')');
return new VerificationMode(a ? a : 0);
}
mockObject(object) {
if (!object || typeof object === "string") {
throw Error(`this ${object} cannot be mocked`);
}
const _this = this;
let mockedObject = {};
let keys = Reflect.ownKeys(object);
keys.filter(key => (typeof Reflect.get(object, key)) === 'function')
.forEach((key) => {
mockedObject[key] = object[key];
mockedObject[key] = _this.mockFunc(mockedObject, mockedObject[key]);
});
return mockedObject;
}
}
function ifMockedFunction(f) {
if (Object.prototype.toString.call(f) != "[object Function]" &&
Object.prototype.toString.call(f) != "[object AsyncFunction]") {
throw Error("not a function");
}
if (!f.stub) {
throw Error("not a mock function");
}
return true;
}
function when(f) {
if (ifMockedFunction(f)) {
return f.stub.bind(f);
}
}
function MockSetup(target, propertyName, descriptor) {
const aboutToAppearOrigin = target.aboutToAppear;
const setup = descriptor.value;
target.aboutToAppear = function (...args) {
if (target.__Param) { // copy attributes and params of the original context
try {
const map = target.__Param;
for (const [key, val] of map) {
this[key] = val; // 'this' refers to context of current function
}
}
catch (e) {
throw new Error(`Mock setup param error: ${e}`);
}
}
if (setup) { // apply the mock content
try {
setup.apply(this);
}
catch (e) {
throw new Error(`Mock setup apply error: ${e}`);
}
}
if (aboutToAppearOrigin) { // append to aboutToAppear function of the original context
aboutToAppearOrigin.apply(this, args);
}
};
}
export { MockSetup, MockKit, when };

View File

@ -0,0 +1,294 @@
/*
* Copyright (c) 2023 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import ExtendInterface from "./ExtendInterface.js";
import VerificationMode from "./VerificationMode.js";
import { ArgumentMatchers } from "./ArgumentMatchers.js";
interface IFunction extends Function {
container: any;
original: any;
propName: string;
originalFromPrototype: boolean
mocker: MockKit
}
class MockKit {
private mFunctions:Array<any> = [];
private stubs = new Map();
private recordCalls = new Map();
private currentSetKey = new Map();
private mockObj = null;
private recordMockedMethod = new Map();
private originalMethod: any;
constructor() {
this.mFunctions = [];
this.stubs = new Map();
this.recordCalls = new Map();
this.currentSetKey = new Map();
this.mockObj = null;
this.recordMockedMethod = new Map();
}
init() {
this.reset();
}
reset() {
this.mFunctions = [];
this.stubs = new Map()
this.recordCalls = new Map();
this.currentSetKey = new Map();
this.mockObj = null;
this.recordMockedMethod = new Map();
}
clearAll() {
this.reset();
}
clear(obj: any) {
if (!obj) throw Error("Please enter an object to be cleaned");
if (typeof (obj) != 'object') throw new Error('Not a object');
this.recordMockedMethod.forEach(function (value, key, map) {
if (key) {
obj[key] = value;
}
});
}
ignoreMock(obj:any, method: any) {
if (typeof (obj) != 'object') throw new Error('Not a object');
if (typeof (method) != 'function') throw new Error('Not a function');
let og = this.recordMockedMethod.get(method.propName);
if (og) {
obj[method.propName] = og;
this.recordMockedMethod.set(method.propName, undefined);
}
}
extend(dest: any, source:any) {
dest["stub"] = source["stub"];
dest["afterReturn"] = source["afterReturn"];
dest["afterReturnNothing"] = source["afterReturnNothing"];
dest["afterAction"] = source["afterAction"];
dest["afterThrow"] = source["afterThrow"];
dest["stubMockedCall"] = source["stubMockedCall"];
dest["clear"] = source["clear"];
return dest;
}
stubApply(f: any, params:any, returnInfo:any) {
let values = this.stubs.get(f);
if (!values) {
values = new Map();
}
let key = params[0];
if (typeof key == "undefined") {
key = "anonymous-mock-" + f.propName;
}
let matcher = new ArgumentMatchers();
if (matcher.matcheStubKey(key)) {
key = matcher.matcheStubKey(key);
if (key) {
this.currentSetKey.set(f, key);
}
}
values.set(key, returnInfo);
this.stubs.set(f, values);
}
getReturnInfo(f: any, params:any) {
let values = this.stubs.get(f);
if (!values) {
return undefined;
}
let retrunKet = params[0];
if (typeof retrunKet == "undefined") {
retrunKet = "anonymous-mock-" + f.propName;
}
let stubSetKey = this.currentSetKey.get(f);
if (stubSetKey && (typeof (retrunKet) != "undefined")) {
retrunKet = stubSetKey;
}
let matcher = new ArgumentMatchers();
if (matcher.matcheReturnKey(params[0], undefined, stubSetKey) && matcher.matcheReturnKey(params[0], undefined, stubSetKey) != stubSetKey) {
retrunKet = params[0];
}
values.forEach(function (value: any, key: any, map: any) {
if (ArgumentMatchers.isRegExp(key) && matcher.matcheReturnKey(params[0], key)) {
retrunKet = key;
}
});
return values.get(retrunKet);
}
findName(obj: any, value: any) {
let properties = this.findProperties(obj);
let name = '';
properties.filter((item:any) => (item !== 'caller' && item !== 'arguments')).forEach(
function (va1:any, idx:any, array:any) {
if (obj[va1] === value) {
name = va1;
}
}
);
return name;
}
isFunctionFromPrototype(f: Function, container:Function, propName: string) {
if (container.constructor != Object && container.constructor.prototype !== container) {
return container.constructor.prototype[propName] === f;
}
return false;
}
findProperties(obj: any, ...arg: Array<any>) {
function getProperty(new_obj:any): Array<any> {
if (new_obj.__proto__ === null) {
return [];
}
let properties = Object.getOwnPropertyNames(new_obj);
return [...properties, ...getProperty(new_obj.__proto__)];
}
return getProperty(obj);
}
recordMethodCall(originalMethod: any, args: any) {
originalMethod['getName'] = function () {
return this.name || this.toString().match(/function\s*([^(]*)\(/)[1];
}
let name = originalMethod.getName();
let arglistString = name + '(' + Array.from(args).toString() + ')';
let records = this.recordCalls.get(arglistString);
if (!records) {
records = 0;
}
records++;
this.recordCalls.set(arglistString, records);
}
mockFunc(originalObject:any, originalMethod:any) {
let tmp = this;
this.originalMethod = originalMethod;
const _this = this;
let f:any = function () {
let args = arguments;
let action = tmp.getReturnInfo(f, args);
if (originalMethod) {
tmp.recordMethodCall(originalMethod, args);
}
if (action) {
return <IFunction> action.apply(_this, args);
}
};
f.container = null || originalObject;
f.original = originalMethod || null;
if (originalObject && originalMethod) {
if (typeof (originalMethod) != 'function') throw new Error('Not a function');
var name = this.findName(originalObject, originalMethod);
originalObject[name] = f;
this.recordMockedMethod.set(name, originalMethod);
f.propName = name;
f.originalFromPrototype = this.isFunctionFromPrototype(f.original, originalObject, f.propName);
}
f.mocker = this;
this.mFunctions.push(f);
this.extend(f, new ExtendInterface(this));
return f;
}
verify(methodName:any, argsArray:any) {
if (!methodName) {
throw Error("not a function name");
}
let a = this.recordCalls.get(methodName + '(' + argsArray.toString() + ')');
return new VerificationMode(a ? a : 0);
}
mockObject(object: any) {
if (!object || typeof object === "string") {
throw Error(`this ${object} cannot be mocked`);
}
const _this = this;
let mockedObject:any = {};
let keys = Reflect.ownKeys(object);
keys.filter(key => (typeof Reflect.get(object, key)) === 'function')
.forEach((key:any) => {
mockedObject[key] = object[key];
mockedObject[key] = _this.mockFunc(mockedObject, mockedObject[key]);
});
return mockedObject;
}
}
function ifMockedFunction(f: any) {
if (Object.prototype.toString.call(f) != "[object Function]" &&
Object.prototype.toString.call(f) != "[object AsyncFunction]") {
throw Error("not a function");
}
if (!f.stub) {
throw Error("not a mock function");
}
return true;
}
function when(f: any) {
if (ifMockedFunction(f)) {
return f.stub.bind(f);
}
}
function MockSetup(target: Object, propertyName: string | Symbol, descriptor: TypedPropertyDescriptor<() => void>): void {
const aboutToAppearOrigin = target.aboutToAppear;
const setup = descriptor.value;
target.aboutToAppear = function (...args: any[]) {
if (target.__Param) { // copy attributes and params of the original context
try {
const map = target.__Param as Map<string, unknown>;
for (const [key, val] of map) {
this[key] = val; // 'this' refers to context of current function
}
} catch (e) {
throw new Error(`Mock setup param error: ${e}`);
}
}
if (setup) { // apply the mock content
try {
setup.apply(this);
} catch (e) {
throw new Error(`Mock setup apply error: ${e}`);
}
}
if (aboutToAppearOrigin) { // append to aboutToAppear function of the original context
aboutToAppearOrigin.apply(this, args);
}
}
}
export {
MockSetup,
MockKit,
when
};

View File

@ -0,0 +1,45 @@
/*
* Copyright (c) 2022-2023 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
class VerificationMode {
constructor(times) {
this.doTimes = times;
}
times(count) {
if (count !== this.doTimes) {
throw Error(`expect ${count} actual ${this.doTimes}`);
}
}
never() {
if (this.doTimes !== 0) {
throw Error(`expect 0 actual ${this.doTimes}`);
}
}
once() {
if (this.doTimes !== 1) {
throw Error(`expect 1 actual ${this.doTimes}`);
}
}
atLeast(count) {
if (count > this.doTimes) {
throw Error('failed ' + count + ' greater than the actual execution times of method');
}
}
atMost(count) {
if (count < this.doTimes) {
throw Error('failed ' + count + ' less than the actual execution times of method');
}
}
}
export default VerificationMode;

View File

@ -0,0 +1,56 @@
/*
* Copyright (c) 2022-2023 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
class VerificationMode {
private doTimes: number
constructor(times: number) {
this.doTimes = times;
}
times(count: number) {
if(count !== this.doTimes) {
throw Error(`expect ${count} actual ${this.doTimes}`);
}
}
never() {
if (this.doTimes !== 0) {
throw Error(`expect 0 actual ${this.doTimes}`);
}
}
once() {
if (this.doTimes !== 1) {
throw Error(`expect 1 actual ${this.doTimes}`);
}
}
atLeast(count: number) {
if (count > this.doTimes) {
throw Error('failed ' + count + ' greater than the actual execution times of method');
}
}
atMost(count: number) {
if (count < this.doTimes) {
throw Error('failed ' + count + ' less than the actual execution times of method');
}
}
}
export default VerificationMode;

View File

@ -0,0 +1,22 @@
{
"app": {
"bundleName": "com.example.hamock",
"debug": true,
"versionCode": 1000000,
"versionName": "1.0.0",
"minAPIVersion": 9,
"targetAPIVersion": 9,
"apiReleaseType": "Release"
},
"module": {
"name": "hamock",
"type": "har",
"deviceTypes": [
"default",
"tablet",
"tv",
"wearable",
"car"
]
}
}

View File

@ -0,0 +1,25 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "JSON schema for mock-config.json5 file",
"definitions": {
"sourceRedirection": {
"description": "A source redirection for mocked module.",
"type": "object",
"required": [
"source"
],
"properties": {
"source": {
"type": "string",
"maxLength": 128,
"minLength": 1
}
}
}
},
"patternProperties": {
".+": {
"$ref": "#/definitions/sourceRedirection"
}
}
}

View File

@ -0,0 +1,17 @@
/**
* Use these variables when you tailor your ArkTS code. They must be of the const type.
*/
export const HAR_VERSION = '1.0.21';
export const BUILD_MODE_NAME = 'debug';
export const DEBUG = true;
export const TARGET_NAME = 'default';
/**
* BuildProfile Class is used only for compatibility purposes.
*/
export default class BuildProfile {
static readonly HAR_VERSION = HAR_VERSION;
static readonly BUILD_MODE_NAME = BUILD_MODE_NAME;
static readonly DEBUG = DEBUG;
static readonly TARGET_NAME = TARGET_NAME;
}

View File

@ -0,0 +1,27 @@
## 1.0.21
- mock支持多参数
- describe中异步函数抛出日志信息
- 修复多测试套时,执行单个测试套会打印其他测试套的日志信息
## 1.0.14
- 堆栈信息打印到cmd
## 1.0.15
- 支持获取测试代码的失败堆栈信息
- mock代码迁移至harmock包
- 适配arkts语法
- 修复覆盖率数据容易截断的bug
## 1.0.16
- 修改覆盖率文件生成功能
- 修改静态方法无法ignoreMock函数
- ## 1.0.17
- 修改not断言失败提示日志
- 自定义错误message信息
- 添加xdescribe, xit API功能
- ## 1.0.18
- 添加全局变量存储API get set
- 自定义断言功能
## 1.0.18-rc.0
添加框架worker执行能力
## 1.0.19
规范日志格式
# 1.0.20
代码告警整改

View File

@ -0,0 +1,177 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

View File

@ -0,0 +1,224 @@
<div style="text-align: center;font-size: xxx-large" >Hypium</div>
<div style="text-align: center">A unit test framework for OpenHarmonyOS application</div>
## Hypium是什么?
***
- Hypium是OpenHarmony上的测试框架提供测试用例编写、执行、结果显示能力用于OpenHarmony系统应用接口以及应用界面测试。
- Hypium结构化模型hypium工程主要由List.test.js与TestCase.test.js组成。
```
rootProject // Hypium工程根目录
├── moduleA
│   ├── src
│      ├── main // 被测试应用目录
│      ├── ohosTest // 测试用例目录
│         ├── js/ets
│            └── test
│               └── List.test.js // 测试用例加载脚本ets目录下为.ets后缀
│               └── TestCase.test.js // 测试用例脚本ets目录下为.ets后缀
└── moduleB
...
│               └── List.test.js // 测试用例加载脚本ets目录下为.ets后缀
│               └── TestCase.test.js // 测试用例脚本ets目录下为.ets后缀
```
## 安装使用
```javascript
ohpm install @ohos/hypium
```
***
- 在DevEco Studio内使用Hypium
- 工程级package.json内配置:
```json
"dependencies": {
"@ohos/hypium": "1.0.21"
}
```
注:
hypium服务于OpenHarmonyOS应用对外接口测试、系统对外接口测试SDK中接口完成HAP自动化测试。详细指导
[Deveco Studio](https://developer.harmonyos.com/cn/develop/deveco-studio)
#### 通用语法
- 测试用例采用业内通用语法describe代表一个测试套 it代表一条用例。
| No. | API | 功能说明 |
| --- | ---------- | ---------------------------------------------------------------------------------------------------------------------- |
| 1 | describe | 定义一个测试套,支持两个参数:测试套名称和测试套函数 |
| 2 | beforeAll | 在测试套内定义一个预置条件,在所有测试用例开始前执行且仅执行一次,支持一个参数:预置动作函数 |
| 3 | beforeEach | 在测试套内定义一个单元预置条件在每条测试用例开始前执行执行次数与it定义的测试用例数一致支持一个参数预置动作函数 |
| 4 | afterEach | 在测试套内定义一个单元清理条件在每条测试用例结束后执行执行次数与it定义的测试用例数一致支持一个参数清理动作函数 |
| 5 | afterAll | 在测试套内定义一个清理条件,在所有测试用例结束后执行且仅执行一次,支持一个参数:清理动作函数 |
| 6 | it | 定义一条测试用例,支持三个参数:用例名称,过滤参数和用例函数 |
| 7 | expect | 支持bool类型判断等多种断言方法 |
#### 断言库
- 示例代码:
```javascript
expect(${actualvalue}).assertX(${expectvalue})
```
- 断言功能列表:
| No. | API | 功能说明 |
| :--- | :------------------------------- | ---------------------------------------------------------------------------------------------- |
| 1 | assertClose | 检验actualvalue和expectvalue(0)的接近程度是否是expectValue(1) |
| 2 | assertContain | 检验actualvalue中是否包含expectvalue |
| 3 | assertDeepEquals | @since1.0.4 检验actualvalue和expectvalue(0)是否是同一个对象 |
| 4 | assertEqual | 检验actualvalue是否等于expectvalue[0] |
| 5 | assertFail | 抛出一个错误 |
| 6 | assertFalse | 检验actualvalue是否是false |
| 7 | assertTrue | 检验actualvalue是否是true |
| 8 | assertInstanceOf | 检验actualvalue是否是expectvalue类型 |
| 9 | assertLarger | 检验actualvalue是否大于expectvalue |
| 10 | assertLess | 检验actualvalue是否小于expectvalue |
| 11 | assertNaN | @since1.0.4 检验actualvalue是否是NaN |
| 12 | assertNegUnlimited | @since1.0.4 检验actualvalue是否等于Number.NEGATIVE_INFINITY |
| 13 | assertNull | 检验actualvalue是否是null |
| 14 | assertPosUnlimited | @since1.0.4 检验actualvalue是否等于Number.POSITIVE_INFINITY |
| 15 | assertPromiseIsPending | @since1.0.4 检验actualvalue是否处于Pending状态【actualvalue为promse对象】 |
| 16 | assertPromiseIsRejected | @since1.0.4 检验actualvalue是否处于Rejected状态【同15】 |
| 17 | assertPromiseIsRejectedWith | @since1.0.4 检验actualvalue是否处于Rejected状态并且比较执行的结果值【同15】 |
| 18 | assertPromiseIsRejectedWithError | @since1.0.4 检验actualvalue是否处于Rejected状态并有异常同时比较异常的类型和message值【同15】 |
| 19 | assertPromiseIsResolved | @since1.0.4 检验actualvalue是否处于Resolved状态【同15】 |
| 20 | assertPromiseIsResolvedWith | @since1.0.4 检验actualvalue是否处于Resolved状态并且比较执行的结果值【同15】 |
| 21 | assertThrowError | 检验actualvalue抛出Error内容是否是expectValue |
| 22 | assertUndefined | 检验actualvalue是否是undefined |
| 23 | not | @since1.0.4 断言结果取反 |
示例代码:
```javascript
import { describe, it, expect } from '@ohos/hypium';
export default async function assertCloseTest() {
describe('assertClose', function () {
it('assertClose_success', 0, function () {
let a = 100;
let b = 0.1;
expect(a).assertClose(99, b);
})
})
}
```
#### 公共系统能力
| No. | API | 功能描述 |
| ---- | ------------------------------------------------------- | ------------------------------------------------------------ |
| 1 | existKeyword(keyword: string, timeout: number): boolean | @since1.0.3 hilog日志中查找指定字段是否存在keyword是待查找关键字timeout为设置的查找时间 |
| 2 | actionStart(tag: string): void | @since1.0.3 cmd窗口输出开始tag |
| 3 | actionEnd(tag: string): void | @since1.0.3 cmd窗口输出结束tag |
示例代码:
```javascript
import { describe, it, expect, SysTestKit} from '@ohos/hypium';
export default function existKeywordTest() {
describe('existKeywordTest', function () {
it('existKeyword',DEFAULT, async function () {
console.info("HelloTest");
let isExist = await SysTestKit.existKeyword('HelloTest');
console.info('isExist ------>' + isExist);
})
})
}
```
```javascript
import { describe, it, expect, SysTestKit} from '@ohos/hypium';
export default function actionTest() {
describe('actionTest', function () {
it('existKeyword',DEFAULT, async function () {
let tag = '[MyTest]';
SysTestKit.actionStart(tag);
//do something
SysTestKit.actionEnd(tag);
})
})
}
```
#### 专项能力
- 测试用例属性筛选能力hypium支持根据用例属性筛选执行指定测试用例使用方式是先在测试用例上标记用例属性后再在测试应用的启动shell命令后新增" -s ${Key} ${Value}"。
| Key | 含义说明 | Value取值范围 |
| -------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| level | 用例级别 | "0","1","2","3","4", 例如:-s level 1 |
| size | 用例粒度 | "small","medium","large", 例如:-s size small |
| testType | 用例测试类型 | "function","performance","power","reliability","security","global","compatibility","user","standard","safety","resilience", 例如:-s testType function |
示例代码
```javascript
import { describe, it, expect, TestType, Size, Level } from '@ohos/hypium';
export default function attributeTest() {
describe('attributeTest', function () {
it("testAttributeIt", TestType.FUNCTION | Size.SMALLTEST | Level.LEVEL0, function () {
console.info('Hello Test');
})
})
}
```
示例命令
```shell
XX -s level 1 -s size small -s testType function
```
该命令的作用是筛选测试应用中同时满足a用例级别是1 b用例粒度是small c用例测试类型是function 三个条件的用例执行。
- 测试套/测试用例名称筛选能力(测试套与用例名称用“#”号连接,多个用“,”英文逗号分隔)
| Key | 含义说明 | Value取值范围 |
| -------- | ----------------------- | -------------------------------------------------------------------------------------------- |
| class | 指定要执行的测试套&用例 | ${describeName}#${itName}${describeName} , 例如:-s class attributeTest#testAttributeIt |
| notClass | 指定不执行的测试套&用例 | ${describeName}#${itName}${describeName} , 例如:-s notClass attributeTest#testAttributeIt |
示例命令
```shell
XX -s class attributeTest#testAttributeIt,abilityTest#testAbilityIt
```
该命令的作用是筛选测试应用中attributeTest测试套下的testAttributeIt测试用例abilityTest测试套下的testAbilityIt测试用例只执行这两条用例。
- 其他能力
| 能力项 | Key | 含义说明 | Value取值范围 |
| ------------ | ------- | ---------------------------- | ---------------------------------------------- |
| 随机执行能力 | random | 测试套&测试用例随机执行 | true, 不传参默认为false 例如:-s random true |
| 空跑能力 | dryRun | 显示要执行的测试用例信息全集 | true , 不传参默认为false例如-s dryRun true |
| 异步超时能力 | timeout | 异步用例执行的超时时间 | 正整数 , 单位ms例如-s timeout 5000 |
##### 约束限制
随机执行能力和空跑能力从npm包1.0.3版本开始支持
#### Mock能力
##### 约束限制
单元测试框架Mock能力从npm包[1.0.1版本](https://repo.harmonyos.com/#/cn/application/atomService/@ohos%2Fhypium/v/1.0.1)开始支持
## 约束
***
本模块首批接口从OpenHarmony SDK API version 8开始支持。
## Hypium开放能力隐私声明
- 我们如何收集和使用您的个人信息
您在使用集成了Hypium开放能力的测试应用时Hypium不会处理您的个人信息。
- SDK处理的个人信息
不涉及。
- SDK集成第三方服务声明
不涉及。
- SDK数据安全保护
不涉及。
- SDK版本更新声明
为了向您提供最新的服务我们会不时更新Hypium版本。我们强烈建议开发者集成使用最新版本的Hypium。

Some files were not shown because too many files have changed in this diff Show More