@buaa_smat/hometrans 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +113 -53
- package/package.json +8 -2
- package/resource/choose_editor.png +0 -0
- package/resource/finish_init.png +0 -0
- package/skills/hmos-convert-pipeline/SKILL.md +5 -21
- package/skills/hmos-incremental-ui-align/SKILL.md +19 -17
- package/skills/hmos-incremental-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243/MVVM/346/250/241/345/274/217/357/274/210V1/357/274/211.md +0 -911
package/README.md
CHANGED
|
@@ -13,13 +13,110 @@ npm install -g @buaa_smat/hometrans
|
|
|
13
13
|
ht init
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
Requires Node.js **>= 18**.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Setup Guide
|
|
21
|
+
|
|
22
|
+
After running `ht init`, follow these steps to complete the installation:
|
|
23
|
+
|
|
24
|
+
### Step 1: Choose your target editor
|
|
25
|
+
|
|
26
|
+
`ht init` automatically detects supported editors (Claude Code, Cursor, OpenCode, Codex) and presents an interactive selection prompt. Use **Space** to toggle and **Enter** to confirm.
|
|
27
|
+
|
|
28
|
+

|
|
29
|
+
|
|
30
|
+
### Step 2: Finish initialization
|
|
31
|
+
|
|
32
|
+
After confirming your editor selection, HomeTrans installs all bundled **skills**, **agents**, and the **MCP server** into each selected editor's configuration directory. The installation is idempotent — re-running `ht init` overwrites only bundled content.
|
|
33
|
+
|
|
34
|
+

|
|
35
|
+
|
|
36
|
+
> **Tip:** Run `ht init --all` to skip the editor prompt and install to every detected editor automatically.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## HarmonyOS Migration Guide
|
|
41
|
+
|
|
42
|
+
鸿蒙软件迁移指导,提供完整的 Android 到 HarmonyOS 迁移流程:
|
|
43
|
+
|
|
44
|
+
### Step 1: Resource Conversion (资源转换)
|
|
45
|
+
|
|
46
|
+
> `hmos-batch-ui-align` 内部会自动调用资源转换;如果是批量UI迁移场景则可跳过此步骤。
|
|
47
|
+
|
|
48
|
+
将 Android 资源文件(图片、字符串、颜色、尺寸等)转换为 HarmonyOS 格式。
|
|
17
49
|
|
|
18
|
-
```
|
|
19
|
-
|
|
50
|
+
```
|
|
51
|
+
/hmos-resources-convert android-project-path=<安卓项目路径> harmonyos-project-path=<鸿蒙项目路径> resource_mapping_path=<资源映射文档路径>
|
|
20
52
|
```
|
|
21
53
|
|
|
22
|
-
|
|
54
|
+
| 参数 | 类型 | 说明 |
|
|
55
|
+
|------|------|------|
|
|
56
|
+
| `android-project-path` | **必选** | Android 项目根目录路径(含 `build.gradle` 或 `build.gradle.kts`) |
|
|
57
|
+
| `harmonyos-project-path` | **必选** | HarmonyOS 项目输出路径 |
|
|
58
|
+
| `resource_mapping_path` | **必选** | Android ↔ HarmonyOS 资源映射文档的完整输出路径(`.md`) |
|
|
59
|
+
| `apk-path` | 可选 | Android APK 文件路径;提供后跳过 Gradle 构建和 apktool 解包步骤,直接从 APK 提取资源 |
|
|
60
|
+
|
|
61
|
+
### Step 2: UI Migration (UI迁移)
|
|
62
|
+
|
|
63
|
+
运行以下命令之一完成 Android UI 的全量或增量迁移。
|
|
64
|
+
|
|
65
|
+
#### 全量迁移 — `hmos-batch-ui-align`
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
/hmos-batch-ui-align android_project_dir=<安卓项目路径> harmony_project_dir=<鸿蒙项目路径> ui_info_root=<页面快照目录>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
| 参数 | 类型 | 说明 |
|
|
72
|
+
|------|------|------|
|
|
73
|
+
| `android_project_dir` | **必选** | Android 项目根目录路径 |
|
|
74
|
+
| `harmony_project_dir` | **必选** | HarmonyOS 项目输出路径 |
|
|
75
|
+
| `ui_info_root` | 可选 | 包含 `page_NNNN_ActivityName` 格式子目录的父目录路径(每子目录含 `meta.json` + `view.xml` + 可选 `screenshot.png`);不提供时 skill 自动通过 ADB 抓取 |
|
|
76
|
+
| `pages` | 可选 | 显式列出的页面子集(如不提供则处理所有页面) |
|
|
77
|
+
|
|
78
|
+
#### 增量迁移 — `hmos-incremental-ui-align`
|
|
79
|
+
|
|
80
|
+
项目路径通过 `config.json` 文件配置(参考 skill 目录下的 `config-example.json`),对齐目标在消息中描述(如 "帮我对齐设置页面的关于页面")。
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
/hmos-incremental-ui-align config-path=<config.json路径>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
| 参数 | 类型 | 说明 |
|
|
87
|
+
|------|------|------|
|
|
88
|
+
| `config-path` | **必选** | `config.json` 文件路径,包含 `android.project_dir`, `harmony.project_dir`, `hmos_sdk_dir`, `glm_api_key`, `capture_output_dir` 等字段 |
|
|
89
|
+
|
|
90
|
+
### Step 3: Generate Spec (生成需求规格)
|
|
91
|
+
|
|
92
|
+
运行 `hmos-spec-generate` 从原始需求描述 `.txt` 文件生成原子场景需求规格文档。
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
/hmos-spec-generate requirement-description-file=<需求描述文件路径> android-project-path=<安卓项目路径> spec-output-dir=<规格输出目录>
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
| 参数 | 类型 | 说明 |
|
|
99
|
+
|------|------|------|
|
|
100
|
+
| `requirement-description-file` | **必选** | 需求描述 `.txt` 文件路径(每段以 `REQ` 开头,空行分隔) |
|
|
101
|
+
| `android-project-path` | **必选** | Android 项目根目录路径(必须位于 Git 仓库内) |
|
|
102
|
+
| `spec-output-dir` | **必选** | 规格文档输出目录(自动创建;每个 REQ 生成 `<feature>-SPEC.md` + `.trace/<feature>.md`) |
|
|
103
|
+
|
|
104
|
+
### Step 4: Convert Pipeline (逻辑代码开发)
|
|
105
|
+
|
|
106
|
+
运行 `hmos-convert-pipeline` 完成后续的逻辑代码开发、代码检视、集成测试流程。
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
/hmos-convert-pipeline android-project-path=<安卓项目路径> harmonyos-project-path=<鸿蒙项目路径> assets-output-path=<输出报告目录>
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
| 参数 | 类型 | 说明 |
|
|
113
|
+
|------|------|------|
|
|
114
|
+
| `android-project-path` | **必选** | Android 项目根目录路径 |
|
|
115
|
+
| `harmonyos-project-path` | **必选** | HarmonyOS 项目根目录路径 |
|
|
116
|
+
| `assets-output-path` | **必选** | 输出/报告文件的存放目录(需包含 `plan.md` 需求规格文件) |
|
|
117
|
+
| `max-rounds-review` | 可选 | 代码检视循环最大轮数(正整数 `>= 1`,默认 `2`) |
|
|
118
|
+
| `max-rounds-test` | 可选 | 自测循环最大轮数(正整数 `>= 1`,默认 `2`) |
|
|
119
|
+
| `skip-test` | 可选 | `true` 跳过集成测试阶段(无真机验证环境时需要设置为true,默认 `false`) |
|
|
23
120
|
|
|
24
121
|
---
|
|
25
122
|
|
|
@@ -66,55 +163,17 @@ Requires Node.js **>= 18**.
|
|
|
66
163
|
|
|
67
164
|
---
|
|
68
165
|
|
|
69
|
-
## What `ht mcp` does
|
|
70
|
-
|
|
71
|
-
Starts a stdio-mode MCP server exposing the following tool:
|
|
72
|
-
|
|
73
|
-
### `extract_commit_context`
|
|
74
|
-
|
|
75
|
-
Extracts code context index from a given git commit in a HarmonyOS project (git diff + ArkTS semantic dependency analysis), for use during AI-assisted Android-to-HarmonyOS code review.
|
|
76
|
-
|
|
77
|
-
**Input parameters:**
|
|
78
|
-
|
|
79
|
-
| Parameter | Type | Required | Description |
|
|
80
|
-
|-----------|------|----------|-------------|
|
|
81
|
-
| `projectPath` | string | Yes | Absolute path to HarmonyOS project root (must contain `.git`) |
|
|
82
|
-
| `commitId` | string | Yes | Git commit ID (diffs against its first parent) |
|
|
83
|
-
| `mode` | string | No | `"default"` builds full call graph; other values skip (default: `"default"`) |
|
|
84
|
-
| `ohosSdkPath` | string | No | OpenHarmony SDK ETS path; falls back to env var `OHOS_SDK_PATH` |
|
|
85
|
-
| `hmsSdkPath` | string | No | HMS SDK ETS path; falls back to env var `HMS_SDK_PATH` |
|
|
86
|
-
|
|
87
|
-
**Output:** `{path, kind, ranges?, resourceNames?}[]`
|
|
88
|
-
- `source` files: 1-based `[start, end]` line-range arrays
|
|
89
|
-
- `resource` files: list of referenced resource names
|
|
90
|
-
|
|
91
|
-
---
|
|
92
|
-
|
|
93
166
|
## Skills
|
|
94
167
|
|
|
95
|
-
| Skill | Trigger phrases | Description |
|
|
96
|
-
|
|
97
|
-
| `hmos-convert-pipeline` | "full Android-to-HarmonyOS pipeline", "run the conversion pipeline end-to-end", "hmos-convert-pipeline" | Runs all conversion agents in sequence with progress tracking, duration stats, and defect recording.
|
|
98
|
-
| `hmos-spec-generate` | "spec generation", "generate spec", "requirement to spec", "atomic scenarios", "scenario decomposition", "req to spec" | Generates atomic-scenario requirement specs from raw `.txt` requirement batches. Reads REQ blocks, explores the Android code graph via GitNexus, writes per-REQ trace files, then synthesizes specs from the trace |
|
|
99
|
-
| `hmos-resources-convert` | "Android resources to HarmonyOS", "migrate Android res", "convert drawables/strings/colors" | Converts Android project resources (strings, colors, dimensions, images, drawables, icons) into HarmonyOS resources, including qualifier directories and XML→JSON format conversion |
|
|
100
|
-
| `hmos-incremental-ui-align` | "UI对齐", "页面对齐", "和安卓对齐", "鸿蒙页面修复", "UI增量开发", "align HarmonyOS with Android" | Automated HarmonyOS-Android UI alignment: navigates to target pages on both devices, captures view trees + screenshots, then aligns HarmonyOS code to match Android |
|
|
101
|
-
| `hmos-batch-ui-align` | "把安卓页面迁移到鸿蒙", "Android UI 转鸿蒙", "批量转 ArkTS" | Batch-converts multiple Android Activity UI snapshots (`page_NNNN_ActivityName`) to HarmonyOS ArkUI (ArkTS) pages |
|
|
102
|
-
| `hmos-integration-test` | "跑自测", "运行自测", "自动测试", "设备测试", "self test", "run autotest" | Runs on-device self-test for a HarmonyOS app: parses `test_case.md`, installs the HAP, executes AutoTest, and produces a verification report — optionally entering a test-and-fix loop |
|
|
103
|
-
|
|
104
|
-
### `hmos-convert-pipeline` Arguments
|
|
105
|
-
|
|
106
|
-
1. `android-project-path` (required): path to the Android source project
|
|
107
|
-
2. `harmonyos-project-path` (required): path to the target HarmonyOS project
|
|
108
|
-
3. `assets-output-path` (required): directory for output/report files
|
|
109
|
-
4. `max-rounds-review` (optional, default `2`): max Code Review → Fix loop rounds
|
|
110
|
-
5. `max-rounds-test` (optional, default `2`): max Self-Test → Fix loop rounds
|
|
111
|
-
6. `variant` (optional, default `enhanced`): `enhanced` | `baseline` — selects the Stage 1/1a agent family
|
|
112
|
-
|
|
113
|
-
### `hmos-spec-generate` Arguments
|
|
114
|
-
|
|
115
|
-
1. `requirement-description-file` (required): absolute path to a single `.txt` file containing one or more REQ blocks separated by blank lines
|
|
116
|
-
2. `android-project-path` (required): absolute path to the Android project root (must be inside a Git repo)
|
|
117
|
-
3. `spec-output-dir` (required): absolute path to the directory where spec files will be written
|
|
168
|
+
| Skill | Trigger phrases | Description | Prerequisites | Arguments | Example |
|
|
169
|
+
|-------|-----------------|-------------|---------------|-----------|--------|
|
|
170
|
+
| `hmos-convert-pipeline` | "full Android-to-HarmonyOS pipeline", "run the conversion pipeline end-to-end", "hmos-convert-pipeline" | Runs all conversion agents in sequence with progress tracking, duration stats, and defect recording. 4 stages: Logic Development (Context Builder) → Logic Coding → Build → Code Review/Fix/Rebuild loop → Self-Test/Fix/Rebuild loop | `assets-output-path` 下需存在 `plan.md` 需求规格文件 | `android-project-path` (required), `harmonyos-project-path` (required), `assets-output-path` (required), `max-rounds-review` (optional, default 2), `max-rounds-test` (optional, default 2), `skip-test` (optional, default false) | `"/hmos-convert-pipeline android-project-path=D:/path/to/android harmonyos-project-path=D:/path/to/harmonyos assets-output-path=D:/path/to/output"` |
|
|
171
|
+
| `hmos-spec-generate` | "spec generation", "generate spec", "requirement to spec", "atomic scenarios", "scenario decomposition", "req to spec" | Generates atomic-scenario requirement specs from raw `.txt` requirement batches. Reads REQ blocks, explores the Android code graph via GitNexus, writes per-REQ trace files, then synthesizes specs from the trace | 需求描述文件 (`.txt`),每段以 `REQ` 开头 | `requirement-description-file` (required), `android-project-path` (required), `spec-output-dir` (required) | `"/hmos-spec-generate requirement-description-file=D:/path/to/req.txt android-project-path=D:/path/to/android spec-output-dir=D:/path/to/specs"` |
|
|
172
|
+
| `hmos-resources-convert` | "Android resources to HarmonyOS", "migrate Android res", "convert drawables/strings/colors" | Converts Android project resources (strings, colors, dimensions, images, drawables, icons) into HarmonyOS resources, including qualifier directories and XML→JSON format conversion | Android 项目中需包含 `res/` 资源目录 | `android-project-path` (required), `harmonyos-project-path` (required), `resource_mapping_path` (required), `apk-path` (optional) | `"/hmos-resources-convert android-project-path=D:/path/to/android harmonyos-project-path=D:/path/to/harmonyos resource_mapping_path=D:/path/to/resource_mapping.md"` |
|
|
173
|
+
| `hmos-incremental-ui-align` | "UI对齐", "页面对齐", "和安卓对齐", "鸿蒙页面修复", "UI增量开发", "align HarmonyOS with Android" | Automated HarmonyOS-Android UI alignment: navigates to target pages on both devices, captures view trees + screenshots, then aligns HarmonyOS code to match Android | 需连接 Android 设备进行 UI 对比;`config-path` 指向的 `config.json` 中需配置项目路径、SDK 路径、API key | `config-path` (required) | `"/hmos-incremental-ui-align config-path=D:/path/to/config.json 帮我对齐设置页面的关于页面"` |
|
|
174
|
+
| `hmos-batch-ui-align` | "把安卓页面迁移到鸿蒙", "Android UI 转鸿蒙", "批量转 ArkTS" | Batch-converts multiple Android Activity UI snapshots (`page_NNNN_ActivityName`) to HarmonyOS ArkUI (ArkTS) pages | `ui_info_root` 下需包含 `page_NNNN_ActivityName` 格式的页面快照子目录 | `android_project_dir` (required), `harmony_project_dir` (required), `ui_info_root` (optional), `pages` (optional) | `"/hmos-batch-ui-align android_project_dir=D:/path/to/android harmony_project_dir=D:/path/to/harmonyos ui_info_root=D:/path/to/pages"` |
|
|
175
|
+
| `hmos-integration-test` | "跑自测", "运行自测", "自动测试", "设备测试", "self test", "run autotest" | Runs on-device self-test for a HarmonyOS app: parses `test_case.md`, installs the HAP, executes AutoTest, and produces a verification report — optionally entering a test-and-fix loop | 需存在 `test_case.md` 测试用例文件,设备需安装待测 HAP | `test-case-path` (required), `hap-path` (required), `output-path` (optional), `pre-test-case-path` (optional), `android-project-path` (optional), `max-rounds` (optional, default 3) | `"/hmos-integration-test test-case-path=D:/path/to/test_case.md hap-path=D:/path/to/app-signed.hap"` |
|
|
176
|
+
| `hmos-fix-build-errors` | "build HarmonyOS project", "fix compile errors", "auto build and fix", "hmos-fix-build-errors" | Builds a HarmonyOS NEXT project from the command line, parses compile errors, fixes them, and retries in a loop until the build succeeds. Default produces an unsigned HAP; `--signed` produces a signed HAP | 有效的 HarmonyOS 项目(含 `build-profile.json5`、`entry/src`、`oh-package.json5`)+ DevEco Studio 安装目录;`--signed` 时签名配置须已存在于 `build-profile.json5` | `harmonyos-project-path` (required), `deveco-studio-path` (required), `--signed` (optional) | `"/hmos-fix-build-errors D:/MyHmosApp \"D:/DevEco Studio\" --signed"` |
|
|
118
177
|
|
|
119
178
|
---
|
|
120
179
|
|
|
@@ -125,14 +184,15 @@ Extracts code context index from a given git commit in a HarmonyOS project (git
|
|
|
125
184
|
| `logic-context-builder` | Constrains HarmonyOS ArkTS app changes into an executable decision contract |
|
|
126
185
|
| `logic-coder` | Executes HarmonyOS ArkTS code implementation from the decision contract (ships with `scripts/platform_context_query.py`) |
|
|
127
186
|
| `build-fixer` | Automatically builds a HarmonyOS project, parses compile errors, fixes them, and retries in a loop until the build succeeds |
|
|
128
|
-
| `code-reviewer` | Reviews HarmonyOS code against user scenarios to validate functional coverage
|
|
187
|
+
| `code-reviewer` | Reviews HarmonyOS code against user scenarios to validate functional coverage |
|
|
188
|
+
| `review-fixer` | Fixes issues from code review reports — verifies each issue before fixing, references Android source, uses cautious per-scenario fix strategies |
|
|
129
189
|
| `spec-generator` | Generates requirement spec documents for each requirement description file in a folder by exploring the Android codebase and decomposing into atomic user scenarios |
|
|
130
190
|
| `self-tester` | Unified self-test agent — parses `test_case.md` into `testcases.json`, runs on-device AutoTest verification, and produces a report. A `setup` boolean controls whether the parse phase runs (skip on round-2+ to reuse prior artifacts) |
|
|
131
191
|
| `self-test-fixer` | Fixes issues identified by self-testing — reads the self-test report, white-box verifies failures, plans and executes fixes in order |
|
|
132
192
|
|
|
133
193
|
---
|
|
134
194
|
|
|
135
|
-
|
|
195
|
+
|
|
136
196
|
|
|
137
197
|
<p align="center">
|
|
138
198
|
<a href="https://gitcode.com/SMAT/HomeTrans">Official Repository</a> •
|
package/package.json
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@buaa_smat/hometrans",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "HomeTrans (Android-to-HarmonyOS) skill + agent installer. Run `ht init` to distribute conversion skills and subagents into AI editors.",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://gitcode.com/SMAT/HomeTrans"
|
|
9
|
+
},
|
|
6
10
|
"type": "module",
|
|
7
11
|
"bin": {
|
|
8
12
|
"hometrans": "dist/cli/index.js",
|
|
@@ -12,7 +16,9 @@
|
|
|
12
16
|
"dist",
|
|
13
17
|
"skills",
|
|
14
18
|
"agents",
|
|
15
|
-
|
|
19
|
+
|
|
20
|
+
"tools",
|
|
21
|
+
"resource"
|
|
16
22
|
],
|
|
17
23
|
"scripts": {
|
|
18
24
|
"build": "node scripts/build.js",
|
|
Binary file
|
|
Binary file
|
|
@@ -20,10 +20,9 @@ Parse `$ARGUMENTS` as positional tokens:
|
|
|
20
20
|
- **Arg 3** (`assets-output-path`): Directory to store all output/report files (required)
|
|
21
21
|
- **Arg 4** (`max-rounds-review`): Maximum number of Stage 3→3a→3b code-review-fix rounds to run (optional, default `2`). Must be a positive integer `>= 1`.
|
|
22
22
|
- **Arg 5** (`max-rounds-test`): Maximum number of Stage 4→4a→4b self-test rounds to run (optional, default `2`). Must be a positive integer `>= 1`.
|
|
23
|
-
- **Arg 6** (`
|
|
24
|
-
- **Arg 7** (`skip-test`): `true` or `false` (optional, default `false`). When `true`, skip Stage 4 / 4a / 4b (Self-Testing Loop) entirely. Use this when no real HarmonyOS device is available for on-device testing. To pass this arg, provide explicit values for Args 4, 5, and 6 first.
|
|
23
|
+
- **Arg 6** (`skip-test`): `true` or `false` (optional, default `false`). When `true`, skip Stage 4 / 4a / 4b (Self-Testing Loop) entirely. Use this when no real HarmonyOS device is available for on-device testing. To pass this arg, provide explicit values for Args 4 and 5 first.
|
|
25
24
|
|
|
26
|
-
If any required argument is missing, ask the user before proceeding. If `max-rounds-review` or `max-rounds-test` is provided but is not a positive integer, ask the user before proceeding. If `
|
|
25
|
+
If any required argument is missing, ask the user before proceeding. If `max-rounds-review` or `max-rounds-test` is provided but is not a positive integer, ask the user before proceeding. If `skip-test` is provided but is not `true` or `false`, ask the user before proceeding.
|
|
27
26
|
|
|
28
27
|
Define shorthand variables for the instructions below:
|
|
29
28
|
|
|
@@ -34,7 +33,6 @@ Define shorthand variables for the instructions below:
|
|
|
34
33
|
| `OUTPUT` | assets-output-path (resolved or generated) |
|
|
35
34
|
| `MAX_ROUNDS_REVIEW` | max-rounds-review — positive integer (default `2`). Controls Stage 3→3a→3b code-review-fix loop. |
|
|
36
35
|
| `MAX_ROUNDS_TEST` | max-rounds-test — positive integer (default `2`). Controls Stage 4→4a→4b self-test loop. |
|
|
37
|
-
| `VARIANT` | variant — `enhanced` (default) or `baseline`. `baseline` switches Stage 1/1a to the minimal pure-LLM baseline agents. |
|
|
38
36
|
| `SKIP_TEST` | skip-test — `true` or `false` (default `false`). When `true`, skip Stage 4 / 4a / 4b entirely (no real device available). |
|
|
39
37
|
|
|
40
38
|
---
|
|
@@ -145,40 +143,26 @@ Flags: `skip-plan-builder: true|false` (default `false`) — when `true`, skip t
|
|
|
145
143
|
|
|
146
144
|
Prompt format (applies to both Stage 1 and Stage 1a): ONLY the key-value lines below. No natural language, step lists, or Markdown.
|
|
147
145
|
|
|
148
|
-
1. Launch the logic context builder agent
|
|
146
|
+
1. Launch the logic context builder agent:
|
|
149
147
|
```
|
|
150
|
-
# VARIANT=enhanced (default) — full agent
|
|
151
148
|
Agent(
|
|
152
149
|
subagent_type="logic-context-builder",
|
|
153
150
|
prompt="harmonyos-project-path: HMOS\nspec-file: OUTPUT/plan.md\noutput-path: OUTPUT/logic"
|
|
154
151
|
)
|
|
155
|
-
|
|
156
|
-
# VARIANT=baseline — pure-LLM baseline agent
|
|
157
|
-
Agent(
|
|
158
|
-
subagent_type="logic-context-builder-minimal",
|
|
159
|
-
prompt="harmonyos-project-path: HMOS\nspec-file: OUTPUT/plan.md\noutput-path: OUTPUT/logic"
|
|
160
|
-
)
|
|
161
152
|
```
|
|
162
153
|
2. Verify `OUTPUT/logic/plan.md` exists.
|
|
163
154
|
|
|
164
155
|
### Stage 1a — Logic Coding
|
|
165
156
|
|
|
166
|
-
1. Launch the logic coding agent
|
|
157
|
+
1. Launch the logic coding agent:
|
|
167
158
|
```
|
|
168
|
-
# VARIANT=enhanced (default) — full agent
|
|
169
159
|
Agent(
|
|
170
160
|
subagent_type="logic-coder",
|
|
171
161
|
prompt="harmonyos-project-path: HMOS\nplan-file: OUTPUT/logic/plan.md\noutput-path: OUTPUT/logic"
|
|
172
162
|
)
|
|
173
|
-
|
|
174
|
-
# VARIANT=baseline — pure-LLM baseline agent
|
|
175
|
-
Agent(
|
|
176
|
-
subagent_type="logic-coding-minimal",
|
|
177
|
-
prompt="harmonyos-project-path: HMOS\nplan-file: OUTPUT/logic/plan.md\noutput-path: OUTPUT/logic"
|
|
178
|
-
)
|
|
179
163
|
```
|
|
180
164
|
2. Copy `OUTPUT/logic/commit-info.md` → `OUTPUT/commit-info.md` and verify it contains a hex `commit-id`.
|
|
181
|
-
3. `OUTPUT/logic/coding-summary.md`
|
|
165
|
+
3. `OUTPUT/logic/coding-summary.md` is human-readable auxiliary; downstream stages consume only `OUTPUT/commit-info.md`.
|
|
182
166
|
|
|
183
167
|
### Stage 2 — Compilation and Build
|
|
184
168
|
|
|
@@ -17,23 +17,25 @@ You are writing ArkTS codes.
|
|
|
17
17
|
- 不要做和用户需求无关的其他修改
|
|
18
18
|
-
|
|
19
19
|
## Step 0: Load Config
|
|
20
|
-
The user MUST
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
20
|
+
The user MUST provide a `config-path` when invoking this skill (e.g., `config-path: D:\path\to\config.json`). The config file must exist before running — verify with a Read, and if it's missing or unreadable, ask the user for a valid path. Refer to `config-example.json` in this skill's directory for the expected schema.
|
|
21
|
+
|
|
22
|
+
Config fields:
|
|
23
|
+
|
|
24
|
+
| Field | Description |
|
|
25
|
+
|---|---|
|
|
26
|
+
| `android.app_name` | Android app name for navigation |
|
|
27
|
+
| `android.package` | Android app package name |
|
|
28
|
+
| `android.project_dir` | Android source code root path |
|
|
29
|
+
| `harmony.app_name` | HarmonyOS app name for navigation |
|
|
30
|
+
| `harmony.package` | HarmonyOS app package name |
|
|
31
|
+
| `harmony.project_dir` | HarmonyOS project root path |
|
|
32
|
+
| `hmos_sdk_dir` | HarmonyOS SDK path (ETS API reference) |
|
|
33
|
+
| `glm_api_key` | Zhipu GLM API key for phone-agent |
|
|
34
|
+
| `capture_output_dir` | Base directory for captured page data |
|
|
33
35
|
|
|
34
36
|
## Step 1: Capture All Related Pages on Android & HarmonyOS Devices
|
|
35
37
|
|
|
36
|
-
Read `scripts/navigation-capure.md` to learn the usage of `
|
|
38
|
+
Read `scripts/navigation-capure.md` to learn the usage of `scripts/app_feature_verify.py` (navigation) and `scripts/page_capture.py` (capture).
|
|
37
39
|
|
|
38
40
|
### Step 1.1: Parse User Request and Build Capture Plan
|
|
39
41
|
Analyze the user's description and build a list of **base pages** to capture.
|
|
@@ -48,8 +50,8 @@ Create the timestamped output directory and per-page sub-directories following t
|
|
|
48
50
|
|
|
49
51
|
### Step 1.2: Capture Base Pages
|
|
50
52
|
For each base page in the plan, on **both** Android and HarmonyOS devices:
|
|
51
|
-
1. Use `
|
|
52
|
-
2. On success, use `
|
|
53
|
+
1. Use `scripts/app_feature_verify.py` to navigate to the page.
|
|
54
|
+
2. On success, use `scripts/page_capture.py` to capture the view tree and screenshot.
|
|
53
55
|
3. For HarmonyOS pages that don't exist yet, navigation will fail — leave the directory empty (expected).
|
|
54
56
|
|
|
55
57
|
### Step 1.3: Discover and Capture Interactive States
|
|
@@ -61,7 +63,7 @@ After capturing each base page, scan its view tree for **interactive elements th
|
|
|
61
63
|
|
|
62
64
|
**For each interactive element found:**
|
|
63
65
|
1. Create a separate capture directory: `{platform}_page_{i}_{base_name}_{state_type}_{state_name}` (e.g., `android_page_1_detail_tab_city`, `android_page_1_detail_popup_filter_identity`).
|
|
64
|
-
2. Navigate to the base page (reuse the same nav path), then append the click action to trigger the state change. Capture with `
|
|
66
|
+
2. Navigate to the base page (reuse the same nav path), then append the click action to trigger the state change. Capture with `scripts/page_capture.py`.
|
|
65
67
|
3. Repeat for **both** Android and HarmonyOS devices.
|
|
66
68
|
4. Each captured state is treated as a separate page pair in Step 2 and Step 3.
|
|
67
69
|
|
|
@@ -1,911 +0,0 @@
|
|
|
1
|
-
# MVVM模式(V1)
|
|
2
|
-
|
|
3
|
-
当开发者掌握了状态管理的基本概念后,通常会尝试开发自己的应用,在应用开发初期,如果未能精心规划项目结构,随着项目扩展和复杂化,状态变量的增多将导致组件间关系变得错综复杂。此时,开发新功能可能引起连锁反应,维护成本也会增加。为此,本文旨在介绍MVVM模式以及ArkUI的UI开发模式与MVVM的关系,指导开发者如何设计项目结构,以便在产品迭代和升级时能更轻松地开发和维护。
|
|
4
|
-
|
|
5
|
-
本文档涵盖了大多数状态管理V1装饰器,所以在阅读本文档前,建议开发者对状态管理V1有一定的了解。建议提前阅读:[状态管理概述](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-state-management-overview)和状态管理V1装饰器相关文档。
|
|
6
|
-
|
|
7
|
-
## MVVM模式介绍
|
|
8
|
-
|
|
9
|
-
### 概念
|
|
10
|
-
|
|
11
|
-
在应用开发中,UI更新需要实时同步数据状态变化,这直接影响应用程序的性能和用户体验。为了解决数据与UI同步的复杂性,ArkUI采用了 Model-View-ViewModel(MVVM)架构模式。MVVM 将应用分为Model、View和ViewModel三个核心部分,实现数据、视图与逻辑的分离。通过这种模式,UI可以自动更新状态变化,从而更高效地管理数据和视图的绑定与更新。
|
|
12
|
-
|
|
13
|
-
- **View**:用户界面层。负责用户界面展示并与用户交互,不包含任何业务逻辑。它通过绑定ViewModel层提供的数据实现动态更新。
|
|
14
|
-
- **Model**:数据访问层。以数据为中心,不直接与用户界面交互。负责数据结构定义,数据管理(获取、存储、更新等),以及业务逻辑处理。
|
|
15
|
-
- **ViewModel**:表示逻辑层。作为连接Model和View的桥梁,通常一个View对应一个ViewModel。View和ViewModel有两种通信方式:
|
|
16
|
-
1. 方法调用:View通过事件监听用户行为,在回调里面触发ViewModel层的方法。例如当View监听到用户Button点击行为,调用ViewModel对应的方法,处理用户操作。
|
|
17
|
-
2. 双向绑定:View绑定ViewModel的数据,实现双向同步。
|
|
18
|
-
|
|
19
|
-
ArkUI的UI开发模式就属于MVVM模式,通过对MVVM概念的基本介绍,开发者大致能猜到状态管理能在MVVM中起什么样的作用,状态管理旨在数据驱动更新,让开发者只用关注页面设计,而不去关注整个UI的刷新逻辑,数据的维护也无需开发者进行感知,由状态变量自动更新完成,而这就是属于ViewModel层所需要支持的内容,因此开发者使用MVVM模式开发自己的应用是最省心省力的。
|
|
20
|
-
|
|
21
|
-
### ArkUI开发模式图
|
|
22
|
-
|
|
23
|
-
ArkUI的UI开发模式即是MVVM模式,而状态变量在MVVM模式中扮演着ViewModel的角色,向上刷新UI,向下更新数据
|
|
24
|
-
|
|
25
|
-
### 分层说明
|
|
26
|
-
|
|
27
|
-
**View层**
|
|
28
|
-
|
|
29
|
-
View层通常可以分为下列组件:
|
|
30
|
-
|
|
31
|
-
- **页面组件**:所有应用基本都是按照页面进行分类的,比如登录页,列表页,编辑页,帮助页,版权页等。每个页面对应需要的数据可能是完全不一样的,也可能多个页面需要的数据是同一套。
|
|
32
|
-
- **业务组件**:本身具备本APP部分业务能力的功能组件,典型的就是这个业务组件可能关联了本项目的ViewModel中的数据,不可以被共享给其他项目使用。
|
|
33
|
-
- **通用组件**:像系统组件一样,这类组件不会关联本APP中ViewModel的数据,这些组件可实现跨越多个项目进行共享,来完成比较通用的功能。
|
|
34
|
-
|
|
35
|
-
**Model层**
|
|
36
|
-
|
|
37
|
-
Model层是应用的原始数据提供者,代表应用的核心业务逻辑和数据。
|
|
38
|
-
|
|
39
|
-
**ViewModel层**
|
|
40
|
-
|
|
41
|
-
为View层的组件提供对应数据,按照页面组织,当用户浏览页面时,某些页面可能不会被显示,因此,页面数据最好设计成懒加载(按需加载)的模式。
|
|
42
|
-
|
|
43
|
-
ViewModel层数据和Model层数据的区别:
|
|
44
|
-
|
|
45
|
-
- Model层数据是按照整个工程、项目来组织数据,构成一套完整的APP业务数据体系。
|
|
46
|
-
- ViewModel层数据,是提供某个页面上使用的数据,它可能是整个APP的业务数据的一部分。另外ViewModel层还可以附加对应Page的辅助页面显示数据,这部分数据可能与本APP的业务完全无关,仅仅是为页面展示提供便利的辅助数据。
|
|
47
|
-
|
|
48
|
-
### 架构核心原则
|
|
49
|
-
|
|
50
|
-
**不可跨层访问**
|
|
51
|
-
|
|
52
|
-
- View层不可以直接调用Model层的数据,只能通过ViewModel提供的方法进行调用。
|
|
53
|
-
- Model层不能直接操作UI,只能通知ViewModel层数据有更新,由ViewModel层更新对应的数据。
|
|
54
|
-
|
|
55
|
-
**下层不可访问上层数据**
|
|
56
|
-
|
|
57
|
-
下层数据通过通知模式更新上层数据。在业务逻辑中,下层不可直接获取上层数据。例如,ViewModel层的逻辑处理不应该依赖View层界面上的某个值。
|
|
58
|
-
|
|
59
|
-
**非父子组件间不可直接访问**
|
|
60
|
-
|
|
61
|
-
这是针对View层设计的核心原则,一个组件应该具备以下逻辑:
|
|
62
|
-
|
|
63
|
-
- 禁止直接访问父组件(必须使用事件或是订阅能力)。
|
|
64
|
-
- 禁止直接访问兄弟组件。这是因为组件应该仅能访问自己的子节点(通过传参)和父节点(通过事件或通知),以此完成组件之间的解耦。
|
|
65
|
-
|
|
66
|
-
对于一个组件,这样设计的原因如下:
|
|
67
|
-
|
|
68
|
-
- 组件自己使用了哪些子组件是明确的,因此可以访问。
|
|
69
|
-
- 组件被放置于哪个父节点下是未知的,因此组件想访问父节点,就只能通过通知或者事件能力完成。
|
|
70
|
-
- 组件不可能知道自己的兄弟节点是谁,因此组件不可以操作兄弟节点。
|
|
71
|
-
|
|
72
|
-
## 备忘录开发实战
|
|
73
|
-
|
|
74
|
-
本节通过备忘录应用的开发,使开发者了解如何使用ArkUI框架设计自己的应用。本节直接进行功能开发,未设计代码架构,即根据需求即时开发,不考虑后续维护,同时,本节还将介绍功能开发所需的装饰器。
|
|
75
|
-
|
|
76
|
-
### @State状态变量
|
|
77
|
-
|
|
78
|
-
[@State](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-state)装饰器是最常用的装饰器之一,用于定义状态变量。通常,这些状态变量作为父组件的数据源,开发者点击时,触发状态变量的更新,刷新UI。
|
|
79
|
-
|
|
80
|
-
```typescript
|
|
81
|
-
@Entry
|
|
82
|
-
@Component
|
|
83
|
-
struct StateIndex {
|
|
84
|
-
@State isFinished: boolean = false;
|
|
85
|
-
|
|
86
|
-
build() {
|
|
87
|
-
Column() {
|
|
88
|
-
Row() {
|
|
89
|
-
// 请将$r('app.string.all_tasks')替换为实际资源文件,在本示例中该资源文件的value值为"全部待办"
|
|
90
|
-
Text($r('app.string.all_tasks'))
|
|
91
|
-
.fontSize(30)
|
|
92
|
-
.fontWeight(FontWeight.Bold)
|
|
93
|
-
}
|
|
94
|
-
.width('100%')
|
|
95
|
-
.margin({ top: 10, bottom: 10 })
|
|
96
|
-
|
|
97
|
-
// 待办事项
|
|
98
|
-
Row({ space: 15 }) {
|
|
99
|
-
if (this.isFinished) {
|
|
100
|
-
// 请将$r('app.media.finished')替换为实际资源文件
|
|
101
|
-
Image($r('app.media.finished'))
|
|
102
|
-
.width(28)
|
|
103
|
-
.height(28)
|
|
104
|
-
} else {
|
|
105
|
-
// 请将$r('app.media.unfinished')替换为实际资源文件
|
|
106
|
-
Image($r('app.media.unfinished'))
|
|
107
|
-
.width(28)
|
|
108
|
-
.height(28)
|
|
109
|
-
}
|
|
110
|
-
// 请将$r('app.string.all_learn_advanced_math')替换为实际资源文件,在本示例中该资源文件的value值为"学习高数"
|
|
111
|
-
Text($r('app.string.learn_advanced_math'))
|
|
112
|
-
.fontSize(24)
|
|
113
|
-
.decoration({ type: this.isFinished ? TextDecorationType.LineThrough : TextDecorationType.None })
|
|
114
|
-
}
|
|
115
|
-
.height('40%')
|
|
116
|
-
.width('100%')
|
|
117
|
-
.border({ width: 5 })
|
|
118
|
-
.padding({ left: 15 })
|
|
119
|
-
.onClick(() => {
|
|
120
|
-
this.isFinished = !this.isFinished;
|
|
121
|
-
})
|
|
122
|
-
}
|
|
123
|
-
.height('100%')
|
|
124
|
-
.width('100%')
|
|
125
|
-
.margin({ top: 5, bottom: 5 })
|
|
126
|
-
.backgroundColor('#90f1f3f5')
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
### @Prop、@Link的作用
|
|
132
|
-
|
|
133
|
-
上述示例中,所有代码都写在了@Entry组件中。随着需要渲染的组件越来越多,@Entry组件必然需要进行拆分,为此,拆分出的子组件就需要使用@Prop和@Link装饰器:
|
|
134
|
-
|
|
135
|
-
- [@Prop](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-prop)是父子间单向传递,子组件会深拷贝父组件数据,可从父组件更新,也可自己更新数据,但不会同步回父组件。
|
|
136
|
-
- [@Link](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-link)是父子间双向传递,父组件改变,会通知所有的@Link,同时@Link的更新也会通知父组件的数据源进行刷新。
|
|
137
|
-
|
|
138
|
-
```typescript
|
|
139
|
-
@Component
|
|
140
|
-
struct PropLinkTodoComponent {
|
|
141
|
-
build() {
|
|
142
|
-
Row() {
|
|
143
|
-
// 请将$r('app.string.all_tasks')替换为实际资源文件,在本示例中该资源文件的value值为"全部待办"
|
|
144
|
-
Text($r('app.string.all_tasks'))
|
|
145
|
-
.fontSize(30)
|
|
146
|
-
.fontWeight(FontWeight.Bold)
|
|
147
|
-
}
|
|
148
|
-
.width('100%')
|
|
149
|
-
.margin({ top: 10, bottom: 10 })
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
@Component
|
|
154
|
-
struct PropLinkAllChooseComponent {
|
|
155
|
-
@Link isFinished: boolean;
|
|
156
|
-
|
|
157
|
-
build() {
|
|
158
|
-
Row() {
|
|
159
|
-
// 请将$r('app.string.check_all')替换为实际资源文件,在本示例中该资源文件的value值为"全选"
|
|
160
|
-
Button($r('app.string.check_all'), { type: ButtonType.Normal })
|
|
161
|
-
.onClick(() => {
|
|
162
|
-
this.isFinished = !this.isFinished;
|
|
163
|
-
})
|
|
164
|
-
.fontSize(30)
|
|
165
|
-
.fontWeight(FontWeight.Bold)
|
|
166
|
-
.backgroundColor('#f7f6cc74')
|
|
167
|
-
}
|
|
168
|
-
.padding({ left: 15 })
|
|
169
|
-
.width('100%')
|
|
170
|
-
.margin({ top: 10, bottom: 10 })
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
@Component
|
|
175
|
-
struct ThingComponent1 {
|
|
176
|
-
@Prop isFinished: boolean;
|
|
177
|
-
|
|
178
|
-
build() {
|
|
179
|
-
// 待办事项1
|
|
180
|
-
Row({ space: 15 }) {
|
|
181
|
-
if (this.isFinished) {
|
|
182
|
-
// 请将$r('app.media.finished')替换为实际资源文件
|
|
183
|
-
Image($r('app.media.finished'))
|
|
184
|
-
.width(28)
|
|
185
|
-
.height(28)
|
|
186
|
-
} else {
|
|
187
|
-
// 请将$r('app.media.unfinished')替换为实际资源文件
|
|
188
|
-
Image($r('app.media.unfinished'))
|
|
189
|
-
.width(28)
|
|
190
|
-
.height(28)
|
|
191
|
-
}
|
|
192
|
-
// 请将$r('app.string.learn_chinese')替换为实际资源文件,在本示例中该资源文件的value值为"学习语文"
|
|
193
|
-
Text($r('app.string.learn_chinese'))
|
|
194
|
-
.fontSize(24)
|
|
195
|
-
.decoration({ type: this.isFinished ? TextDecorationType.LineThrough : TextDecorationType.None })
|
|
196
|
-
}
|
|
197
|
-
.height('40%')
|
|
198
|
-
.width('100%')
|
|
199
|
-
.border({ width: 5 })
|
|
200
|
-
.padding({ left: 15 })
|
|
201
|
-
.onClick(() => {
|
|
202
|
-
this.isFinished = !this.isFinished;
|
|
203
|
-
})
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
@Component
|
|
208
|
-
struct ThingComponent2 {
|
|
209
|
-
@Prop isFinished: boolean;
|
|
210
|
-
|
|
211
|
-
build() {
|
|
212
|
-
// 待办事项1
|
|
213
|
-
Row({ space: 15 }) {
|
|
214
|
-
if (this.isFinished) {
|
|
215
|
-
// 请将$r('app.media.finished')替换为实际资源文件
|
|
216
|
-
Image($r('app.media.finished'))
|
|
217
|
-
.width(28)
|
|
218
|
-
.height(28)
|
|
219
|
-
} else {
|
|
220
|
-
// 请将$r('app.media.unfinished')替换为实际资源文件
|
|
221
|
-
Image($r('app.media.unfinished'))
|
|
222
|
-
.width(28)
|
|
223
|
-
.height(28)
|
|
224
|
-
}
|
|
225
|
-
// 请将$r('app.string.learn_advanced_math')替换为实际资源文件,在本示例中该资源文件的value值为"学习高数"
|
|
226
|
-
Text($r('app.string.learn_advanced_math'))
|
|
227
|
-
.fontSize(24)
|
|
228
|
-
.decoration({ type: this.isFinished ? TextDecorationType.LineThrough : TextDecorationType.None })
|
|
229
|
-
}
|
|
230
|
-
.height('40%')
|
|
231
|
-
.width('100%')
|
|
232
|
-
.border({ width: 5 })
|
|
233
|
-
.padding({ left: 15 })
|
|
234
|
-
.onClick(() => {
|
|
235
|
-
this.isFinished = !this.isFinished;
|
|
236
|
-
})
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
@Entry
|
|
241
|
-
@Component
|
|
242
|
-
struct PropLinkIndex {
|
|
243
|
-
@State isFinished: boolean = false;
|
|
244
|
-
|
|
245
|
-
build() {
|
|
246
|
-
Column() {
|
|
247
|
-
// 全部待办
|
|
248
|
-
PropLinkTodoComponent()
|
|
249
|
-
|
|
250
|
-
// 全选
|
|
251
|
-
PropLinkAllChooseComponent({ isFinished: this.isFinished })
|
|
252
|
-
|
|
253
|
-
// 待办事项1
|
|
254
|
-
ThingComponent1({ isFinished: this.isFinished })
|
|
255
|
-
|
|
256
|
-
// 待办事项2
|
|
257
|
-
ThingComponent2({ isFinished: this.isFinished })
|
|
258
|
-
}
|
|
259
|
-
.height('100%')
|
|
260
|
-
.width('100%')
|
|
261
|
-
.margin({ top: 5, bottom: 5 })
|
|
262
|
-
.backgroundColor('#90f1f3f5')
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
```
|
|
266
|
-
|
|
267
|
-
### 循环渲染组件
|
|
268
|
-
|
|
269
|
-
- 上个示例虽然拆分出了子组件,但发现组件1和组件2的代码非常相似,当渲染的组件除了数据外,其他设置都相同时,此时就需要使用[ForEach循环渲染](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-rendering-control-foreach)。
|
|
270
|
-
- ForEach使用之后,冗余代码变得更少,并且代码结构更加清晰。
|
|
271
|
-
|
|
272
|
-
```typescript
|
|
273
|
-
@Component
|
|
274
|
-
struct ForEachTodoComponent {
|
|
275
|
-
build() {
|
|
276
|
-
Row() {
|
|
277
|
-
// 请将$r('app.string.all_tasks')替换为实际资源文件,在本示例中该资源文件的value值为"全部待办"
|
|
278
|
-
Text($r('app.string.all_tasks'))
|
|
279
|
-
.fontSize(30)
|
|
280
|
-
.fontWeight(FontWeight.Bold)
|
|
281
|
-
}
|
|
282
|
-
.width('100%')
|
|
283
|
-
.margin({ top: 10, bottom: 10 })
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
@Component
|
|
288
|
-
struct ForEachAllChooseComponent {
|
|
289
|
-
@Link isFinished: boolean;
|
|
290
|
-
|
|
291
|
-
build() {
|
|
292
|
-
Row() {
|
|
293
|
-
// 请将$r('app.string.check_all')替换为实际资源文件,在本示例中该资源文件的value值为"全选"
|
|
294
|
-
Button($r('app.string.check_all'), { type: ButtonType.Normal })
|
|
295
|
-
.onClick(() => {
|
|
296
|
-
this.isFinished = !this.isFinished;
|
|
297
|
-
})
|
|
298
|
-
.fontSize(30)
|
|
299
|
-
.fontWeight(FontWeight.Bold)
|
|
300
|
-
.backgroundColor('#f7f6cc74')
|
|
301
|
-
}
|
|
302
|
-
.padding({ left: 15 })
|
|
303
|
-
.width('100%')
|
|
304
|
-
.margin({ top: 10, bottom: 10 })
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
@Component
|
|
309
|
-
struct ForEachThingComponent {
|
|
310
|
-
@Prop isFinished: boolean;
|
|
311
|
-
@Prop thing: string;
|
|
312
|
-
|
|
313
|
-
build() {
|
|
314
|
-
// 待办事项1
|
|
315
|
-
Row({ space: 15 }) {
|
|
316
|
-
if (this.isFinished) {
|
|
317
|
-
// 请将$r('app.media.finished')替换为实际资源文件
|
|
318
|
-
Image($r('app.media.finished'))
|
|
319
|
-
.width(28)
|
|
320
|
-
.height(28)
|
|
321
|
-
} else {
|
|
322
|
-
// 请将$r('app.media.unfinished')替换为实际资源文件
|
|
323
|
-
Image($r('app.media.unfinished'))
|
|
324
|
-
.width(28)
|
|
325
|
-
.height(28)
|
|
326
|
-
// ...
|
|
327
|
-
}
|
|
328
|
-
Text(`${this.thing}`)
|
|
329
|
-
.fontSize(24)
|
|
330
|
-
.decoration({ type: this.isFinished ? TextDecorationType.LineThrough : TextDecorationType.None })
|
|
331
|
-
}
|
|
332
|
-
.height('8%')
|
|
333
|
-
.width('90%')
|
|
334
|
-
.padding({ left: 15 })
|
|
335
|
-
.opacity(this.isFinished ? 0.3 : 1)
|
|
336
|
-
.border({ width: 1 })
|
|
337
|
-
.borderColor(Color.White)
|
|
338
|
-
.borderRadius(25)
|
|
339
|
-
.backgroundColor(Color.White)
|
|
340
|
-
.onClick(() => {
|
|
341
|
-
this.isFinished = !this.isFinished;
|
|
342
|
-
})
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
@Entry
|
|
347
|
-
@Component
|
|
348
|
-
struct ForEachIndex {
|
|
349
|
-
@State isFinished: boolean = false;
|
|
350
|
-
@State planList: ResourceStr[] = [
|
|
351
|
-
// 请将$r('app.string.get_up')替换为实际资源文件,在本示例中该资源文件的value值为"7.30 起床"
|
|
352
|
-
$r('app.string.get_up'),
|
|
353
|
-
// 请将$r('app.string.breakfast')替换为实际资源文件,在本示例中该资源文件的value值为"8.30 早餐"
|
|
354
|
-
$r('app.string.breakfast'),
|
|
355
|
-
// 请将$r('app.string.lunch')替换为实际资源文件,在本示例中该资源文件的value值为"11.30 中餐"
|
|
356
|
-
$r('app.string.lunch'),
|
|
357
|
-
// 请将$r('app.string.dinner')替换为实际资源文件,在本示例中该资源文件的value值为"17.30 晚餐"
|
|
358
|
-
$r('app.string.dinner'),
|
|
359
|
-
// 请将$r('app.string.midnight_snack')替换为实际资源文件,在本示例中该资源文件的value值为"21.30 夜宵"
|
|
360
|
-
$r('app.string.midnight_snack'),
|
|
361
|
-
// 请将$r('app.string.bathe')替换为实际资源文件,在本示例中该资源文件的value值为"22.30 洗澡"
|
|
362
|
-
$r('app.string.bathe'),
|
|
363
|
-
// 请将$r('app.string.sleep')替换为实际资源文件,在本示例中该资源文件的value值为"1.30 睡觉"
|
|
364
|
-
$r('app.string.sleep')
|
|
365
|
-
];
|
|
366
|
-
context1 = this.getUIContext().getHostContext();
|
|
367
|
-
|
|
368
|
-
aboutToAppear(): void {
|
|
369
|
-
for (let i = 0; i < this.planList.length; i++) {
|
|
370
|
-
this.planList[i] = this.context1!.resourceManager.getStringSync((this.planList[i] as Resource).id);
|
|
371
|
-
};
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
build() {
|
|
375
|
-
Column() {
|
|
376
|
-
// 全部待办
|
|
377
|
-
ForEachTodoComponent()
|
|
378
|
-
|
|
379
|
-
// 全选
|
|
380
|
-
ForEachAllChooseComponent({ isFinished: this.isFinished })
|
|
381
|
-
|
|
382
|
-
List() {
|
|
383
|
-
ForEach(this.planList, (item: string) => {
|
|
384
|
-
// 待办事项1
|
|
385
|
-
ForEachThingComponent({ isFinished: this.isFinished, thing: item })
|
|
386
|
-
.margin(5)
|
|
387
|
-
})
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
.height('100%')
|
|
391
|
-
.width('100%')
|
|
392
|
-
.margin({ top: 5, bottom: 5 })
|
|
393
|
-
.backgroundColor('#90f1f3f5')
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
```
|
|
397
|
-
|
|
398
|
-
### @Builder方法
|
|
399
|
-
|
|
400
|
-
- Builder方法用于组件内定义方法,可以使得相同代码可以在组件内进行复用。
|
|
401
|
-
- 本示例不仅使用了[@Builder](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-builder)方法进行去重,还对数据进行了移除,可以看到此时代码更加清晰易读,相对于最开始的代码,@Entry组件基本只用于处理页面构建逻辑,而不处理大量与页面设计无关的内容。
|
|
402
|
-
|
|
403
|
-
```typescript
|
|
404
|
-
@Observed
|
|
405
|
-
class TodoListData {
|
|
406
|
-
public planList: ResourceStr[] = [
|
|
407
|
-
// 请将$r('app.string.get_up')替换为实际资源文件,在本示例中该资源文件的value值为"7.30 起床"
|
|
408
|
-
$r('app.string.get_up'),
|
|
409
|
-
// 请将$r('app.string.breakfast')替换为实际资源文件,在本示例中该资源文件的value值为"8.30 早餐"
|
|
410
|
-
$r('app.string.breakfast'),
|
|
411
|
-
// 请将$r('app.string.lunch')替换为实际资源文件,在本示例中该资源文件的value值为"11.30 中餐"
|
|
412
|
-
$r('app.string.lunch'),
|
|
413
|
-
// 请将$r('app.string.dinner')替换为实际资源文件,在本示例中该资源文件的value值为"17.30 晚餐"
|
|
414
|
-
$r('app.string.dinner'),
|
|
415
|
-
// 请将$r('app.string.midnight_snack')替换为实际资源文件,在本示例中该资源文件的value值为"21.30 夜宵"
|
|
416
|
-
$r('app.string.midnight_snack'),
|
|
417
|
-
// 请将$r('app.string.bathe')替换为实际资源文件,在本示例中该资源文件的value值为"22.30 洗澡"
|
|
418
|
-
$r('app.string.bathe'),
|
|
419
|
-
// 请将$r('app.string.sleep')替换为实际资源文件,在本示例中该资源文件的value值为"1.30 睡觉"
|
|
420
|
-
$r('app.string.sleep')
|
|
421
|
-
];
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
@Component
|
|
425
|
-
struct StateTodoComponent {
|
|
426
|
-
build() {
|
|
427
|
-
Row() {
|
|
428
|
-
// 请将$r('app.string.all_tasks')替换为实际资源文件,在本示例中该资源文件的value值为"全部待办"
|
|
429
|
-
Text($r('app.string.all_tasks'))
|
|
430
|
-
.fontSize(30)
|
|
431
|
-
.fontWeight(FontWeight.Bold)
|
|
432
|
-
}
|
|
433
|
-
.width('100%')
|
|
434
|
-
.margin({ top: 10, bottom: 10 })
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
@Component
|
|
439
|
-
struct BuilderAllChooseComponent {
|
|
440
|
-
@Link isFinished: boolean;
|
|
441
|
-
|
|
442
|
-
build() {
|
|
443
|
-
Row() {
|
|
444
|
-
// 请将$r('app.string.check_all')替换为实际资源文件,在本示例中该资源文件的value值为"全选"
|
|
445
|
-
Button($r('app.string.check_all'), { type: ButtonType.Capsule })
|
|
446
|
-
.onClick(() => {
|
|
447
|
-
this.isFinished = !this.isFinished;
|
|
448
|
-
})
|
|
449
|
-
.fontSize(30)
|
|
450
|
-
.fontWeight(FontWeight.Bold)
|
|
451
|
-
.backgroundColor('#f7f6cc74')
|
|
452
|
-
}
|
|
453
|
-
.padding({ left: 15 })
|
|
454
|
-
.width('100%')
|
|
455
|
-
.margin({ top: 10, bottom: 10 })
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
@Component
|
|
460
|
-
struct BuilderThingComponent {
|
|
461
|
-
@Prop isFinished: boolean;
|
|
462
|
-
@Prop thing: string;
|
|
463
|
-
|
|
464
|
-
@Builder
|
|
465
|
-
displayIcon(icon: Resource) {
|
|
466
|
-
Image(icon)
|
|
467
|
-
.width(28)
|
|
468
|
-
.height(28)
|
|
469
|
-
.onClick(() => {
|
|
470
|
-
this.isFinished = !this.isFinished;
|
|
471
|
-
})
|
|
472
|
-
// ...
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
build() {
|
|
476
|
-
// 待办事项1
|
|
477
|
-
Row({ space: 15 }) {
|
|
478
|
-
if (this.isFinished) {
|
|
479
|
-
// 请将$r('app.media.finished')替换为实际资源文件
|
|
480
|
-
this.displayIcon($r('app.media.finished'));
|
|
481
|
-
} else {
|
|
482
|
-
// 请将$r('app.media.unfinished')替换为实际资源文件
|
|
483
|
-
this.displayIcon($r('app.media.unfinished'));
|
|
484
|
-
}
|
|
485
|
-
Text(`${this.thing}`)
|
|
486
|
-
.fontSize(24)
|
|
487
|
-
.decoration({ type: this.isFinished ? TextDecorationType.LineThrough : TextDecorationType.None })
|
|
488
|
-
.onClick(() => {
|
|
489
|
-
// 请将$r('app.string.la_la')替换为实际资源文件,在本示例中该资源文件的value值为"啦"
|
|
490
|
-
this.thing += this.getUIContext().getHostContext()!.resourceManager.getStringSync($r('app.string.la_la').id);
|
|
491
|
-
})
|
|
492
|
-
}
|
|
493
|
-
.height('8%')
|
|
494
|
-
.width('90%')
|
|
495
|
-
.padding({ left: 15 })
|
|
496
|
-
.opacity(this.isFinished ? 0.3 : 1)
|
|
497
|
-
.border({ width: 1 })
|
|
498
|
-
.borderColor(Color.White)
|
|
499
|
-
.borderRadius(25)
|
|
500
|
-
.backgroundColor(Color.White)
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
@Entry
|
|
505
|
-
@Component
|
|
506
|
-
struct BuilderIndex {
|
|
507
|
-
@State isFinished: boolean = false;
|
|
508
|
-
@State data: TodoListData = new TodoListData(); // View绑定ViewModel的数据
|
|
509
|
-
|
|
510
|
-
aboutToAppear(): void {
|
|
511
|
-
for (let i = 0; i < this.data.planList.length; i++) {
|
|
512
|
-
this.data.planList[i] =
|
|
513
|
-
this.getUIContext().getHostContext()!.resourceManager.getStringSync((this.data.planList[i] as Resource).id);
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
build() {
|
|
518
|
-
Column() {
|
|
519
|
-
// 全部待办
|
|
520
|
-
StateTodoComponent()
|
|
521
|
-
|
|
522
|
-
// 全选
|
|
523
|
-
BuilderAllChooseComponent({ isFinished: this.isFinished })
|
|
524
|
-
|
|
525
|
-
List() {
|
|
526
|
-
ForEach(this.data.planList, (item: string) => {
|
|
527
|
-
// 待办事项1
|
|
528
|
-
BuilderThingComponent({ isFinished: this.isFinished, thing: item })
|
|
529
|
-
.margin(5)
|
|
530
|
-
})
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
.height('100%')
|
|
534
|
-
.width('100%')
|
|
535
|
-
.margin({ top: 5, bottom: 5 })
|
|
536
|
-
.backgroundColor('#90f1f3f5')
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
```
|
|
540
|
-
|
|
541
|
-
### 总结
|
|
542
|
-
|
|
543
|
-
- 通过逐步优化代码结构,可以看到@Entry组件作为页面的入口,其build函数应该仅考虑将需要的组件组合起来,类似于搭积木。被page调用的子组件则如同积木,等着被需要的page进行调用。状态变量类似于粘合剂,当触发UI刷新事件时,状态变量自动刷新绑定的组件,实现page的按需刷新。
|
|
544
|
-
- 虽然现有的架构并未使用到MVVM的设计理念,但MVVM的核心理念已初见端倪。ArkUI的UI开发天然适合MVVM模式。在ArkUI中,page和组件构成View层,page负责组织组件,组件则作为构成元素。当组件需要更新时,通过状态变量驱动组件刷新,从而更新page。ViewModel的数据则来源于Model层。
|
|
545
|
-
- 示例中的代码功能较为简单,但随着功能的增加,主页面的代码量也会逐渐增多。当备忘录需要添加更多功能,且其他页面也需要使用到主页面的组件时,可以考虑采用MVVM模式来组织项目结构。
|
|
546
|
-
|
|
547
|
-
## 通过MVVM开发备忘录实战
|
|
548
|
-
|
|
549
|
-
上一章节展示了非MVVM模式下的代码组织方式。随着主页面代码的增加,应该采取合理的分层策略,使项目结构清晰,组件之间不互相引用,避免后期维护时牵一发而动全身,增加功能更新的困难。本章将通过对MVVM的核心文件组织模式,向开发者展示如何使用MVVM来重构上一章节的代码。
|
|
550
|
-
|
|
551
|
-
### MVVM文件结构说明
|
|
552
|
-
|
|
553
|
-
```
|
|
554
|
-
├── src
|
|
555
|
-
│ ├── ets
|
|
556
|
-
│ │ ├── pages 存放页面组件。
|
|
557
|
-
│ │ ├── views 存放业务组件。
|
|
558
|
-
│ │ ├── shares 存放通用组件。
|
|
559
|
-
│ │ └── viewmodel 数据服务。
|
|
560
|
-
│ │ ├── LoginViewModel.ets 登录页ViewModel。
|
|
561
|
-
│ │ └── xxxViewModel.ets 其他页ViewModel。
|
|
562
|
-
```
|
|
563
|
-
|
|
564
|
-
### 分层设计技巧
|
|
565
|
-
|
|
566
|
-
**Model层**
|
|
567
|
-
|
|
568
|
-
- model层存放本应用核心数据结构,这层本身和UI开发关系不大,让用户按照自己的业务逻辑进行封装。
|
|
569
|
-
|
|
570
|
-
**ViewModel层**
|
|
571
|
-
|
|
572
|
-
> **注意:** ViewModel层不只是存放数据,它同时需要提供数据的服务及处理。
|
|
573
|
-
|
|
574
|
-
- ViewModel层是为视图服务的数据层。其设计具有两个特点:
|
|
575
|
-
1. 按照页面组织数据。
|
|
576
|
-
2. 每个页面数据进行懒加载。
|
|
577
|
-
|
|
578
|
-
**View层**
|
|
579
|
-
|
|
580
|
-
View层根据需要来组织,但View层需要区分以下三种组件:
|
|
581
|
-
|
|
582
|
-
- **页面组件**:提供整体页面布局,实现多页面之间的跳转,前后台事件处理等页面内容。
|
|
583
|
-
- **业务组件**:被页面引用,构建出页面。
|
|
584
|
-
- **共享组件**:与项目无关的多项目共享组件。
|
|
585
|
-
|
|
586
|
-
共享组件和业务组件的区别:
|
|
587
|
-
|
|
588
|
-
- **业务组件**:包含了ViewModel数据,没有ViewModel,这个组件不能运行。
|
|
589
|
-
- **共享组件**:不包含ViewModel层的数据,需要的数据从外部传入。共享组件包含一个自定义组件,只要外部参数(无业务参数)满足,就可以工作。
|
|
590
|
-
|
|
591
|
-
### 代码示例
|
|
592
|
-
|
|
593
|
-
按MVVM模式组织结构,重构如下:
|
|
594
|
-
|
|
595
|
-
```
|
|
596
|
-
├── src
|
|
597
|
-
│ ├── ets
|
|
598
|
-
│ │ ├── model
|
|
599
|
-
│ │ │ ├── ThingModel.ets
|
|
600
|
-
│ │ │ └── TodoListModel.ets
|
|
601
|
-
│ │ ├── pages
|
|
602
|
-
│ │ │ ├── Index.ets
|
|
603
|
-
│ │ ├── views
|
|
604
|
-
│ │ │ ├── AllChooseComponent.ets
|
|
605
|
-
│ │ │ ├── ThingComponent.ets
|
|
606
|
-
│ │ │ ├── TodoComponent.ets
|
|
607
|
-
│ │ │ └── TodoListComponent.ets
|
|
608
|
-
│ │ ├── viewmodel
|
|
609
|
-
│ │ │ ├── ThingViewModel.ets
|
|
610
|
-
│ │ │ └── TodoListViewModel.ets
|
|
611
|
-
│ └── resources
|
|
612
|
-
│ ├── rawfile
|
|
613
|
-
│ │ ├── default_tasks.json
|
|
614
|
-
```
|
|
615
|
-
|
|
616
|
-
文件代码如下:
|
|
617
|
-
|
|
618
|
-
#### ThingModel.ets
|
|
619
|
-
|
|
620
|
-
```typescript
|
|
621
|
-
export default class ThingModel {
|
|
622
|
-
public thingName: string = 'Todo';
|
|
623
|
-
public isFinish: boolean = false;
|
|
624
|
-
}
|
|
625
|
-
```
|
|
626
|
-
|
|
627
|
-
#### TodoListModel.ets
|
|
628
|
-
|
|
629
|
-
```typescript
|
|
630
|
-
import { common } from '@kit.AbilityKit';
|
|
631
|
-
import { util } from '@kit.ArkTS';
|
|
632
|
-
import { hilog } from '@kit.PerformanceAnalysisKit';
|
|
633
|
-
import ThingModel from './ThingModel';
|
|
634
|
-
|
|
635
|
-
const DOMAIN = 0x0001;
|
|
636
|
-
const TAG = 'TodoListModel';
|
|
637
|
-
|
|
638
|
-
export default class TodoListModel {
|
|
639
|
-
public things: Array<ThingModel> = [];
|
|
640
|
-
|
|
641
|
-
constructor(things: Array<ThingModel>) {
|
|
642
|
-
this.things = things;
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
async loadTasks(context: common.UIAbilityContext) {
|
|
646
|
-
try {
|
|
647
|
-
let getJson = await context.resourceManager.getRawFileContent('default_tasks.json');
|
|
648
|
-
let textDecoderOptions: util.TextDecoderOptions = { ignoreBOM: true };
|
|
649
|
-
let textDecoder = util.TextDecoder.create('utf-8', textDecoderOptions);
|
|
650
|
-
let result = textDecoder.decodeToString(getJson, { stream: false });
|
|
651
|
-
this.things = JSON.parse(result);
|
|
652
|
-
} catch (error) {
|
|
653
|
-
hilog.error(DOMAIN, TAG, 'Failed to load tasks. Cause: %{public}s', JSON.stringify(error.message));
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
```
|
|
658
|
-
|
|
659
|
-
#### Index.ets
|
|
660
|
-
|
|
661
|
-
```typescript
|
|
662
|
-
import { common } from '@kit.AbilityKit';
|
|
663
|
-
// import ViewModel
|
|
664
|
-
import TodoListViewModel from '../viewmodel/TodoListViewModel';
|
|
665
|
-
|
|
666
|
-
// import View
|
|
667
|
-
import { TodoComponent } from '../views/TodoComponent';
|
|
668
|
-
import { AllChooseComponent } from '../views/AllChooseComponent';
|
|
669
|
-
import { TodoListComponent } from '../views/TodoListComponent';
|
|
670
|
-
|
|
671
|
-
@Entry
|
|
672
|
-
@Component
|
|
673
|
-
struct TodoList {
|
|
674
|
-
@State todoListViewModel: TodoListViewModel = new TodoListViewModel(); // View绑定ViewModel的数据
|
|
675
|
-
private context = this.getUIContext().getHostContext() as common.UIAbilityContext;
|
|
676
|
-
|
|
677
|
-
async aboutToAppear() {
|
|
678
|
-
await this.todoListViewModel.loadTasks(this.context);
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
build() {
|
|
682
|
-
Column() {
|
|
683
|
-
Row({ space: 40 }) {
|
|
684
|
-
// 全部待办
|
|
685
|
-
TodoComponent()
|
|
686
|
-
// 全选
|
|
687
|
-
AllChooseComponent({ todoListViewModel: this.todoListViewModel })
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
Column() {
|
|
691
|
-
TodoListComponent({ thingViewModelArray: this.todoListViewModel.things })
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
.height('100%')
|
|
695
|
-
.width('100%')
|
|
696
|
-
.margin({ top: 5, bottom: 5 })
|
|
697
|
-
.backgroundColor('#90f1f3f5')
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
```
|
|
701
|
-
|
|
702
|
-
#### AllChooseComponent.ets
|
|
703
|
-
|
|
704
|
-
```typescript
|
|
705
|
-
import TodoListViewModel from '../viewmodel/TodoListViewModel';
|
|
706
|
-
import { common } from '@kit.AbilityKit';
|
|
707
|
-
|
|
708
|
-
@Component
|
|
709
|
-
export struct AllChooseComponent {
|
|
710
|
-
context1 = this.getUIContext().getHostContext() as common.UIAbilityContext;
|
|
711
|
-
// 请在resources\base\element\string.json文件中配置name为'check_all',value为非空字符串的资源
|
|
712
|
-
@State titleName: ResourceStr = this.context1.resourceManager.getStringSync($r('app.string.check_all').id);
|
|
713
|
-
@Link todoListViewModel: TodoListViewModel;
|
|
714
|
-
|
|
715
|
-
build() {
|
|
716
|
-
Row() {
|
|
717
|
-
Button(`${this.titleName}`, { type: ButtonType.Capsule })
|
|
718
|
-
.onClick(() => {
|
|
719
|
-
this.todoListViewModel.chooseAll(); // View层点击事件发生时,调用ViewModel层方法chooseAll处理逻辑
|
|
720
|
-
this.titleName = this.todoListViewModel.isChosen ?
|
|
721
|
-
// 请在resources\base\element\string.json文件中配置name为'check_all',value为非空字符串的资源
|
|
722
|
-
this.context1.resourceManager.getStringSync($r('app.string.check_all').id)
|
|
723
|
-
// 请在resources\base\element\string.json文件中配置name为'deselect_all',value为非空字符串的资源
|
|
724
|
-
: this.context1.resourceManager.getStringSync($r('app.string.deselect_all').id);
|
|
725
|
-
})
|
|
726
|
-
.fontSize(30)
|
|
727
|
-
.fontWeight(FontWeight.Bold)
|
|
728
|
-
.backgroundColor('#f7f6cc74')
|
|
729
|
-
}
|
|
730
|
-
.padding({ left: this.todoListViewModel.isChosen ? 15 : 0 })
|
|
731
|
-
.width('100%')
|
|
732
|
-
.margin({ top: 10, bottom: 10 })
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
```
|
|
736
|
-
|
|
737
|
-
#### ThingComponent.ets
|
|
738
|
-
|
|
739
|
-
```typescript
|
|
740
|
-
import ThingViewModel from '../viewmodel/ThingViewModel';
|
|
741
|
-
|
|
742
|
-
@Component
|
|
743
|
-
export struct ThingComponent {
|
|
744
|
-
@ObjectLink thing: ThingViewModel;
|
|
745
|
-
|
|
746
|
-
@Builder
|
|
747
|
-
displayIcon(icon: Resource) {
|
|
748
|
-
Image(icon)
|
|
749
|
-
.width(28)
|
|
750
|
-
.height(28)
|
|
751
|
-
.onClick(() => {
|
|
752
|
-
this.thing.updateIsFinish(); // View层点击事件发生时,调用ViewModel层方法updateIsFinish处理逻辑
|
|
753
|
-
})
|
|
754
|
-
.id(this.thing.thingName)
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
build() {
|
|
758
|
-
// 待办事项
|
|
759
|
-
Row({ space: 15 }) {
|
|
760
|
-
if (this.thing.isFinish) {
|
|
761
|
-
// 请将$r('app.media.finished')替换为实际资源文件
|
|
762
|
-
this.displayIcon($r('app.media.finished'));
|
|
763
|
-
} else {
|
|
764
|
-
// 请将$r('app.media.unfinished')替换为实际资源文件
|
|
765
|
-
this.displayIcon($r('app.media.unfinished'));
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
Text(`${this.thing.thingName}`)
|
|
769
|
-
.fontSize(24)
|
|
770
|
-
.decoration({ type: this.thing.isFinish ? TextDecorationType.LineThrough : TextDecorationType.None })
|
|
771
|
-
.onClick(() => {
|
|
772
|
-
this.thing.addSuffixes(); // View层点击事件发生时,调用ViewModel层方法addSuffixes处理逻辑
|
|
773
|
-
})
|
|
774
|
-
}
|
|
775
|
-
.height('8%')
|
|
776
|
-
.width('90%')
|
|
777
|
-
.padding({ left: 15 })
|
|
778
|
-
.opacity(this.thing.isFinish ? 0.3 : 1)
|
|
779
|
-
.border({ width: 1 })
|
|
780
|
-
.borderColor(Color.White)
|
|
781
|
-
.borderRadius(25)
|
|
782
|
-
.backgroundColor(Color.White)
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
```
|
|
786
|
-
|
|
787
|
-
#### TodoComponent.ets
|
|
788
|
-
|
|
789
|
-
```typescript
|
|
790
|
-
@Component
|
|
791
|
-
export struct TodoComponent {
|
|
792
|
-
build() {
|
|
793
|
-
Row() {
|
|
794
|
-
// 请将$r('app.string.all_tasks')替换为实际资源文件,在本示例中该资源文件的value值为"全部待办"
|
|
795
|
-
Text($r('app.string.all_tasks'))
|
|
796
|
-
.fontSize(30)
|
|
797
|
-
.fontWeight(FontWeight.Bold)
|
|
798
|
-
}
|
|
799
|
-
.padding({ left: 15 })
|
|
800
|
-
.width('50%')
|
|
801
|
-
.margin({ top: 10, bottom: 10 })
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
```
|
|
805
|
-
|
|
806
|
-
#### TodoListComponent.ets
|
|
807
|
-
|
|
808
|
-
```typescript
|
|
809
|
-
import ThingViewModel from '../viewmodel/ThingViewModel';
|
|
810
|
-
import { ThingViewModelArray } from '../viewmodel/TodoListViewModel'
|
|
811
|
-
import { ThingComponent } from './ThingComponent';
|
|
812
|
-
|
|
813
|
-
@Component
|
|
814
|
-
export struct TodoListComponent {
|
|
815
|
-
@ObjectLink thingViewModelArray: ThingViewModelArray;
|
|
816
|
-
|
|
817
|
-
build() {
|
|
818
|
-
Column() {
|
|
819
|
-
List() {
|
|
820
|
-
ForEach(this.thingViewModelArray, (item: ThingViewModel) => {
|
|
821
|
-
// 待办事项
|
|
822
|
-
ListItem() {
|
|
823
|
-
ThingComponent({ thing: item })
|
|
824
|
-
.margin(5)
|
|
825
|
-
}
|
|
826
|
-
}, (item: ThingViewModel) => {
|
|
827
|
-
return item.thingName;
|
|
828
|
-
})
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
```
|
|
834
|
-
|
|
835
|
-
#### ThingViewModel.ets
|
|
836
|
-
|
|
837
|
-
```typescript
|
|
838
|
-
import ThingModel from '../model/ThingModel';
|
|
839
|
-
|
|
840
|
-
@Observed
|
|
841
|
-
export default class ThingViewModel {
|
|
842
|
-
@Track public thingName: string = 'Todo';
|
|
843
|
-
@Track public isFinish: boolean = false;
|
|
844
|
-
public context: Context = AppStorage.get('context')!;
|
|
845
|
-
|
|
846
|
-
updateTask(thing: ThingModel) {
|
|
847
|
-
this.thingName = thing.thingName;
|
|
848
|
-
this.isFinish = thing.isFinish;
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
updateIsFinish(): void {
|
|
852
|
-
this.isFinish = !this.isFinish;
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
addSuffixes(): void {
|
|
856
|
-
// 请在resources\base\element\string.json文件中配置name为'la_la',value为非空字符串的资源
|
|
857
|
-
this.thingName += this.context.resourceManager.getStringSync($r('app.string.la_la').id);
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
```
|
|
861
|
-
|
|
862
|
-
#### TodoListViewModel.ets
|
|
863
|
-
|
|
864
|
-
```typescript
|
|
865
|
-
import ThingViewModel from './ThingViewModel';
|
|
866
|
-
import { common } from '@kit.AbilityKit';
|
|
867
|
-
import TodoListModel from '../model/TodoListModel';
|
|
868
|
-
|
|
869
|
-
@Observed
|
|
870
|
-
export class ThingViewModelArray extends Array<ThingViewModel> {
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
@Observed
|
|
874
|
-
export default class TodoListViewModel {
|
|
875
|
-
@Track public isChosen: boolean = true;
|
|
876
|
-
@Track public things: ThingViewModelArray = new ThingViewModelArray();
|
|
877
|
-
|
|
878
|
-
async loadTasks(context: common.UIAbilityContext) {
|
|
879
|
-
let todoList = new TodoListModel([]);
|
|
880
|
-
await todoList.loadTasks(context);
|
|
881
|
-
for (let thing of todoList.things) {
|
|
882
|
-
let todoListViewModel = new ThingViewModel();
|
|
883
|
-
todoListViewModel.updateTask(thing);
|
|
884
|
-
this.things.push(todoListViewModel);
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
chooseAll(): void {
|
|
889
|
-
for (let thing of this.things) {
|
|
890
|
-
thing.isFinish = this.isChosen;
|
|
891
|
-
}
|
|
892
|
-
this.isChosen = !this.isChosen;
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
```
|
|
896
|
-
|
|
897
|
-
#### default_tasks.json
|
|
898
|
-
|
|
899
|
-
```json
|
|
900
|
-
[
|
|
901
|
-
{"thingName": "7.30起床", "isFinish": false},
|
|
902
|
-
{"thingName": "8.30早餐", "isFinish": false},
|
|
903
|
-
{"thingName": "11.30中餐", "isFinish": false},
|
|
904
|
-
{"thingName": "17.30晚餐", "isFinish": false},
|
|
905
|
-
{"thingName": "21.30夜宵", "isFinish": false},
|
|
906
|
-
{"thingName": "22.30洗澡", "isFinish": false},
|
|
907
|
-
{"thingName": "1.30睡觉", "isFinish": false}
|
|
908
|
-
]
|
|
909
|
-
```
|
|
910
|
-
|
|
911
|
-
MVVM模式拆分后的代码结构更加清晰,模块职责更明确。新页面需要使用事件组件,比如TodoListComponent组件,只需导入组件。
|