已开启
feat: add SportsHealth 运动健康应用示例(纯仓颉实现) #421
feat: add SportsHealth 运动健康应用示例(纯仓颉实现) #421
已开启
stwilliam503388创建于 18 天前
102 个文件变更+3710-0
@@ -0,0 +1,26 @@
1+/node_modules
2+/oh_modules
3+/local.properties
4+/build-profile.json5
5+/build.log
6+/nul
7+/.idea
8+**/build
9+/.hvigor
10+.cxx
11+/.clangd
12+/.clang-format
13+/.clang-tidy
14+**/.test
15+/.appanalyzer
16+/.agents/
17+/skills/
18+**/cj_res
19+**/*.cj.macrocall
20+**/ability_mainability_entry.cj
21+**/module_**_entry.cj
22+**/IDL_Dependencies_List~
23+**/build-script-cache
24+ 
25+# UI automation outputs (generated)
26+/ui_capture_output*/
@@ -0,0 +1,10 @@
1+{
2+ "app": {
3+ "bundleName": "com.example.sportshealthcangjieproject",
4+ "vendor": "example",
5+ "versionCode": 1000000,
6+ "versionName": "1.0.0",
7+ "icon": "$media:layered_image",
8+ "label": "$string:app_name"
9+ }
10+}
@@ -0,0 +1,8 @@
1+{
2+ "string": [
3+ {
4+ "name": "app_name",
5+ "value": "SportsHealthCangjieProject"
6+ }
7+ ]
8+}
@@ -0,0 +1,7 @@
1+{
2+ "layered-image":
3+ {
4+ "background" : "$media:background",
5+ "foreground" : "$media:foreground"
6+ }
7+}
@@ -0,0 +1,149 @@
1+# SportsHealth - 运动健康应用(纯仓颉语言实现)
2+ 
3+> 仓颉(Cangjie)语言实现的 HarmonyOS 运动健康应用,基于 ArkUI 声明式 UI 框架,演示 **Navigation + Tabs 多级导航** 在纯仓颉工程中的完整落地。
4+ 
5+## 场景介绍
6+ 
7+运动健康类应用是穿戴与手机生态中的高频场景。本示例实现了一个功能完整的运动健康 App:**5 个主 Tab + 18 个路由子页面**,覆盖健康数据概览、跑步记录、商城、内容发现、个人中心,并包含蓝牙穿戴设备绑定的完整页面流程(含运行时权限检测)。
8+ 
9+| Tab | 页面 | 功能说明 |
10+|-----|------|------|
11+| 🏠 首页 | HomeTab | 健康数据概览,运动/睡眠/心率等指标卡片展示 |
12+| 🏃 跑步 | RunTab | 跑步记录、运动数据展示 |
13+| 🛒 商城 | ShopTab | 运动装备商品浏览 |
14+| 🔍 发现 | FindTab | 内容发现/推荐 |
15+| 👤 我的 | MineTab | 个人中心,汇聚全部子页面入口 |
16+ 
17+18 个路由子页面集中于 `subpages.cj`(1421 行):设置、添加设备、我的设备、设备扫描列表、个人信息、消息中心、健康报告、心率监测、运动圈子、版本更新、用户协议、隐私政策、关于、权限管理、推送设置、清理缓存、意见反馈等。
18+ 
19+## 实现思路
20+ 
21+### 1. Navigation + Tabs 组合导航
22+ 
23+主页面在 `Navigation` 容器内部嵌套 `Tabs`,路由栈挂在 Navigation 之上,保证子页面路由返回后 Tab 状态不丢失:
24+ 
25+```cangjie
26+@Entry
27+@Component
28+class EntryView {
29+ @Provide var stack: NavPathStack = NavPathStack()
30+ @State var currentIndex: Int32 = 0
31+ var tabsController: TabsController = TabsController()
32+ 
33+ func build() {
34+ Navigation(this.stack) {
35+ Tabs(barPosition: BarPosition.End, controller: this.tabsController, index: this.currentIndex) {
36+ TabContent() { HomeTab() }.tabBar(...)
37+ // ... 5 个 TabContent
38+ }
39+ }
40+ }
41+}
42+```
43+ 
44+### 2. appPageMap 路由表集中映射
45+ 
46+全部 18 个子页面路由通过 `@Builder appPageMap` 集中注册,按路由名分发到对应页面组件,新增页面只需在此处追加一个分支:
47+ 
48+```cangjie
49+@Builder
50+func appPageMap(name: String, param: Any) {
51+ if (name == "settings") {
52+ SettingsPage()
53+ } else if (name == "device_list") {
54+ DeviceListPage()
55+ } else if (name == "health_report") {
56+ HealthReportPage()
57+ } // ... 共 18 个路由分支
58+ else {
59+ Column() { Text("Not Found") } // 兜底页
60+ }
61+}
62+```
63+ 
64+### 3. @Provide / @Consume 跨层级共享路由栈
65+ 
66+入口处 `@Provide` 提供唯一的 `NavPathStack`,各 Tab 页面与子页面统一 `@Consume` 消费后调用 `pushPathByName` 完成跳转,无需逐层透传:
67+ 
68+```cangjie
69+@Component
70+class HomePage {
71+ @Consume var stack: NavPathStack
72+ 
73+ func build() {
74+ // ...
75+ .onClick({ _: ClickEvent =>
76+ this.stack.pushPathByName("my_devices", "")
77+ })
78+ }
79+}
80+```
81+ 
82+### 4. 运行时权限检测
83+ 
84+`main_ability.cj` 在启动时申请计步/蓝牙/后台运行等权限;「添加设备」页通过 `atManager.checkAccessToken` 实时检测蓝牙权限状态并驱动 UI 刷新:
85+ 
86+```cangjie
87+@State var bluetoothGranted: Bool = true
88+ 
89+func refreshBluetoothPermissionState(): Unit {
90+ // atManager.checkAccessToken(...) 逐项检测权限
91+ this.bluetoothGranted = (atManager.checkAccessToken(tokenID, p) != GrantStatus.PermissionDenied)
92+}
93+```
94+ 
95+### 5. UI 自动化友好设计
96+ 
97+首页叠加了「不可见但可寻址」的点击热区(透明 Text),供 `ui_scenarios/` 下基于 hdc 的页面冒烟脚本精准定位,对齐 ArkTS 模板的自动化测试思路。
98+ 
99+## 功能特性
100+ 
101+- ✅ 底部 5 Tab 主导航(首页/跑步/商城/发现/我的)
102+- ✅ Navigation 路由栈管理,18 个子页面集中注册
103+- ✅ @Provide / @Consume 跨层级状态传递
104+- ✅ 蓝牙穿戴设备绑定流程页面(扫描列表 → 添加 → 我的设备)
105+- ✅ 运行时权限申请与状态检测
106+- ✅ 计步/心率/睡眠等健康数据卡片展示
107+- ✅ 基于 hdc 的 UI 冒烟自动化脚本(ui_scenarios/)
108+- ✅ 全量文案走 `@r(app.string.xxx)` 资源引用
109+ 
110+## 环境要求
111+ 
112+- DevEco Studio(含 Cangjie 支持)
113+- HarmonyOS SDK 6.0.2(22),API Level 22
114+- 仓颉语言插件(DevEco Studio 内安装)
115+ 
116+## 使用说明
117+ 
118+1. 用 DevEco Studio 打开本目录,等待仓颉依赖解析完成
119+2. 选择模拟器或真机设备,点击 Run 运行
120+3. 底部 5 个 Tab 可自由切换;「我的」页面可进入全部子页面,体验设备扫描/绑定流程
121+4. 附带 `ui_scenarios/` UI 自动化脚本(基于 hdc 的冒烟导航脚本),可在真机上批量走查页面
122+ 
123+## 工程目录
124+ 
125+```
126+entry/src/main/cangjie/
127+├── index.cj # 入口:EntryView + Navigation/Tabs 容器 + appPageMap 路由表
128+├── main_ability.cj # Ability 生命周期、运行时权限申请
129+├── ability_stage.cj # Stage 模型配置
130+└── pages/
131+ ├── home_tab.cj # 首页:健康数据概览(78 行)
132+ ├── run_tab.cj # 跑步:运动记录(160 行)
133+ ├── shop_tab.cj # 商城:商品浏览(236 行)
134+ ├── find_tab.cj # 发现:内容推荐(184 行)
135+ ├── mine_tab.cj # 我的:个人中心入口汇聚(162 行)
136+ └── subpages.cj # 18 个路由子页面集中实现(1421 行)
137+```
138+ 
139+## 难点与踩坑记录
140+ 
141+- `Navigation``Tabs` 的嵌套顺序:路由栈必须挂在 Tabs 之上,否则子页面返回后 Tab 选中态丢失
142+- `@Provide`/`@Consume` 的变量名必须完全一致(`stack`),否则子组件消费失败、跳转无响应
143+- 仓颉侧 `NavPathStack``pushPathByName` 第二参数必传(空串占位),与 ArkTS 可选参数行为不同
144+- 子页面集中单文件(subpages.cj 1400+ 行)与拆分多文件的取舍:集中便于路由总览,拆分利于编译增量
145+- 仓颉包管理(cjpm.toml)与 oh-package 协作:依赖解析需等待 DevEco 同步完成再构建
146+ 
147+## 原项目
148+ 
149+本项目源自个人原创仓库:[SportsHealthCangjieProject](https://github.com/stwilliam503388-creator/SportsHealthCangjieProject)
@@ -0,0 +1,33 @@
1+{
2+ "files": [
3+ "**/*.ets",
4+ "**/*.cj"
5+ ],
6+ "ignore": [
7+ "**/src/ohosTest/**/*",
8+ "**/src/test/**/*",
9+ "**/src/mock/**/*",
10+ "**/node_modules/**/*",
11+ "**/oh_modules/**/*",
12+ "**/build/**/*",
13+ "**/.preview/**/*"
14+ ],
15+ "ruleSet": [
16+ "plugin:@performance/recommended",
17+ "plugin:@typescript-eslint/recommended"
18+ ],
19+ "rules": {
20+ "@security/no-unsafe-aes": "error",
21+ "@security/no-unsafe-hash": "error",
22+ "@security/no-unsafe-mac": "warn",
23+ "@security/no-unsafe-dh": "error",
24+ "@security/no-unsafe-dsa": "error",
25+ "@security/no-unsafe-ecdsa": "error",
26+ "@security/no-unsafe-rsa-encrypt": "error",
27+ "@security/no-unsafe-rsa-sign": "error",
28+ "@security/no-unsafe-rsa-key": "error",
29+ "@security/no-unsafe-dsa-key": "error",
30+ "@security/no-unsafe-dh-key": "error",
31+ "@security/no-unsafe-3des": "error"
32+ }
33+}
@@ -0,0 +1,8 @@
1+/node_modules
2+/oh_modules
3+/.preview
4+/build
5+/.cxx
6+/.test
7+**/IDL_Dependencies_List~
8+**/build-script-cache
@@ -0,0 +1,24 @@
1+{
2+ "apiType": "stageMode",
3+ "buildOption": {
4+ "cangjieOptions": {
5+ "path": "./cjpm.toml",
6+ "abiFilters": ["arm64-v8a", "x86_64"]
7+ },
8+ "externalNativeOptions": {
9+ "abiFilters": ["arm64-v8a", "x86_64"]
10+ },
11+ "nativeLib": {
12+ "filter": {
13+ "enableOverride": true
14+ }
15+ }
16+ },
17+ "buildOptionSet": [
18+ ],
19+ "targets": [
20+ {
21+ "name": "default"
22+ }
23+ ]
24+}
@@ -0,0 +1,38 @@
1+[package]
2+ cjc-version = "1.1.0"
3+ compile-option = "--dy-std --cfg=\"${COMPILE_CONDITION_ENTRY}\""
4+ description = "CangjieUI Application"
5+ link-option = ""
6+ name = "ohos_app_cangjie_entry"
7+ output-type = "dynamic"
8+ src-dir = "./src/main/cangjie"
9+ target-dir = ""
10+ version = "1.0.0"
11+ package-configuration = {}
12+ scripts = {}
13+ 
14+[profile]
15+ [profile.build]
16+ incremental = true
17+ lto = ""
18+ [profile.build.combined]
19+ ohos_app_cangjie_entry = "dynamic"
20+ [profile.customized-option]
21+ debug = "-g -Woff all -Won apilevel-check"
22+ release = "--fast-math -O2 -s -Woff all -Won apilevel-check"
23+ [profile.test]
24+ 
25+[target.aarch64-linux-ohos]
26+ compile-option = "-B \"${DEVECO_CANGJIE_HOME}/build-tools/third_party/llvm/bin\" -B \"${DEVECO_OH_NATIVE_HOME}/sysroot/usr/lib/aarch64-linux-ohos\" -L \"${DEVECO_OH_NATIVE_HOME}/sysroot/usr/lib/aarch64-linux-ohos\" -L \"${DEVECO_OH_NATIVE_HOME}/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos\" -L \"${DEVECO_OH_NATIVE_HOME}/llvm/lib/aarch64-linux-ohos\" --sysroot \"${DEVECO_OH_NATIVE_HOME}/sysroot\""
27+[target.aarch64-linux-ohos.bin-dependencies]
28+ path-option = ["${AARCH64_LIBS}", "${AARCH64_MACRO_LIBS}", "${AARCH64_KIT_LIBS}"]
29+ package-option = {}
30+ 
31+[target.x86_64-linux-ohos]
32+ compile-option = "-B \"${DEVECO_CANGJIE_HOME}/build-tools/third_party/llvm/bin\" -B \"${DEVECO_OH_NATIVE_HOME}/sysroot/usr/lib/x86_64-linux-ohos\" -L \"${DEVECO_OH_NATIVE_HOME}/sysroot/usr/lib/x86_64-linux-ohos\" -L \"${DEVECO_OH_NATIVE_HOME}/llvm/lib/clang/15.0.4/lib/x86_64-linux-ohos\" -L \"${DEVECO_OH_NATIVE_HOME}/llvm/lib/x86_64-linux-ohos\" --sysroot \"${DEVECO_OH_NATIVE_HOME}/sysroot\""
33+[target.x86_64-linux-ohos.bin-dependencies]
34+ path-option = ["${X86_64_OHOS_LIBS}", "${X86_64_OHOS_MACRO_LIBS}", "${X86_64_OHOS_KIT_LIBS}"]
35+ 
36+[target.x86_64-unknown-windows-gnu.bin-dependencies]
37+ path-option = ["${X86_64_LIBS}", "${X86_64_MACRO_LIBS}"]
38+ package-option = {}
@@ -0,0 +1,6 @@
1+import { hapTasks } from '@ohos/hvigor-ohos-plugin';
2+ 
3+export default {
4+ system: hapTasks, /* Built-in plugin of Hvigor. It cannot be modified. */
5+ plugins: [] /* Custom plugin to extend the functionality of Hvigor. */
6+}
@@ -0,0 +1,10 @@
1+{
2+ "name": "entry",
3+ "version": "1.0.0",
4+ "description": "Please describe the basic information.",
5+ "main": "",
6+ "author": "",
7+ "license": "",
8+ "dependencies": {}
9+}
10+ 
@@ -0,0 +1,10 @@
1+package ohos_app_cangjie_entry
2+ 
3+import kit.AbilityKit.AbilityStage
4+import kit.PerformanceAnalysisKit.Hilog
5+ 
6+class MyAbilityStage <: AbilityStage {
7+ public override func onCreate(): Unit {
8+ Hilog.info(1, "Cangjie", "MyAbilityStage onCreated.")
9+ }
10+}
@@ -0,0 +1,175 @@
1+package ohos_app_cangjie_entry
2+ 
3+import kit.ArkUI.*
4+import kit.LocalizationKit.*
5+import ohos.arkui.state_macro_manage.*
6+ 
7+import ohos_app_cangjie_entry.pages.HomeTab
8+import ohos_app_cangjie_entry.pages.RunTab
9+import ohos_app_cangjie_entry.pages.FindTab
10+import ohos_app_cangjie_entry.pages.ShopTab
11+import ohos_app_cangjie_entry.pages.MineTab
12+import ohos_app_cangjie_entry.pages.SettingsPage
13+import ohos_app_cangjie_entry.pages.AddDevicePage
14+import ohos_app_cangjie_entry.pages.MyDevicesPage
15+import ohos_app_cangjie_entry.pages.DeviceListPage
16+import ohos_app_cangjie_entry.pages.MyselfPage
17+import ohos_app_cangjie_entry.pages.MessagePage
18+import ohos_app_cangjie_entry.pages.HealthReportPage
19+import ohos_app_cangjie_entry.pages.HeartMonitoringPage
20+import ohos_app_cangjie_entry.pages.SecretPage
21+import ohos_app_cangjie_entry.pages.CommonNewPage
22+import ohos_app_cangjie_entry.pages.CheckUpdatesPage
23+import ohos_app_cangjie_entry.pages.UserPolicyPage
24+import ohos_app_cangjie_entry.pages.PrivacyPolicyPage
25+import ohos_app_cangjie_entry.pages.AboutPage
26+import ohos_app_cangjie_entry.pages.PermissionSettingsPage
27+import ohos_app_cangjie_entry.pages.PushSettingsPage
28+import ohos_app_cangjie_entry.pages.CacheClearPage
29+import ohos_app_cangjie_entry.pages.FeedbackPage
30+ 
31+@Builder
32+func appPageMap(name: String, param: Any) {
33+ if (name == "settings") {
34+ SettingsPage()
35+ } else if (name == "device_add") {
36+ AddDevicePage()
37+ } else if (name == "my_devices") {
38+ MyDevicesPage()
39+ } else if (name == "device_list") {
40+ DeviceListPage()
41+ } else if (name == "myself") {
42+ MyselfPage()
43+ } else if (name == "message") {
44+ MessagePage()
45+ } else if (name == "health_report") {
46+ HealthReportPage()
47+ } else if (name == "heart_monitoring") {
48+ HeartMonitoringPage()
49+ } else if (name == "secret") {
50+ SecretPage()
51+ } else if (name == "common_new") {
52+ CommonNewPage()
53+ } else if (name == "check_updates") {
54+ CheckUpdatesPage()
55+ } else if (name == "user_policy") {
56+ UserPolicyPage()
57+ } else if (name == "privacy_policy") {
58+ PrivacyPolicyPage()
59+ } else if (name == "about") {
60+ AboutPage()
61+ } else if (name == "permission_settings") {
62+ PermissionSettingsPage()
63+ } else if (name == "push_settings") {
64+ PushSettingsPage()
65+ } else if (name == "cache_clear") {
66+ CacheClearPage()
67+ } else if (name == "feedback") {
68+ FeedbackPage()
69+ } else {
70+ Column() {
71+ Text("Not Found")
72+ .fontSize(18)
73+ .fontWeight(FontWeight.W600)
74+ Text(name)
75+ .fontSize(14)
76+ .opacity(0.6)
77+ .margin(top: 4)
78+ }
79+ .padding(24)
80+ .width(100.percent)
81+ .height(100.percent)
82+ }
83+}
84+ 
85+@Entry
86+@Component
87+class EntryView {
88+ @State var currentIndex: Int32 = 0
89+ var tabsController: TabsController = TabsController()
90+ @Provide var stack: NavPathStack = NavPathStack()
91+ 
92+ func tabIcon(index: Int32): ResourceStr {
93+ if (index == 0) {
94+ return @r(app.media.ic_public_home)
95+ } else if (index == 1) {
96+ return @r(app.media.ic_public_run)
97+ } else if (index == 2) {
98+ return @r(app.media.ic_public_find)
99+ } else if (index == 3) {
100+ return @r(app.media.ic_public_cart)
101+ }
102+ return @r(app.media.ic_public_person)
103+ }
104+ 
105+ func tabIconSelected(index: Int32): ResourceStr {
106+ if (index == 0) {
107+ return @r(app.media.ic_public_home_sel)
108+ } else if (index == 1) {
109+ return @r(app.media.ic_public_run_sel)
110+ } else if (index == 2) {
111+ return @r(app.media.ic_public_find_sel)
112+ } else if (index == 3) {
113+ return @r(app.media.ic_public_cart_sel)
114+ }
115+ return @r(app.media.ic_public_person_sel)
116+ }
117+ 
118+ @Builder
119+ func tabBuilder(index: Int32, title: AppResource) {
120+ Column() {
121+ Image(if (this.currentIndex == index) { this.tabIconSelected(index) } else { this.tabIcon(index) })
122+ .width(24)
123+ .height(24)
124+ .objectFit(ImageFit.Contain)
125+ Text(title)
126+ .fontSize(10)
127+ .margin(top: 4)
128+ .fontColor(if (this.currentIndex == index) { 0xFFFB6522 } else { @r(app.color.tabTitleColor) })
129+ }
130+ .justifyContent(FlexAlign.Center)
131+ .width(100.percent)
132+ .height(100.percent)
133+ }
134+ 
135+ func build() {
136+ Navigation(this.stack) {
137+ Tabs(barPosition: BarPosition.End, controller: this.tabsController, index: this.currentIndex) {
138+ TabContent() {
139+ HomeTab()
140+ }
141+ .tabBar({=> bind(this.tabBuilder, this)(0, @r(app.string.tab_home)) })
142+ 
143+ TabContent() {
144+ RunTab()
145+ }
146+ .tabBar({=> bind(this.tabBuilder, this)(1, @r(app.string.tab_run)) })
147+ 
148+ TabContent() {
149+ FindTab()
150+ }
151+ .tabBar({=> bind(this.tabBuilder, this)(2, @r(app.string.tab_find)) })
152+ 
153+ TabContent() {
154+ ShopTab()
155+ }
156+ .tabBar({=> bind(this.tabBuilder, this)(3, @r(app.string.tab_shop)) })
157+ 
158+ TabContent() {
159+ MineTab()
160+ }
161+ .tabBar({=> bind(this.tabBuilder, this)(4, @r(app.string.tab_mine)) })
162+ }
163+ .barHeight(56)
164+ .barMode(BarMode.Fixed)
165+ .scrollable(false)
166+ .onChange({ index: Int32 =>
167+ this.currentIndex = index
168+ })
169+ .width(100.percent)
170+ .height(100.percent)
171+ }
172+ .hideTitleBar(true)
173+ .navDestination(bind(appPageMap, this))
174+ }
175+}
@@ -0,0 +1,114 @@
1+package ohos_app_cangjie_entry
2+ 
3+import kit.PerformanceAnalysisKit.Hilog
4+import kit.AbilityKit.Want
5+import kit.AbilityKit.UIAbility
6+import kit.AbilityKit.LaunchParam
7+import kit.AbilityKit.LaunchReason
8+import kit.ArkUI.*
9+import ohos.arkui.state_macro_manage.*
10+import ohos.business_exception.BusinessException
11+import kit.AbilityKit.{AbilityAccessCtrl, BundleManager, BundleFlag, GrantStatus, Permissions, UIAbilityContext}
12+import ohos.security.permission_request_result.PermissionRequestResult
13+ 
14+class MainAbility <: UIAbility {
15+ public init() {
16+ super()
17+ registerSelf()
18+ }
19+ 
20+ public override func onCreate(want: Want, launchParam: LaunchParam): Unit {
21+ Hilog.info(1, "Cangjie", "MainAbility OnCreated.${want.abilityName}")
22+ match (launchParam.launchReason) {
23+ case LaunchReason.StartAbility => Hilog.info(1, "Cangjie", "START_ABILITY")
24+ case _ => ()
25+ }
26+ }
27+ 
28+ public override func onWindowStageCreate(windowStage: WindowStage): Unit {
29+ Hilog.info(1, "Cangjie", "MainAbility onWindowStageCreate.")
30+ 
31+ // Align with ArkTS template: compute safe-area avoid heights and store in AppStorage.
32+ AppStorage.setOrCreate("windowStage", windowStage)
33+ AppStorage.setOrCreate<Int64>("topRectHeightPx", 0)
34+ AppStorage.setOrCreate<Int64>("bottomRectHeightPx", 0)
35+ 
36+ try {
37+ let mainWindow: Window = windowStage.getMainWindow()
38+ mainWindow.setWindowLayoutFullScreen(true)
39+ 
40+ let systemAvoidArea = mainWindow.getWindowAvoidArea(AvoidAreaType.TypeSystem)
41+ AppStorage.setOrCreate<Int64>(
42+ "topRectHeightPx",
43+ Int64(systemAvoidArea.topRect.height)
44+ )
45+ 
46+ let navAvoidArea = mainWindow.getWindowAvoidArea(AvoidAreaType.TypeNavigationIndicator)
47+ AppStorage.setOrCreate<Int64>(
48+ "bottomRectHeightPx",
49+ Int64(navAvoidArea.bottomRect.height)
50+ )
51+ } catch (e: BusinessException) {
52+ Hilog.error(1, "Cangjie", "Compute avoid area failed. Code: ${e.code}, message: ${e.message}", "")
53+ }
54+ 
55+ windowStage.loadContent("EntryView")
56+ 
57+ // Request runtime permissions for in-app features (pedometer / bluetooth / background)
58+ // Note: per docs, requestPermissionsFromUser should be called after loadContent.
59+ this.requestInUsePermissions()
60+ }
61+ 
62+ func requestInUsePermissions(): Unit {
63+ try {
64+ // Check current grant state first to avoid unnecessary prompts.
65+ let tokenID = BundleManager.getBundleInfoForSelf(BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION)
66+ .appInfo.accessTokenId
67+ let atManager = AbilityAccessCtrl.createAtManager()
68+ 
69+ let permissionList: Array<Permissions> = [
70+ "ohos.permission.ACTIVITY_MOTION",
71+ "ohos.permission.ACCESS_BLUETOOTH",
72+ "ohos.permission.KEEP_BACKGROUND_RUNNING"
73+ ]
74+ 
75+ var needRequest: Bool = false
76+ for (p in permissionList) {
77+ if (atManager.checkAccessToken(tokenID, p) == GrantStatus.PermissionDenied) {
78+ needRequest = true
79+ break
80+ }
81+ }
82+ if (!needRequest) {
83+ return
84+ }
85+ 
86+ var resultCallback = {
87+ errorCode: Option<BusinessException>, data: Option<PermissionRequestResult> => match (errorCode) {
88+ case Some(e) => Hilog.error(1, "Cangjie", "requestPermissionsFromUser failed, errcode: ${e.code}", "")
89+ case _ =>
90+ match (data) {
91+ case Some(value) =>
92+ // Avoid out-of-bounds when the array is empty.
93+ if (value.permissions.size > 0) {
94+ for (i in (0..(value.permissions.size - 1))) {
95+ if (value.authResults[i] == 0) {
96+ Hilog.info(1, "Cangjie", "permission granted: ${value.permissions[i]}")
97+ } else {
98+ Hilog.info(1, "Cangjie", "permission denied: ${value.permissions[i]}")
99+ }
100+ }
101+ }
102+ case _ => Hilog.error(1, "Cangjie", "requestPermissionsFromUser error: data is null", "")
103+ }
104+ }
105+ }
106+ 
107+ atManager.requestPermissionsFromUser(this.context, permissionList, resultCallback)
108+ } catch (e: BusinessException) {
109+ Hilog.error(1, "Cangjie", "requestInUsePermissions BusinessException: ${e.code}, ${e.message}", "")
110+ } catch (e: Exception) {
111+ Hilog.error(1, "Cangjie", "requestInUsePermissions Exception: ${e}", "")
112+ }
113+ }
114+}
@@ -0,0 +1,184 @@
1+package ohos_app_cangjie_entry.pages
2+ 
3+import kit.ArkUI.*
4+import kit.LocalizationKit.*
5+// Avoid name clash with ArkUI List
6+import ohos.arkui.state_macro_manage.*
7+ 
8+@Component
9+public class FindTab {
10+ func build() {
11+ FindPage()
12+ }
13+}
14+ 
15+@Component
16+class FindPage {
17+ @Consume var stack: NavPathStack
18+ 
19+ @StorageLink["topRectHeightPx"] var topRectHeightPx: Int64 = 0
20+ 
21+ @State var focusIndex: Int32 = 0
22+ var controller: TabsController = TabsController()
23+ var scrollerForList: Scroller = Scroller()
24+ 
25+ var tabArray: Array<AppResource> = [
26+ @r(app.string.find_follow),
27+ @r(app.string.find_recommend),
28+ @r(app.string.find_activity),
29+ @r(app.string.find_knowledge)
30+ ]
31+ 
32+ @Builder
33+ func topTab(tabName: AppResource, tabIndex: Int32) {
34+ Column() {
35+ Text(tabName)
36+ .fontSize(if (tabIndex == this.focusIndex) { 22 } else { 16 })
37+ if (tabIndex == this.focusIndex) {
38+ Image(@r(app.media.a_lot_personal_run))
39+ .width(10)
40+ .height(2)
41+ }
42+ }
43+ .width(70)
44+ .borderRadius(topLeft: 10, topRight: 10)
45+ .justifyContent(FlexAlign.Center)
46+ .onClick({ _: ClickEvent =>
47+ this.controller.changeIndex(tabIndex)
48+ this.focusIndex = tabIndex
49+ })
50+ }
51+ 
52+ func build() {
53+ Column() {
54+ Row() {
55+ List(scroller: this.scrollerForList) {
56+ ForEach(
57+ this.tabArray,
58+ itemGeneratorFunc: { item: AppResource, idx: Int64 =>
59+ ListItem() {
60+ this.topTab(item, Int32(idx.position()))
61+ }
62+ },
63+ keyGeneratorFunc: { item: AppResource, idx: Int64 => return "${idx}" }
64+ )
65+ }
66+ .listDirection(Axis.Horizontal)
67+ .scrollBar(BarState.Off)
68+ }
69+ .height(50)
70+ .justifyContent(FlexAlign.Start)
71+ .width(100.percent)
72+ 
73+ Tabs(barPosition: BarPosition.Start, controller: this.controller) {
74+ TabContent() {
75+ Scroll() {
76+ Column(space: 10) {
77+ Row() {
78+ Image(@r(app.media.a_lot_personal_run))
79+ .width(100.percent)
80+ .height(10.percent)
81+ }
82+ 
83+ List(space: 10) {
84+ ListItem() {
85+ Column(space: 5) {
86+ Row() {
87+ Row(space: 5) {
88+ Image(@r(app.media.touxiang1))
89+ .width(40)
90+ .height(40)
91+ .borderRadius(24)
92+ Text("吃肉不上头")
93+ }
94+ 
95+ Button(@r(app.string.btn_follow))
96+ .backgroundColor(0xFF50E5E5)
97+ .fontColor(Color.White)
98+ .width(60)
99+ .height(30)
100+ }
101+ .justifyContent(FlexAlign.SpaceBetween)
102+ .width(95.percent)
103+ 
104+ Row() {
105+ Image(@r(app.media.a_lot_personal_run))
106+ .width(100)
107+ .height(100)
108+ .borderRadius(10)
109+ Image(@r(app.media.bilik))
110+ .width(100)
111+ .height(100)
112+ .borderRadius(10)
113+ Image(@r(app.media.figer))
114+ .width(100)
115+ .height(100)
116+ .borderRadius(10)
117+ }
118+ .width(95.percent)
119+ .justifyContent(FlexAlign.SpaceBetween)
120+ }
121+ .width(100.percent)
122+ }
123+ }
124+ .height(80.percent)
125+ .scrollBar(BarState.Off)
126+ }
127+ .width(100.percent)
128+ .height(100.percent)
129+ }
130+ .scrollBar(BarState.Off)
131+ .width(100.percent)
132+ .height(100.percent)
133+ }
134+ 
135+ TabContent() {
136+ Scroll() {
137+ Column() {
138+ Row() {
139+ Image(@r(app.media.men15))
140+ .height(10.percent)
141+ .width(100.percent)
142+ }
143+ .width(100.percent)
144+ }
145+ .width(100.percent)
146+ .height(100.percent)
147+ }
148+ }
149+ 
150+ TabContent() {
151+ Column() {
152+ Text("我是页面 活动 的内容")
153+ .height(300)
154+ .width(100.percent)
155+ .fontSize(30)
156+ }
157+ }
158+ 
159+ TabContent() {
160+ Column() {
161+ Text("我是页面 涨知识 的内容")
162+ .height(300)
163+ .width(100.percent)
164+ .fontSize(30)
165+ }
166+ }
167+ }
168+ .barMode(BarMode.Scrollable)
169+ .barHeight(0)
170+ .animationDuration(Some(100.0))
171+ .onChange({ index: Int32 =>
172+ this.focusIndex = index
173+ this.scrollerForList.scrollToIndex(index, smooth: true)
174+ })
175+ .width(100.percent)
176+ .layoutWeight(1)
177+ }
178+ .alignItems(HorizontalAlign.Start)
179+ .width(100.percent)
180+ .height(100.percent)
181+ .margin(top: getUIContext().px2vp(this.topRectHeightPx.px) ?? 0.vp)
182+ .backgroundColor(@r(app.color.primaryBgColor))
183+ }
184+}
@@ -0,0 +1,78 @@
1+package ohos_app_cangjie_entry.pages
2+ 
3+import kit.ArkUI.*
4+import kit.LocalizationKit.*
5+import ohos.arkui.state_macro_manage.*
6+ 
7+@Component
8+public class HomeTab {
9+ func build() {
10+ HomePage()
11+ }
12+}
13+ 
14+@Component
15+class HomePage {
16+ @Consume var stack: NavPathStack
17+ 
18+ func build() {
19+ Stack() {
20+ Column() {
21+ Stack() {
22+ Image(@r(app.media.img_3))
23+ .width(100.percent)
24+ .margin(left: 40)
25+ // Invisible but addressable click target for UI automation.
26+ Text(@r(app.string.mine_personal_data))
27+ .width(50)
28+ .height(50)
29+ .backgroundColor(Color.White)
30+ .margin(left: 300)
31+ .opacity(0.0)
32+ .onClick({ _: ClickEvent =>
33+ // Align with ArkTS template: Home -> MyDeviceOne (device list)
34+ this.stack.pushPathByName("my_devices", "")
35+ })
36+ }
37+ .margin(top: 30)
38+ 
39+ Row() {
40+ Image(@r(app.media.img_16))
41+ .height(21)
42+ }
43+ .margin(left: -220)
44+ 
45+ Scroll() {
46+ Column() {
47+ Row() {
48+ Image(@r(app.media.img_6))
49+ .width(166)
50+ .height(144)
51+ .margin(top: 33)
52+ Image(@r(app.media.img_9))
53+ .width(36)
54+ .margin(left: 31)
55+ }
56+ .margin(left: 50)
57+ 
58+ Image(@r(app.media.img_10))
59+ .width(304)
60+ .margin(top: 20.5)
61+ Image(@r(app.media.img_17))
62+ .width(328)
63+ .margin(top: 20.5)
64+ Image(@r(app.media.img_13))
65+ .width(328)
66+ .margin(top: 12)
67+ Image(@r(app.media.img_14))
68+ .width(328)
69+ .margin(top: 6.5)
70+ }
71+ }
72+ .layoutWeight(1)
73+ .scrollBar(BarState.Off)
74+ }
75+ .backgroundColor(@r(app.color.primaryBgColor))
76+ }
77+ }
78+}
@@ -0,0 +1,162 @@
1+package ohos_app_cangjie_entry.pages
2+ 
3+import kit.ArkUI.*
4+import kit.LocalizationKit.*
5+import ohos.arkui.state_macro_manage.*
6+ 
7+@Component
8+public class MineTab {
9+ func build() {
10+ MinePage()
11+ }
12+}
13+ 
14+@Component
15+class MinePage {
16+ @Consume var stack: NavPathStack
17+ 
18+ // Use StorageLink (two-way) here to avoid StorageProp accessor restrictions.
19+ @StorageLink["topRectHeightPx"] var topRectHeightPx: Int64 = 0
20+ 
21+ var mineItemIds: Array<String> = [
22+ "1",
23+ "2",
24+ "3",
25+ "4",
26+ "5",
27+ "6",
28+ "7",
29+ "8",
30+ "9",
31+ "10",
32+ "11",
33+ "12",
34+ "13"
35+ ]
36+ 
37+ var mineItemTitles: Array<AppResource> = [
38+ @r(app.string.mine_personal_data),
39+ @r(app.string.mine_check_updates),
40+ @r(app.string.user_policy_about),
41+ @r(app.string.set_about),
42+ @r(app.string.personal_policy_about),
43+ @r(app.string.mine_about),
44+ @r(app.string.permission_about),
45+ @r(app.string.new_about),
46+ @r(app.string.new_ask_about),
47+ @r(app.string.cache_about),
48+ @r(app.string.opinion_about),
49+ @r(app.string.health_report),
50+ @r(app.string.heart_monitoring)
51+ ]
52+ 
53+ func build() {
54+ Column() {
55+ Column() {
56+ Image(@r(app.media.ic_user))
57+ .objectFit(ImageFit.Contain)
58+ .height(66)
59+ .width(66)
60+ .margin(top: getUIContext().px2vp(this.topRectHeightPx.px) ?? 0.vp)
61+ 
62+ Column() {
63+ Text("LV.7")
64+ .fontSize(12)
65+ .fontWeight(FontWeight.Bolder)
66+ .fontColor(@r(app.color.leveColor))
67+ }
68+ .width(44)
69+ .height(16)
70+ .margin(top: -8)
71+ .borderRadius(5)
72+ .backgroundColor(@r(app.color.leveBgColor))
73+ .justifyContent(FlexAlign.Center)
74+ 
75+ Text("JoIin")
76+ .fontSize(20)
77+ .margin(bottom: 6)
78+ .fontWeight(FontWeight.Normal)
79+ .fontColor(@r(app.color.black))
80+ .fontFamily(@r(app.string.Helvetica))
81+ 
82+ Text("这是一条简短地个人签")
83+ .fontSize(16)
84+ .fontWeight(FontWeight.Normal)
85+ .fontColor(@r(app.color.signatureColor))
86+ .fontFamily(@r(app.string.PingFangSC_Regular))
87+ }
88+ .width(100.percent)
89+ 
90+ Scroll() {
91+ List() {
92+ ForEach(
93+ this.mineItemIds,
94+ itemGeneratorFunc: { itemId: String, idx: Int64 =>
95+ ListItem() {
96+ Column() {
97+ Flex(justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center) {
98+ Text(this.mineItemTitles[idx.position()])
99+ .fontSize(16)
100+ .height(40)
101+ Image(@r(app.media.ic_right_grey))
102+ .objectFit(ImageFit.Contain)
103+ .height(12)
104+ .width(7)
105+ }
106+ .height(48)
107+ .onClick({ _: ClickEvent =>
108+ if (idx.position() == 0) {
109+ // Align with ArkTS template: Mine -> DeviceListPageOne.
110+ this.stack.pushPathByName("device_list", "")
111+ } else if (idx.position() == 1) {
112+ this.stack.pushPathByName("check_updates", "")
113+ } else if (idx.position() == 2) {
114+ this.stack.pushPathByName("user_policy", "")
115+ } else if (idx.position() == 3) {
116+ this.stack.pushPathByName("settings", "")
117+ } else if (idx.position() == 4) {
118+ this.stack.pushPathByName("privacy_policy", "")
119+ } else if (idx.position() == 5) {
120+ this.stack.pushPathByName("about", "")
121+ } else if (idx.position() == 6) {
122+ this.stack.pushPathByName("permission_settings", "")
123+ } else if (idx.position() == 7) {
124+ this.stack.pushPathByName("push_settings", "")
125+ } else if (idx.position() == 8) {
126+ this.stack.pushPathByName("message", "")
127+ } else if (idx.position() == 9) {
128+ this.stack.pushPathByName("cache_clear", "")
129+ } else if (idx.position() == 10) {
130+ this.stack.pushPathByName("feedback", "")
131+ } else if (idx.position() == 11) {
132+ this.stack.pushPathByName("health_report", "")
133+ } else if (idx.position() == 12) {
134+ this.stack.pushPathByName("heart_monitoring", "")
135+ }
136+ })
137+ 
138+ Row() {}
139+ .height(1)
140+ .width(100.percent)
141+ .backgroundColor(@r(app.color.borderColor))
142+ }
143+ }
144+ .backgroundColor(Color.White)
145+ .margin(left: 24, right: 24)
146+ .height(48)
147+ },
148+ keyGeneratorFunc: { itemId: String, idx: Int64 => return "${idx}_${itemId}" }
149+ )
150+ }
151+ .borderRadius(topLeft: 24, topRight: 24)
152+ .backgroundColor(Color.White)
153+ .clip(true)
154+ }
155+ .scrollBar(BarState.Off)
156+ .layoutWeight(1)
157+ }
158+ .height(100.percent)
159+ .width(100.percent)
160+ .backgroundColor(@r(app.color.mineBgColor))
161+ }
162+}
@@ -0,0 +1,160 @@
1+package ohos_app_cangjie_entry.pages
2+ 
3+import kit.ArkUI.*
4+import kit.LocalizationKit.*
5+import ohos.arkui.state_macro_manage.*
6+import kit.SensorServiceKit.*
7+import kit.PerformanceAnalysisKit.Hilog
8+import ohos.business_exception.BusinessException
9+import ohos.callback_invoke.*
10+import kit.AbilityKit.{AbilityAccessCtrl, BundleManager, BundleFlag, GrantStatus, Permissions}
11+ 
12+@Component
13+public class RunTab {
14+ func build() {
15+ RunPage()
16+ }
17+}
18+ 
19+@Component
20+class RunPage {
21+ @State var stepNum: Int64 = 0
22+ @State var pedometerSubscribed: Bool = false
23+ @State var motionGranted: Bool = true
24+ @StorageLink["topRectHeightPx"] var topRectHeightPx: Int64 = 0
25+ @StorageLink["bottomRectHeightPx"] var bottomRectHeightPx: Int64 = 0
26+ 
27+ func updateSteps(v: Int64) {
28+ this.stepNum = v
29+ }
30+ 
31+ func refreshPermissionState(): Unit {
32+ try {
33+ let tokenID = BundleManager.getBundleInfoForSelf(BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION)
34+ .appInfo.accessTokenId
35+ let atManager = AbilityAccessCtrl.createAtManager()
36+ let p: Permissions = "ohos.permission.ACTIVITY_MOTION"
37+ this.motionGranted = (atManager.checkAccessToken(tokenID, p) != GrantStatus.PermissionDenied)
38+ } catch (e: Exception) {
39+ this.motionGranted = false
40+ }
41+ }
42+ 
43+ func build() {
44+ Column() {
45+ Column() {
46+ Image(@r(app.media.img_36))
47+ .width(328)
48+ .height(56)
49+ .margin(left: -10, top: getUIContext().px2vp(this.topRectHeightPx.px) ?? 0.vp)
50+ 
51+ Stack() {
52+ Image(@r(app.media.img_37))
53+ .width(100.percent)
54+ .margin(left: 0, top: 12)
55+ .height(56)
56+ Row() {}
57+ .width(100)
58+ .height(30)
59+ .margin(left: -220)
60+ }
61+ 
62+ Stack() {
63+ Image(@r(app.media.img_43))
64+ .width(100.percent)
65+ .margin(left: 0, top: 50)
66+ .height(415)
67+ 
68+ Column() {
69+ Stack() {
70+ Image(@r(app.media.img_39))
71+ .width(160)
72+ .margin(left: 0, top: 16)
73+ .height(102)
74+ 
75+ Row() {}
76+ .width(200)
77+ .height(40)
78+ .margin(left: -5, top: -300)
79+ .backgroundColor(0xFFF1F3F5)
80+ 
81+ Text("${this.stepNum}")
82+ .fontSize(40)
83+ .margin(left: 23, top: -300)
84+ .fontWeight(FontWeight.Bold)
85+ .width(100)
86+ 
87+ Row() {}
88+ .width(120)
89+ .height(20)
90+ .margin(left: -5, top: -230)
91+ .backgroundColor(0xFFEAF4EC)
92+ 
93+ Text("累计步数")
94+ .fontSize(15)
95+ .margin(left: 38, top: -230)
96+ .fontWeight(FontWeight.Bold)
97+ .width(100)
98+ .fontColor(0xFF999999)
99+ }
100+ 
101+ if (!this.motionGranted) {
102+ Text("未授予运动与健身权限,步数可能为0")
103+ .fontSize(12)
104+ .opacity(0.7)
105+ .margin(top: 8)
106+ }
107+ 
108+ Image(@r(app.media.img_40))
109+ .width(216)
110+ .margin(left: 0, top: 16)
111+ .height(64)
112+ }
113+ }
114+ 
115+ Image(@r(app.media.img_41))
116+ .width(328)
117+ .margin(left: 0, top: 28)
118+ .height(329)
119+ }
120+ }
121+ .height(100.percent)
122+ .width(100.percent)
123+ .padding(bottom: getUIContext().px2vp(this.bottomRectHeightPx.px) ?? 0.vp)
124+ .backgroundColor(0xFFF1F3F5)
125+ .onAppear({ =>
126+ this.refreshPermissionState()
127+ if (!this.motionGranted) {
128+ return
129+ }
130+ if (this.pedometerSubscribed) {
131+ return
132+ }
133+ try {
134+ let callback = PedometerCallback(this)
135+ let options = Options(interval: IntervalOption.SensorNumber(100000000))
136+ on(SensorId.Pedometer, callback, option: options)
137+ this.pedometerSubscribed = true
138+ } catch (e: BusinessException) {
139+ Hilog.error(0, "Cangjie", "Pedometer subscribe failed. Code: ${e.code}, message: ${e.message}", "")
140+ }
141+ })
142+ }
143+}
144+ 
145+class PedometerCallback <: Callback1Argument<PedometerResponse> {
146+ var owner: RunPage
147+ init(owner: RunPage) {
148+ this.owner = owner
149+ }
150+ 
151+ public func invoke(err: ?BusinessException, arg: PedometerResponse): Unit {
152+ match (err) {
153+ case Some(e) =>
154+ Hilog.error(0, "Cangjie", "Pedometer callback error. Code: ${e.code}, message: ${e.message}", "")
155+ return
156+ case _ => ()
157+ }
158+ this.owner.updateSteps(arg.steps)
159+ }
160+}
@@ -0,0 +1,236 @@
1+package ohos_app_cangjie_entry.pages
2+ 
3+import kit.ArkUI.*
4+import kit.LocalizationKit.*
5+import ohos.arkui.state_macro_manage.*
6+ 
7+@Component
8+public class ShopTab {
9+ func build() {
10+ ShopPage()
11+ }
12+}
13+ 
14+@Component
15+class ShopPage {
16+ var swiperController: SwiperController = SwiperController()
17+ 
18+ @StorageLink["topRectHeightPx"] var topRectHeightPx: Int64 = 0
19+ @StorageLink["bottomRectHeightPx"] var bottomRectHeightPx: Int64 = 0
20+ 
21+ var categoryTitles: Array<String> = [
22+ "方便代餐",
23+ "控卡主食",
24+ "助燃嗨吃",
25+ "体脂秤",
26+ "运动装备",
27+ "低卡零食",
28+ "高蛋白",
29+ "咖啡冲调",
30+ "营养早餐",
31+ "更多"
32+ ]
33+ var categoryChars: Array<String> = ["烫", "滚", "减", "脂", "季", "领", "劵", "更", "优", "惠"]
34+ 
35+ var recommendImages: Array<ResourceStr> = [
36+ @r(app.media.recommendation1),
37+ @r(app.media.recommendation2),
38+ @r(app.media.recommendation3),
39+ @r(app.media.recommendation4),
40+ @r(app.media.recommendation5),
41+ @r(app.media.recommendation2),
42+ @r(app.media.recommendation2)
43+ ]
44+ var recommendTitles: Array<String> = [
45+ "21天控卡减脂餐",
46+ "控卡代餐",
47+ "低脂零食",
48+ "体脂测量",
49+ "快乐燃脂",
50+ "21天控卡减脂餐",
51+ "21天控卡减脂餐"
52+ ]
53+ var recommendTags: Array<String> = [
54+ "[ 热辣 ] 推荐",
55+ "控卡代餐",
56+ "低脂零食",
57+ "体脂测量",
58+ "快乐燃脂",
59+ "[热辣]推荐",
60+ "[热辣]推荐"
61+ ]
62+ var recommendBadges: Array<String> = [
63+ "21天控卡减脂餐",
64+ "2件9折",
65+ "3件85折",
66+ "领劵立减5元",
67+ "领劵立减10元",
68+ "",
69+ ""
70+ ]
71+ 
72+ @Builder
73+ func recommendCard(idx: Int64) {
74+ Stack(alignContent: if (idx.position() == 0) { Alignment.TopStart } else { Alignment.Start }) {
75+ Image(this.recommendImages[idx.position()])
76+ .width(100.percent)
77+ .height(if (idx.position() == 0) { 200 } else { 100 })
78+ .borderRadius(10)
79+ 
80+ if (this.recommendBadges[idx.position()] != "") {
81+ Column(space: 10) {
82+ Text(this.recommendTags[idx.position()])
83+ .fontSize(16)
84+ .fontColor(0xFF000000)
85+ 
86+ Flex(justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center) {
87+ Text(this.recommendBadges[idx.position()])
88+ .fontSize(13)
89+ .fontColor(Color.White)
90+ .textAlign(TextAlign.Center)
91+ .maxLines(1)
92+ .textOverflow(TextOverflow.Ellipsis)
93+ }
94+ .constraintSize(maxWidth: if (idx.position() == 0) { 112 } else { 88 })
95+ .borderRadius(if (idx.position() == 0) { 14 } else { 13 })
96+ .height(if (idx.position() == 0) { 28 } else { 26 })
97+ .backgroundColor(0xFF206DF5)
98+ .padding(left: 12, right: 12)
99+ }
100+ .alignItems(HorizontalAlign.Start)
101+ .margin(left: if (idx.position() == 0) { 10 } else { 0 }, top: if (idx.position() == 0) { 10 } else { 0 })
102+ .padding(left: if (idx.position() == 0) { 0 } else { 5 }, top: if (idx.position() == 0) { 0 } else { 5 })
103+ } else {
104+ Column() {
105+ Text(this.recommendTags[idx.position()])
106+ .fontSize(16)
107+ .fontColor(0xFF000000)
108+ Text(this.recommendTitles[idx.position()])
109+ .fontSize(16)
110+ .fontColor(0xFF000000)
111+ }
112+ .alignItems(HorizontalAlign.Start)
113+ .padding(left: 5, top: 5)
114+ }
115+ }
116+ }
117+ 
118+ func build() {
119+ Column(space: 5) {
120+ Flex(justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center) {
121+ Text(@r(app.string.shop_title))
122+ .fontSize(30)
123+ .fontWeight(FontWeight.Bold)
124+ .margin(left: 15)
125+ 
126+ Row() {
127+ Image(@r(app.media.input_search))
128+ .width(20)
129+ .height(20)
130+ Image(@r(app.media.startIcon))
131+ .width(20)
132+ .height(20)
133+ }
134+ .margin(right: 15)
135+ }
136+ .margin(bottom: 10)
137+ 
138+ // 对齐 ArkTS 模板:顶部安全区
139+ .expandSafeArea(types: [SafeAreaType.System], edges: [SafeAreaEdge.Top])
140+ 
141+ Swiper(controller: this.swiperController) {
142+ Stack(alignContent: Alignment.Center) {
143+ Image(@r(app.media.bread))
144+ .width(100.percent)
145+ .height(100.percent)
146+ Text("持久饱腹满足感")
147+ .fontSize(30)
148+ .fontWeight(FontWeight.W500)
149+ .fontColor(Color.White)
150+ }
151+ 
152+ Text("0")
153+ .width(90.percent)
154+ .height(100.percent)
155+ .backgroundColor(Color.Gray)
156+ .textAlign(TextAlign.Center)
157+ .fontSize(30)
158+ 
159+ Text("1")
160+ .width(90.percent)
161+ .height(100.percent)
162+ .backgroundColor(Color.Green)
163+ .textAlign(TextAlign.Center)
164+ .fontSize(30)
165+ 
166+ Text("2")
167+ .width(90.percent)
168+ .height(100.percent)
169+ .backgroundColor(0xFFFFC0CB)
170+ .textAlign(TextAlign.Center)
171+ .fontSize(30)
172+ }
173+ .loop(true)
174+ .width(95.percent)
175+ .height(20.percent)
176+ .borderRadius(10)
177+ 
178+ List(space: 5) {
179+ ForEach(
180+ this.categoryTitles,
181+ itemGeneratorFunc: { item: String, idx: Int64 =>
182+ ListItem() {
183+ Column(space: 5) {
184+ Stack() {
185+ Image(@r(app.media.tang1))
186+ .width(50)
187+ .height(50)
188+ .borderRadius(10)
189+ Text(this.categoryChars[idx.position()])
190+ .fontSize(40)
191+ .fontColor(Color.White)
192+ }
193+ Text(item)
194+ .fontSize(14)
195+ }
196+ .alignItems(HorizontalAlign.Center)
197+ }
198+ },
199+ keyGeneratorFunc: { item: String, idx: Int64 => return "${idx}_${item}" }
200+ )
201+ }
202+ .width(90.percent)
203+ .lanes(5)
204+ 
205+ // Replace Grid with scrollable 2-column masonry-like layout to match ArkTS WaterFlow placement & scrolling
206+ Scroll() {
207+ Row(space: 10) {
208+ Column(space: 5) {
209+ this.recommendCard(0)
210+ this.recommendCard(3)
211+ this.recommendCard(5)
212+ }
213+ .width(0)
214+ .layoutWeight(1)
215+ 
216+ Column(space: 5) {
217+ this.recommendCard(1)
218+ this.recommendCard(2)
219+ this.recommendCard(4)
220+ this.recommendCard(6)
221+ }
222+ .width(0)
223+ .layoutWeight(1)
224+ }
225+ .width(100.percent)
226+ }
227+ .width(95.percent)
228+ .scrollBar(BarState.Off)
229+ .layoutWeight(1)
230+ }
231+ .width(100.percent)
232+ .height(100.percent)
233+ .padding(bottom: getUIContext().px2vp(this.bottomRectHeightPx.px) ?? 0.vp)
234+ .margin(top: getUIContext().px2vp(this.topRectHeightPx.px) ?? 0.vp)
235+ }
236+}
@@ -0,0 +1,1421 @@
1+package ohos_app_cangjie_entry.pages
2+ 
3+import kit.ArkUI.*
4+import kit.LocalizationKit.*
5+import ohos.arkui.state_macro_manage.*
6+import ohos.web.webview.*
7+import kit.AbilityKit.{AbilityAccessCtrl, BundleManager, BundleFlag, GrantStatus, Permissions}
8+import std.sync.*
9+ 
10+@Component
11+public class SettingsPage {
12+ @Consume var stack: NavPathStack
13+ 
14+ @StorageLink["topRectHeightPx"] var topRectHeightPx: Int64 = 0
15+ 
16+ func build() {
17+ NavDestination() {
18+ Column() {
19+ Scroll() {
20+ List() {
21+ ForEach(
22+ [
23+ @r(app.string.set_count_danger),
24+ @r(app.string.count_style),
25+ @r(app.string.lanyan_style),
26+ @r(app.string.third_about),
27+ @r(app.string.user_personal_style),
28+ @r(app.string.back_count_style)
29+ ],
30+ itemGeneratorFunc: { title: AppResource, idx: Int64 =>
31+ ListItem() {
32+ Column() {
33+ Flex(justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center) {
34+ Text(title)
35+ .fontSize(16)
36+ .height(40)
37+ Image(@r(app.media.ic_right_grey))
38+ .objectFit(ImageFit.Contain)
39+ .height(12)
40+ .width(7)
41+ }
42+ .height(48)
43+ .onClick({ _: ClickEvent =>
44+ if (idx.position() == 3) {
45+ this.stack.pushPathByName("common_new", "")
46+ } else if (idx.position() == 4) {
47+ this.stack.pushPathByName("secret", "")
48+ }
49+ })
50+ 
51+ Row() {}
52+ .height(1)
53+ .width(100.percent)
54+ .backgroundColor(@r(app.color.borderColor))
55+ }
56+ }
57+ .backgroundColor(Color.White)
58+ .margin(left: 24, right: 24)
59+ },
60+ keyGeneratorFunc: { title: AppResource, idx: Int64 => return "${idx}" }
61+ )
62+ }
63+ .backgroundColor(Color.White)
64+ .clip(true)
65+ }
66+ .scrollBar(BarState.Off)
67+ }
68+ .width(100.percent)
69+ .height(100.percent)
70+ }
71+ .title("设置")
72+ .padding(top: getUIContext().px2vp(this.topRectHeightPx.px) ?? 0.vp)
73+ .backgroundColor(@r(app.color.mineBgColor))
74+ .onBackPressed({ =>
75+ this.stack.pop()
76+ return true
77+ })
78+ }
79+}
80+ 
81+@Component
82+public class AddDevicePage {
83+ @Consume var stack: NavPathStack
84+ 
85+ @StorageLink["topRectHeightPx"] var topRectHeightPx: Int64 = 0
86+ 
87+ // Align with ArkTS template AddDeviceDemoOne: show connecting state
88+ @State var connectingIdx: Int64 = -1
89+ 
90+ // Align with ArkTS template AddDeviceDemoOne behavior: a list of discovered devices
91+ // (Cangjie demo: mock list; keep UI & click behavior consistent)
92+ @State var foundDeviceNames: Array<String> = []
93+ private var deviceBgs: Array<AppResource> = [
94+ @r(app.media.img_26),
95+ @r(app.media.img_27),
96+ @r(app.media.img_28),
97+ @r(app.media.img_29),
98+ @r(app.media.img_30)
99+ ]
100+ 
101+ @State var connectedIdx: Int64 = -1
102+ 
103+ @State var bluetoothGranted: Bool = true
104+ 
105+ func refreshBluetoothPermissionState(): Unit {
106+ try {
107+ let tokenID = BundleManager.getBundleInfoForSelf(BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION)
108+ .appInfo.accessTokenId
109+ let atManager = AbilityAccessCtrl.createAtManager()
110+ let p: Permissions = "ohos.permission.ACCESS_BLUETOOTH"
111+ this.bluetoothGranted = (atManager.checkAccessToken(tokenID, p) != GrantStatus.PermissionDenied)
112+ } catch (e: Exception) {
113+ this.bluetoothGranted = false
114+ }
115+ }
116+ 
117+ func ensureMockScanList() {
118+ if (this.foundDeviceNames.size > 0) {
119+ return
120+ }
121+ this.foundDeviceNames = [
122+ "BLE Device A",
123+ "BLE Device B",
124+ "BLE Device C",
125+ "BLE Device D",
126+ "BLE Device E"
127+ ]
128+ }
129+ 
130+ func build() {
131+ NavDestination() {
132+ Column() {
133+ Image(@r(app.media.img_25))
134+ .width(140)
135+ .height(48)
136+ .margin(top: 16, left: -220)
137+ 
138+ if (!this.bluetoothGranted) {
139+ Text("未授予蓝牙权限,无法连接设备")
140+ .fontSize(12)
141+ .opacity(0.7)
142+ .margin(top: 8, left: 24)
143+ }
144+ 
145+ Scroll() {
146+ Column() {
147+ ForEach(
148+ this.foundDeviceNames,
149+ itemGeneratorFunc: { name: String, idx: Int64 =>
150+ Stack(alignContent: Alignment.TopStart) {
151+ Image(this.deviceBgs[(idx.position() % Int64(this.deviceBgs.size)).position()])
152+ .width(340)
153+ .height(64)
154+ .margin(top: 12, left: 10)
155+ 
156+ Row() {}
157+ .width(200)
158+ .height(40)
159+ .backgroundColor(Color.White)
160+ .margin(top: 30, left: 60)
161+ 
162+ Text(name)
163+ .fontSize(16)
164+ .fontWeight(FontWeight.Bold)
165+ .margin(top: 35, left: 65)
166+ 
167+ Row() {}
168+ .backgroundColor(0xFFF3F3F3)
169+ .width(80)
170+ .height(31)
171+ .margin(top: 30, left: 260)
172+ .borderRadius(20)
173+ 
174+ if (this.connectedIdx == idx.position()) {
175+ Text("已连接")
176+ .fontSize(14)
177+ .fontWeight(FontWeight.Bold)
178+ .fontColor(0xFFFB6522)
179+ .margin(top: 37, left: 278)
180+ .onClick({ _: ClickEvent =>
181+ this.connectedIdx = -1
182+ })
183+ } else {
184+ if (this.connectingIdx == idx.position()) {
185+ // Cangjie demo: no BLE API; keep a manual connecting state.
186+ LoadingProgress()
187+ .height(40)
188+ .width(50)
189+ .margin(top: 27, left: 275)
190+ 
191+ Row() {}
192+ .width(100)
193+ .height(50)
194+ .backgroundColor(Color.White)
195+ .margin(top: 20, left: 260)
196+ .opacity(0.0)
197+ .onClick({ _: ClickEvent =>
198+ this.connectingIdx = -1
199+ this.connectedIdx = idx.position()
200+ })
201+ } else {
202+ Text("连接")
203+ .fontSize(14)
204+ .fontWeight(FontWeight.Bold)
205+ .fontColor(0xFFFB6522)
206+ .margin(top: 37, left: 285)
207+ .onClick({ _: ClickEvent =>
208+ if (!this.bluetoothGranted) {
209+ return
210+ }
211+ this.connectingIdx = idx.position()
212+ })
213+ }
214+ }
215+ }
216+ .margin(left: -30)
217+ },
218+ keyGeneratorFunc: { name: String, idx: Int64 => return "${idx}_${name}" }
219+ )
220+ 
221+ Stack() {
222+ Image(@r(app.media.img_33))
223+ .width(340)
224+ .height(40)
225+ .margin(top: 64, left: 10)
226+ Row() {}
227+ .width(100)
228+ .height(50)
229+ .backgroundColor(Color.White)
230+ .margin(top: 32, left: 240)
231+ .opacity(0.0)
232+ }
233+ }
234+ }
235+ .scrollBar(BarState.Off)
236+ }
237+ .width(100.percent)
238+ .height(100.percent)
239+ }
240+ .title("添加设备")
241+ // Align with ArkTS template: fixed top padding.
242+ .padding(top: 30)
243+ .backgroundColor(0xFFF1F3F5)
244+ .onAppear({ =>
245+ this.refreshBluetoothPermissionState()
246+ this.ensureMockScanList()
247+ })
248+ .onBackPressed({ =>
249+ this.stack.pop()
250+ return true
251+ })
252+ }
253+}
254+ 
255+@Component
256+public class MyDevicesPage {
257+ @Consume var stack: NavPathStack
258+ 
259+ @StorageLink["topRectHeightPx"] var topRectHeightPx: Int64 = 0
260+ 
261+ func build() {
262+ NavDestination() {
263+ Column() {
264+ Stack() {
265+ Image(@r(app.media.img_19))
266+ .width(350)
267+ .margin(left: 0)
268+ Row() {}
269+ .width(50)
270+ .height(50)
271+ .backgroundColor(Color.White)
272+ .margin(left: -300)
273+ .opacity(0.0)
274+ .onClick({ _: ClickEvent =>
275+ this.stack.pop()
276+ })
277+ }
278+ .margin(top: getUIContext().px2vp(this.topRectHeightPx.px) ?? 0.vp)
279+ 
280+ Row() {
281+ Image(@r(app.media.img_20))
282+ .width(200)
283+ }
284+ .margin(top: 39)
285+ 
286+ Image(@r(app.media.img_21))
287+ .width(219.66)
288+ .height(260)
289+ .margin(top: 52)
290+ 
291+ Stack() {
292+ Image(@r(app.media.img_22))
293+ .width(192)
294+ .height(40)
295+ 
296+ // Invisible but addressable click target for UI automation.
297+ Text("添加设备")
298+ .width(192)
299+ .height(40)
300+ .opacity(0.0)
301+ .onClick({ _: ClickEvent =>
302+ this.stack.pushPathByName("device_add", "")
303+ })
304+ }
305+ .margin(top: 48)
306+ 
307+ Image(@r(app.media.img_23))
308+ .width(328)
309+ .height(56)
310+ .margin(top: 24)
311+ }
312+ .height(100.percent)
313+ .width(100.percent)
314+ .backgroundColor(0xFFF1F3F5)
315+ }
316+ .hideTitleBar(true)
317+ .onBackPressed({ =>
318+ this.stack.pop()
319+ return true
320+ })
321+ }
322+}
323+ 
324+@Component
325+public class DeviceListPage {
326+ @Consume var stack: NavPathStack
327+ 
328+ func build() {
329+ NavDestination() {
330+ Column() {
331+ Column() {
332+ Stack() {
333+ Image(@r(app.media.img_19))
334+ .width(328)
335+ .height(56)
336+ .margin(left: 0)
337+ Row() {}
338+ .width(50)
339+ .height(50)
340+ .backgroundColor(Color.White)
341+ .margin(left: -300)
342+ .opacity(0.0)
343+ .onClick({ _: ClickEvent =>
344+ this.stack.pop()
345+ })
346+ }
347+ .margin(top: 30)
348+ 
349+ Image(@r(app.media.img_25))
350+ .width(140)
351+ .height(48)
352+ .margin(top: 16, left: -1)
353+ 
354+ Stack() {
355+ Image(@r(app.media.img_26))
356+ .width(340)
357+ .height(64)
358+ .margin(top: 12, left: 10)
359+ Row() {}
360+ .width(100)
361+ .height(50)
362+ .backgroundColor(Color.White)
363+ .margin(top: 28, left: 240)
364+ .opacity(0.0)
365+ .onClick({ _: ClickEvent =>
366+ this.stack.pushPathByName("device_add", "")
367+ })
368+ }
369+ 
370+ Stack() {
371+ Image(@r(app.media.img_27))
372+ .width(340)
373+ .height(64)
374+ .margin(top: 12, left: 10)
375+ Row() {}
376+ .width(100)
377+ .height(50)
378+ .backgroundColor(Color.White)
379+ .margin(top: 28, left: 240)
380+ .opacity(0.0)
381+ .onClick({ _: ClickEvent =>
382+ this.stack.pushPathByName("device_add", "")
383+ })
384+ }
385+ 
386+ Stack() {
387+ Image(@r(app.media.img_28))
388+ .width(340)
389+ .height(64)
390+ .margin(top: 12, left: 10)
391+ Row() {}
392+ .width(100)
393+ .height(50)
394+ .backgroundColor(Color.White)
395+ .margin(top: 28, left: 240)
396+ .opacity(0.0)
397+ .onClick({ _: ClickEvent =>
398+ this.stack.pushPathByName("device_add", "")
399+ })
400+ }
401+ 
402+ Stack() {
403+ Image(@r(app.media.img_29))
404+ .width(340)
405+ .height(64)
406+ .margin(top: 12, left: 10)
407+ Row() {}
408+ .width(100)
409+ .height(50)
410+ .backgroundColor(Color.White)
411+ .margin(top: 28, left: 240)
412+ .opacity(0.0)
413+ .onClick({ _: ClickEvent =>
414+ this.stack.pushPathByName("device_add", "")
415+ })
416+ }
417+ 
418+ Stack() {
419+ Image(@r(app.media.img_30))
420+ .width(340)
421+ .height(64)
422+ .margin(top: 12, left: 10)
423+ Row() {}
424+ .width(100)
425+ .height(50)
426+ .backgroundColor(Color.White)
427+ .margin(top: 28, left: 240)
428+ .opacity(0.0)
429+ .onClick({ _: ClickEvent =>
430+ this.stack.pushPathByName("device_add", "")
431+ })
432+ }
433+ 
434+ Stack() {
435+ Image(@r(app.media.img_31))
436+ .width(340)
437+ .height(64)
438+ .margin(top: 12, left: 10)
439+ Row() {}
440+ .width(100)
441+ .height(50)
442+ .backgroundColor(Color.White)
443+ .margin(top: 28, left: 240)
444+ .opacity(0.0)
445+ .onClick({ _: ClickEvent =>
446+ this.stack.pushPathByName("device_add", "")
447+ })
448+ }
449+ 
450+ Stack() {
451+ Image(@r(app.media.img_33))
452+ .width(340)
453+ .height(40)
454+ .margin(top: 84, left: 10)
455+ }
456+ }
457+ .alignItems(HorizontalAlign.Start)
458+ .width(90.percent)
459+ }
460+ .height(100.percent)
461+ .width(100.percent)
462+ .backgroundColor(0xFFF1F3F5)
463+ }
464+ .hideTitleBar(true)
465+ .onBackPressed({ =>
466+ this.stack.pop()
467+ return true
468+ })
469+ }
470+}
471+ 
472+@Component
473+public class MyselfPage {
474+ @Consume var stack: NavPathStack
475+ 
476+ @StorageLink["topRectHeightPx"] var topRectHeightPx: Int64 = 0
477+ 
478+ func build() {
479+ NavDestination() {
480+ Scroll() {
481+ List() {
482+ ForEach(
483+ [
484+ @r(app.string.third_about),
485+ @r(app.string.user_personal_style)
486+ ],
487+ itemGeneratorFunc: { title: AppResource, idx: Int64 =>
488+ ListItem() {
489+ Column() {
490+ Flex(justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center) {
491+ Text(title)
492+ .fontSize(16)
493+ .height(40)
494+ Image(@r(app.media.ic_right_grey))
495+ .objectFit(ImageFit.Contain)
496+ .height(12)
497+ .width(7)
498+ }
499+ .height(48)
500+ .onClick({ _: ClickEvent =>
501+ if (idx.position() == 0) {
502+ this.stack.pushPathByName("common_new", "")
503+ } else if (idx.position() == 1) {
504+ this.stack.pushPathByName("secret", "")
505+ }
506+ })
507+ 
508+ Row() {}
509+ .height(1)
510+ .width(100.percent)
511+ .backgroundColor(@r(app.color.borderColor))
512+ }
513+ }
514+ .backgroundColor(Color.White)
515+ .margin(left: 24, right: 24)
516+ },
517+ keyGeneratorFunc: { title: AppResource, idx: Int64 => return "${idx}" }
518+ )
519+ }
520+ .backgroundColor(Color.White)
521+ .clip(true)
522+ }
523+ .scrollBar(BarState.Off)
524+ .width(100.percent)
525+ .height(100.percent)
526+ }
527+ .title("隐私政策")
528+ .padding(top: getUIContext().px2vp(this.topRectHeightPx.px) ?? 0.vp)
529+ .backgroundColor(@r(app.color.mineBgColor))
530+ .onBackPressed({ =>
531+ this.stack.pop()
532+ return true
533+ })
534+ }
535+}
536+ 
537+@Component
538+public class MessagePage {
539+ @Consume var stack: NavPathStack
540+ 
541+ @StorageLink["topRectHeightPx"] var topRectHeightPx: Int64 = 0
542+ 
543+ @State var currentIndex: Int32 = 0
544+ 
545+ var titles: Array<String> = ["个人消息", "客服消息"]
546+ 
547+ func build() {
548+ NavDestination() {
549+ Column() {
550+ Tabs(barPosition: BarPosition.Start, index: this.currentIndex) {
551+ TabContent() {
552+ List(space: 5) {
553+ ListItem() {
554+ Row() {
555+ Row(space: 10) {
556+ Image(@r(app.media.app_icon))
557+ .width(40)
558+ Column(space: 5) {
559+ Flex(justifyContent: FlexAlign.SpaceBetween) {
560+ Text("健康运动通知")
561+ Text("2021-06-08")
562+ .fontSize(12)
563+ }
564+ .width(80.percent)
565+ Text("欢迎来到健康运动")
566+ .fontSize(12)
567+ }
568+ .alignItems(HorizontalAlign.Start)
569+ }
570+ .margin(left: 20)
571+ }
572+ .width(100.percent)
573+ }
574+ .backgroundColor(Color.White)
575+ .width(100.percent)
576+ .height(70)
577+ 
578+ ListItem() {
579+ Row() {
580+ Row(space: 10) {
581+ Image(@r(app.media.app_icon))
582+ .width(40)
583+ Column(space: 5) {
584+ Flex(justifyContent: FlexAlign.SpaceBetween) {
585+ Text("监控通知")
586+ Text("2021-06-08")
587+ .fontSize(12)
588+ }
589+ .width(80.percent)
590+ Text("欢迎来到健康运动")
591+ .fontSize(12)
592+ }
593+ .alignItems(HorizontalAlign.Start)
594+ }
595+ .margin(left: 20)
596+ }
597+ .width(100.percent)
598+ }
599+ .backgroundColor(Color.White)
600+ .width(100.percent)
601+ .height(70)
602+ 
603+ ListItem() {
604+ Row() {
605+ Row(space: 10) {
606+ Image(@r(app.media.app_icon))
607+ .width(40)
608+ Column(space: 5) {
609+ Flex(justifyContent: FlexAlign.SpaceBetween) {
610+ Text("健康通知")
611+ Text("2021-06-08")
612+ .fontSize(12)
613+ }
614+ .width(80.percent)
615+ Text("欢迎来到健康运动")
616+ .fontSize(12)
617+ }
618+ .alignItems(HorizontalAlign.Start)
619+ }
620+ .margin(left: 20)
621+ }
622+ .width(100.percent)
623+ }
624+ .backgroundColor(Color.White)
625+ .width(100.percent)
626+ .height(70)
627+ }
628+ .width(100.percent)
629+ .height(98.percent)
630+ }
631+ .tabBar({=>
632+ Column() {
633+ Text(this.titles[0])
634+ .fontColor(if (this.currentIndex == 0) { 0xFF1698CE } else { 0xFF6B6B6B })
635+ }
636+ })
637+ .backgroundColor(@r(app.color.primaryBgColor))
638+ 
639+ TabContent() {
640+ List() {
641+ ListItem() {
642+ Row() {
643+ Row(space: 10) {
644+ Image(@r(app.media.app_icon))
645+ .width(40)
646+ Column(space: 5) {
647+ Flex(justifyContent: FlexAlign.SpaceBetween) {
648+ Text("健康通知")
649+ Text("2021-06-08")
650+ .fontSize(12)
651+ }
652+ .width(80.percent)
653+ Text("欢迎来到健康运动")
654+ .fontSize(12)
655+ }
656+ .alignItems(HorizontalAlign.Start)
657+ }
658+ .margin(left: 20)
659+ }
660+ .width(100.percent)
661+ }
662+ .backgroundColor(Color.White)
663+ .width(100.percent)
664+ .height(70)
665+ }
666+ .width(100.percent)
667+ .height(100.percent)
668+ .margin(top: 10)
669+ }
670+ .tabBar({=>
671+ Column() {
672+ Text(this.titles[1])
673+ .fontColor(if (this.currentIndex == 1) { 0xFF1698CE } else { 0xFF6B6B6B })
674+ }
675+ })
676+ .backgroundColor(@r(app.color.primaryBgColor))
677+ }
678+ .animationDuration(Some(0.0))
679+ .backgroundColor(Color.White)
680+ .onChange({ index: Int32 =>
681+ this.currentIndex = index
682+ })
683+ }
684+ .width(100.percent)
685+ .height(100.percent)
686+ .backgroundColor(0xFFF3F2F7)
687+ }
688+ .title("消息中心")
689+ .padding(top: getUIContext().px2vp(this.topRectHeightPx.px) ?? 0.vp)
690+ .onBackPressed({ =>
691+ this.stack.pop()
692+ return true
693+ })
694+ }
695+}
696+ 
697+@Component
698+public class CheckUpdatesPage {
699+ @Consume var stack: NavPathStack
700+ @StorageLink["topRectHeightPx"] var topRectHeightPx: Int64 = 0
701+ 
702+ func build() {
703+ NavDestination() {
704+ Column() {
705+ Text("当前已是最新版本")
706+ .fontSize(16)
707+ .fontColor(0xFF333333)
708+ Text("无可用更新")
709+ .fontSize(12)
710+ .opacity(0.6)
711+ .margin(top: 8)
712+ }
713+ .width(100.percent)
714+ .height(100.percent)
715+ .justifyContent(FlexAlign.Center)
716+ }
717+ .title("检查更新")
718+ .padding(top: getUIContext().px2vp(this.topRectHeightPx.px) ?? 0.vp)
719+ .backgroundColor(@r(app.color.mineBgColor))
720+ .onBackPressed({ =>
721+ this.stack.pop()
722+ return true
723+ })
724+ }
725+}
726+ 
727+@Component
728+public class UserPolicyPage {
729+ @Consume var stack: NavPathStack
730+ @StorageLink["topRectHeightPx"] var topRectHeightPx: Int64 = 0
731+ 
732+ func build() {
733+ NavDestination() {
734+ Scroll() {
735+ Column(space: 12) {
736+ Text("用户协议")
737+ .fontSize(18)
738+ .fontWeight(FontWeight.W600)
739+ Text("本页面为示例占位,用于保证路由完整与运行不崩溃。")
740+ .fontSize(14)
741+ .opacity(0.7)
742+ Text("后续可替换为真实协议内容或加载 Web 页面。")
743+ .fontSize(14)
744+ .opacity(0.7)
745+ }
746+ .padding(24)
747+ .width(100.percent)
748+ }
749+ .scrollBar(BarState.Off)
750+ }
751+ .title("用户协议")
752+ .padding(top: getUIContext().px2vp(this.topRectHeightPx.px) ?? 0.vp)
753+ .backgroundColor(@r(app.color.mineBgColor))
754+ .onBackPressed({ =>
755+ this.stack.pop()
756+ return true
757+ })
758+ }
759+}
760+ 
761+@Component
762+public class PrivacyPolicyPage {
763+ @Consume var stack: NavPathStack
764+ @StorageLink["topRectHeightPx"] var topRectHeightPx: Int64 = 0
765+ 
766+ func build() {
767+ NavDestination() {
768+ Scroll() {
769+ Column(space: 12) {
770+ Text("隐私政策")
771+ .fontSize(18)
772+ .fontWeight(FontWeight.W600)
773+ Text("本页面为示例占位,用于保证路由完整与运行不崩溃。")
774+ .fontSize(14)
775+ .opacity(0.7)
776+ Text("后续可替换为真实隐私政策内容或加载 Web 页面。")
777+ .fontSize(14)
778+ .opacity(0.7)
779+ }
780+ .padding(24)
781+ .width(100.percent)
782+ }
783+ .scrollBar(BarState.Off)
784+ }
785+ .title("隐私政策")
786+ .padding(top: getUIContext().px2vp(this.topRectHeightPx.px) ?? 0.vp)
787+ .backgroundColor(@r(app.color.mineBgColor))
788+ .onBackPressed({ =>
789+ this.stack.pop()
790+ return true
791+ })
792+ }
793+}
794+ 
795+@Component
796+public class AboutPage {
797+ @Consume var stack: NavPathStack
798+ @StorageLink["topRectHeightPx"] var topRectHeightPx: Int64 = 0
799+ 
800+ func build() {
801+ NavDestination() {
802+ Column(space: 10) {
803+ Image(@r(app.media.app_icon))
804+ .width(64)
805+ .height(64)
806+ .borderRadius(14)
807+ Text("SportsHealth")
808+ .fontSize(18)
809+ .fontWeight(FontWeight.W600)
810+ Text("版本 1.0.0")
811+ .fontSize(12)
812+ .opacity(0.6)
813+ }
814+ .width(100.percent)
815+ .height(100.percent)
816+ .justifyContent(FlexAlign.Center)
817+ }
818+ .title("关于")
819+ .padding(top: getUIContext().px2vp(this.topRectHeightPx.px) ?? 0.vp)
820+ .backgroundColor(@r(app.color.mineBgColor))
821+ .onBackPressed({ =>
822+ this.stack.pop()
823+ return true
824+ })
825+ }
826+}
827+ 
828+@Component
829+public class PermissionSettingsPage {
830+ @Consume var stack: NavPathStack
831+ @StorageLink["topRectHeightPx"] var topRectHeightPx: Int64 = 0
832+ 
833+ func build() {
834+ NavDestination() {
835+ Column(space: 12) {
836+ Text("如需修改权限,请前往系统设置中手动开启。")
837+ .fontSize(14)
838+ .opacity(0.7)
839+ Text("提示:若曾拒绝且不再询问,应用将无法再次弹窗申请。")
840+ .fontSize(12)
841+ .opacity(0.6)
842+ }
843+ .padding(24)
844+ .width(100.percent)
845+ .height(100.percent)
846+ }
847+ .title("权限设置")
848+ .padding(top: getUIContext().px2vp(this.topRectHeightPx.px) ?? 0.vp)
849+ .backgroundColor(@r(app.color.mineBgColor))
850+ .onBackPressed({ =>
851+ this.stack.pop()
852+ return true
853+ })
854+ }
855+}
856+ 
857+@Component
858+public class PushSettingsPage {
859+ @Consume var stack: NavPathStack
860+ @StorageLink["topRectHeightPx"] var topRectHeightPx: Int64 = 0
861+ 
862+ @State var enablePush: Bool = true
863+ 
864+ func build() {
865+ NavDestination() {
866+ Column() {
867+ Row() {
868+ Text("推送消息")
869+ .fontSize(16)
870+ Toggle(ToggleType.Switch, isOn: this.enablePush)
871+ .onChange({ v: Bool =>
872+ this.enablePush = v
873+ })
874+ }
875+ .width(100.percent)
876+ .justifyContent(FlexAlign.SpaceBetween)
877+ .padding(24)
878+ }
879+ .width(100.percent)
880+ .height(100.percent)
881+ }
882+ .title("推送消息设置")
883+ .padding(top: getUIContext().px2vp(this.topRectHeightPx.px) ?? 0.vp)
884+ .backgroundColor(@r(app.color.mineBgColor))
885+ .onBackPressed({ =>
886+ this.stack.pop()
887+ return true
888+ })
889+ }
890+}
891+ 
892+@Component
893+public class CacheClearPage {
894+ @Consume var stack: NavPathStack
895+ @StorageLink["topRectHeightPx"] var topRectHeightPx: Int64 = 0
896+ 
897+ @State var cleared: Bool = false
898+ 
899+ func build() {
900+ NavDestination() {
901+ Column(space: 16) {
902+ Text(if (this.cleared) { "已清理缓存" } else { "缓存大小:--" })
903+ .fontSize(14)
904+ .opacity(0.7)
905+ Button("清理")
906+ .width(120)
907+ .height(40)
908+ .backgroundColor(0xFFFB6522)
909+ .fontColor(Color.White)
910+ .onClick({ _: ClickEvent =>
911+ this.cleared = true
912+ })
913+ }
914+ .padding(24)
915+ .width(100.percent)
916+ .height(100.percent)
917+ }
918+ .title("缓存清除")
919+ .padding(top: getUIContext().px2vp(this.topRectHeightPx.px) ?? 0.vp)
920+ .backgroundColor(@r(app.color.mineBgColor))
921+ .onBackPressed({ =>
922+ this.stack.pop()
923+ return true
924+ })
925+ }
926+}
927+ 
928+@Component
929+public class FeedbackPage {
930+ @Consume var stack: NavPathStack
931+ @StorageLink["topRectHeightPx"] var topRectHeightPx: Int64 = 0
932+ 
933+ @State var text: String = ""
934+ @State var submitted: Bool = false
935+ 
936+ func build() {
937+ NavDestination() {
938+ Column(space: 12) {
939+ if (this.submitted) {
940+ Text("已提交,感谢反馈")
941+ .fontSize(16)
942+ } else {
943+ TextArea(text: this.text)
944+ .height(160)
945+ .onChange({ v: String =>
946+ this.text = v
947+ })
948+ Button("提交")
949+ .width(120)
950+ .height(40)
951+ .backgroundColor(0xFFFB6522)
952+ .fontColor(Color.White)
953+ .onClick({ _: ClickEvent =>
954+ this.submitted = true
955+ })
956+ }
957+ }
958+ .padding(24)
959+ .width(100.percent)
960+ .height(100.percent)
961+ }
962+ .title("意见反馈")
963+ .padding(top: getUIContext().px2vp(this.topRectHeightPx.px) ?? 0.vp)
964+ .backgroundColor(@r(app.color.mineBgColor))
965+ .onBackPressed({ =>
966+ this.stack.pop()
967+ return true
968+ })
969+ }
970+}
971+ 
972+@Component
973+public class HealthReportPage {
974+ @Consume var stack: NavPathStack
975+ 
976+ @StorageLink["topRectHeightPx"] var topRectHeightPx: Int64 = 0
977+ 
978+ func build() {
979+ NavDestination() {
980+ Column() {
981+ List(space: 5) {
982+ ListItem() {
983+ Row() {
984+ Row(space: 10) {
985+ Image(@r(app.media.app_icon))
986+ .width(40)
987+ Column(space: 5) {
988+ Flex(justifyContent: FlexAlign.SpaceBetween) {
989+ Text("健康周报")
990+ Text("2021-06-08")
991+ .fontSize(12)
992+ }
993+ .width(80.percent)
994+ }
995+ .alignItems(HorizontalAlign.Start)
996+ }
997+ .margin(left: 20)
998+ }
999+ .width(100.percent)
1000+ }
1001+ .backgroundColor(Color.White)
1002+ .width(100.percent)
1003+ .height(70)
1004+ 
1005+ ListItem() {
1006+ Row() {
1007+ Row(space: 10) {
1008+ Image(@r(app.media.app_icon))
1009+ .width(40)
1010+ Column(space: 5) {
1011+ Flex(justifyContent: FlexAlign.SpaceBetween) {
1012+ Text("健康周报")
1013+ Text("2021-06-08")
1014+ .fontSize(12)
1015+ }
1016+ .width(80.percent)
1017+ }
1018+ .alignItems(HorizontalAlign.Start)
1019+ }
1020+ .margin(left: 20)
1021+ }
1022+ .width(100.percent)
1023+ }
1024+ .backgroundColor(Color.White)
1025+ .width(100.percent)
1026+ .height(70)
1027+ 
1028+ ListItem() {
1029+ Row() {
1030+ Row(space: 10) {
1031+ Image(@r(app.media.app_icon))
1032+ .width(40)
1033+ Column(space: 5) {
1034+ Flex(justifyContent: FlexAlign.SpaceBetween) {
1035+ Text("健康周报")
1036+ Text("2021-06-08")
1037+ .fontSize(12)
1038+ }
1039+ .width(80.percent)
1040+ }
1041+ .alignItems(HorizontalAlign.Start)
1042+ }
1043+ .margin(left: 20)
1044+ }
1045+ .width(100.percent)
1046+ }
1047+ .backgroundColor(Color.White)
1048+ .width(100.percent)
1049+ .height(70)
1050+ }
1051+ .width(100.percent)
1052+ .height(98.percent)
1053+ .margin(top: 10)
1054+ }
1055+ .width(100.percent)
1056+ .height(100.percent)
1057+ .backgroundColor(0xFFF3F2F7)
1058+ }
1059+ .title("健康报告")
1060+ .padding(top: getUIContext().px2vp(this.topRectHeightPx.px) ?? 0.vp)
1061+ .onBackPressed({ =>
1062+ this.stack.pop()
1063+ return true
1064+ })
1065+ }
1066+}
1067+ 
1068+@Component
1069+public class HeartMonitoringPage {
1070+ @Consume var stack: NavPathStack
1071+ 
1072+ @StorageLink["topRectHeightPx"] var topRectHeightPx: Int64 = 0
1073+ 
1074+ @State var check: Int64 = 2
1075+ var time: Array<String> = ["时", "日", "周", "月", "年"]
1076+ 
1077+ func build() {
1078+ NavDestination() {
1079+ Column() {
1080+ Column() {
1081+ Row() {
1082+ ForEach(
1083+ this.time,
1084+ itemGeneratorFunc: { item: String, idx: Int64 =>
1085+ Text(item)
1086+ .borderRadius(5)
1087+ .width(20.percent)
1088+ .height(100.percent)
1089+ .textAlign(TextAlign.Center)
1090+ .backgroundColor(if (idx.position() == this.check) { Color.White } else { Color.Transparent })
1091+ .onClick({ _: ClickEvent =>
1092+ this.check = idx.position()
1093+ })
1094+ },
1095+ keyGeneratorFunc: { item: String, idx: Int64 => return "${idx}" }
1096+ )
1097+ }
1098+ .width(100.percent)
1099+ .height(30)
1100+ .justifyContent(FlexAlign.SpaceBetween)
1101+ .backgroundColor(0xFFE6E6E7)
1102+ .margin(top: 10)
1103+ .padding(2)
1104+ .borderRadius(5)
1105+ 
1106+ Text("今天")
1107+ .width(100.percent)
1108+ .margin(top: 20, bottom: 30)
1109+ .fontWeight(FontWeight.W500)
1110+ .fontSize(16)
1111+ 
1112+ Row() {}
1113+ .height(25.percent)
1114+ 
1115+ Flex(justifyContent: FlexAlign.SpaceBetween) {
1116+ Text("最新")
1117+ .fontSize(13)
1118+ Text("75次/分")
1119+ .fontSize(13)
1120+ }
1121+ .width(100.percent)
1122+ .height(30)
1123+ .backgroundColor(0xFFE6E6E7)
1124+ .padding(left: 10, right: 10)
1125+ .borderRadius(10)
1126+ .margin(top: 30, bottom: 20)
1127+ 
1128+ Text("显示更多心率数据")
1129+ .fontColor(0xFF007DFF)
1130+ .fontSize(14)
1131+ .margin(bottom: 30)
1132+ }
1133+ .backgroundColor(Color.White)
1134+ .padding(left: 15, right: 15)
1135+ 
1136+ Column() {
1137+ Text("关于心率")
1138+ .width(100.percent)
1139+ .fontSize(18)
1140+ .fontWeight(FontWeight.W600)
1141+ Text("心率是指正常人安静状态下每分钟心跳的次数,也叫安静心率,一般为60~100次/分,可因年龄、性别或其他生理因素产生个体差异。 一般来说,年龄越小,心率越快,老年人心跳比年轻人慢,女性的心率比同龄男性快,这些都是正常的生理现象。")
1142+ .backgroundColor(Color.White)
1143+ .width(100.percent)
1144+ .padding(10)
1145+ .borderRadius(10)
1146+ .margin(top: 10)
1147+ }
1148+ .padding(20)
1149+ }
1150+ .width(100.percent)
1151+ .height(100.percent)
1152+ .backgroundColor(0xFFE6E6E7)
1153+ }
1154+ .title("心率")
1155+ .padding(top: getUIContext().px2vp(this.topRectHeightPx.px) ?? 0.vp)
1156+ .onBackPressed({ =>
1157+ this.stack.pop()
1158+ return true
1159+ })
1160+ }
1161+}
1162+ 
1163+@Component
1164+public class SecretPage {
1165+ @Consume var stack: NavPathStack
1166+ private let controller: WebviewController = WebviewController()
1167+ 
1168+ @StorageLink["topRectHeightPx"] var topRectHeightPx: Int64 = 0
1169+ 
1170+ @State var loading: Bool = true
1171+ @State var showFallback: Bool = false
1172+ 
1173+ private var loadTimeoutTimer: Option<Timer> = None
1174+ 
1175+ func cancelLoadTimer(): Unit {
1176+ match (this.loadTimeoutTimer) {
1177+ case Some(t) => t.cancel()
1178+ case _ => ()
1179+ }
1180+ this.loadTimeoutTimer = None
1181+ }
1182+ 
1183+ func armLoadTimer(): Unit {
1184+ this.cancelLoadTimer()
1185+ this.loadTimeoutTimer = Some(Timer.once(10000 * Duration.millisecond) {
1186+ => launch {
1187+ if (this.loading && !this.showFallback) {
1188+ try {
1189+ this.controller.stop()
1190+ } catch (e: Exception) {
1191+ ()
1192+ }
1193+ this.loading = false
1194+ this.showFallback = true
1195+ }
1196+ }
1197+ })
1198+ }
1199+ 
1200+ func build() {
1201+ NavDestination() {
1202+ Column() {
1203+ if (this.loading && !this.showFallback) {
1204+ Row(space: 8) {
1205+ LoadingProgress()
1206+ .width(16)
1207+ .height(16)
1208+ Text("正在加载...")
1209+ .fontSize(12)
1210+ .opacity(0.6)
1211+ Button("停止")
1212+ .height(28)
1213+ .backgroundColor(0xFFF3F2F7)
1214+ .onClick({ _: ClickEvent =>
1215+ this.cancelLoadTimer()
1216+ this.controller.stop()
1217+ this.loading = false
1218+ this.showFallback = true
1219+ })
1220+ }
1221+ .padding(left: 16, right: 16, top: 8, bottom: 8)
1222+ .width(100.percent)
1223+ .backgroundColor(0xFFFFFFFF)
1224+ }
1225+ 
1226+ if (this.showFallback) {
1227+ Column(space: 12) {
1228+ Text("页面未加载完成")
1229+ .fontSize(16)
1230+ Text("你可以刷新重试,或返回")
1231+ .fontSize(12)
1232+ .opacity(0.6)
1233+ Row(space: 12) {
1234+ Button("刷新")
1235+ .width(120)
1236+ .height(40)
1237+ .backgroundColor(0xFFFB6522)
1238+ .fontColor(Color.White)
1239+ .onClick({ _: ClickEvent =>
1240+ this.showFallback = false
1241+ this.loading = true
1242+ this.armLoadTimer()
1243+ this.controller.reload()
1244+ })
1245+ Button("返回")
1246+ .width(120)
1247+ .height(40)
1248+ .backgroundColor(0xFFF3F2F7)
1249+ .onClick({ _: ClickEvent =>
1250+ this.stack.pop()
1251+ })
1252+ }
1253+ }
1254+ .padding(24)
1255+ .width(100.percent)
1256+ .height(100.percent)
1257+ .justifyContent(FlexAlign.Center)
1258+ .backgroundColor(0xFFFFFFFF)
1259+ } else {
1260+ Web(src: @rawfile("index.html"), controller: this.controller)
1261+ .width(100.percent)
1262+ .height(100.percent)
1263+ .onPageBegin({ evt =>
1264+ this.loading = true
1265+ this.showFallback = false
1266+ this.armLoadTimer()
1267+ })
1268+ .onPageEnd({ evt =>
1269+ this.loading = false
1270+ this.showFallback = false
1271+ this.cancelLoadTimer()
1272+ })
1273+ }
1274+ }
1275+ .width(100.percent)
1276+ .height(100.percent)
1277+ }
1278+ .title("用户隐私数据清单")
1279+ .padding(top: getUIContext().px2vp(this.topRectHeightPx.px) ?? 0.vp)
1280+ .onAppear({ =>
1281+ this.loading = true
1282+ this.showFallback = false
1283+ this.armLoadTimer()
1284+ })
1285+ .onBackPressed({ =>
1286+ this.cancelLoadTimer()
1287+ this.stack.pop()
1288+ return true
1289+ })
1290+ }
1291+}
1292+ 
1293+@Component
1294+public class CommonNewPage {
1295+ @Consume var stack: NavPathStack
1296+ private let controller: WebviewController = WebviewController()
1297+ 
1298+ @StorageLink["topRectHeightPx"] var topRectHeightPx: Int64 = 0
1299+ 
1300+ @State var loading: Bool = true
1301+ @State var showFallback: Bool = false
1302+ 
1303+ private var loadTimeoutTimer: Option<Timer> = None
1304+ 
1305+ func cancelLoadTimer(): Unit {
1306+ match (this.loadTimeoutTimer) {
1307+ case Some(t) => t.cancel()
1308+ case _ => ()
1309+ }
1310+ this.loadTimeoutTimer = None
1311+ }
1312+ 
1313+ func armLoadTimer(): Unit {
1314+ this.cancelLoadTimer()
1315+ this.loadTimeoutTimer = Some(Timer.once(10000 * Duration.millisecond) {
1316+ => launch {
1317+ if (this.loading && !this.showFallback) {
1318+ try {
1319+ this.controller.stop()
1320+ } catch (e: Exception) {
1321+ ()
1322+ }
1323+ this.loading = false
1324+ this.showFallback = true
1325+ }
1326+ }
1327+ })
1328+ }
1329+ 
1330+ func build() {
1331+ NavDestination() {
1332+ Column() {
1333+ if (this.loading && !this.showFallback) {
1334+ Row(space: 8) {
1335+ LoadingProgress()
1336+ .width(16)
1337+ .height(16)
1338+ Text("正在加载...")
1339+ .fontSize(12)
1340+ .opacity(0.6)
1341+ Button("停止")
1342+ .height(28)
1343+ .backgroundColor(0xFFF3F2F7)
1344+ .onClick({ _: ClickEvent =>
1345+ this.cancelLoadTimer()
1346+ this.controller.stop()
1347+ this.loading = false
1348+ this.showFallback = true
1349+ })
1350+ }
1351+ .padding(left: 16, right: 16, top: 8, bottom: 8)
1352+ .width(100.percent)
1353+ .backgroundColor(0xFFFFFFFF)
1354+ }
1355+ 
1356+ if (this.showFallback) {
1357+ Column(space: 12) {
1358+ Text("页面未加载完成")
1359+ .fontSize(16)
1360+ Text("你可以刷新重试,或返回")
1361+ .fontSize(12)
1362+ .opacity(0.6)
1363+ Row(space: 12) {
1364+ Button("刷新")
1365+ .width(120)
1366+ .height(40)
1367+ .backgroundColor(0xFFFB6522)
1368+ .fontColor(Color.White)
1369+ .onClick({ _: ClickEvent =>
1370+ this.showFallback = false
1371+ this.loading = true
1372+ this.armLoadTimer()
1373+ this.controller.reload()
1374+ })
1375+ Button("返回")
1376+ .width(120)
1377+ .height(40)
1378+ .backgroundColor(0xFFF3F2F7)
1379+ .onClick({ _: ClickEvent =>
1380+ this.stack.pop()
1381+ })
1382+ }
1383+ }
1384+ .padding(24)
1385+ .width(100.percent)
1386+ .height(100.percent)
1387+ .justifyContent(FlexAlign.Center)
1388+ .backgroundColor(0xFFFFFFFF)
1389+ } else {
1390+ Web(src: @rawfile("index.html"), controller: this.controller)
1391+ .width(100.percent)
1392+ .height(100.percent)
1393+ .onPageBegin({ evt =>
1394+ this.loading = true
1395+ this.showFallback = false
1396+ this.armLoadTimer()
1397+ })
1398+ .onPageEnd({ evt =>
1399+ this.loading = false
1400+ this.showFallback = false
1401+ this.cancelLoadTimer()
1402+ })
1403+ }
1404+ }
1405+ .width(100.percent)
1406+ .height(100.percent)
1407+ }
1408+ .title("第三方共享信息清单")
1409+ .padding(top: getUIContext().px2vp(this.topRectHeightPx.px) ?? 0.vp)
1410+ .onAppear({ =>
1411+ this.loading = true
1412+ this.showFallback = false
1413+ this.armLoadTimer()
1414+ })
1415+ .onBackPressed({ =>
1416+ this.cancelLoadTimer()
1417+ this.stack.pop()
1418+ return true
1419+ })
1420+ }
1421+}
@@ -0,0 +1,68 @@
1+{
2+ "module": {
3+ "name": "entry",
4+ "type": "entry",
5+ "description": "$string:module_desc",
6+ "mainElement": "EntryAbility",
7+ "deviceTypes": [
8+ "phone"
9+ ],
10+ "deliveryWithInstall": true,
11+ "installationFree": false,
12+ "srcEntry": "ohos_app_cangjie_entry.MyAbilityStage",
13+ "abilities": [
14+ {
15+ "name": "EntryAbility",
16+ "srcEntry": "ohos_app_cangjie_entry.MainAbility",
17+ "description": "$string:EntryAbility_desc",
18+ "icon": "$media:layered_image",
19+ "label": "$string:EntryAbility_label",
20+ "startWindowIcon": "$media:startIcon",
21+ "startWindowBackground": "$color:start_window_background",
22+ "exported": true,
23+ "skills": [
24+ {
25+ "entities": [
26+ "entity.system.home"
27+ ],
28+ "actions": [
29+ "action.system.home"
30+ ]
31+ }
32+ ]
33+ }
34+ ],
35+ "requestPermissions": [
36+ {
37+ "name": "ohos.permission.ACTIVITY_MOTION",
38+ "reason": "$string:permission_activity_motion_reason",
39+ "usedScene": {
40+ "abilities": [
41+ "EntryAbility"
42+ ],
43+ "when": "inuse"
44+ }
45+ },
46+ {
47+ "name": "ohos.permission.ACCESS_BLUETOOTH",
48+ "reason": "$string:permission_access_bluetooth_reason",
49+ "usedScene": {
50+ "abilities": [
51+ "EntryAbility"
52+ ],
53+ "when": "inuse"
54+ }
55+ },
56+ {
57+ "name": "ohos.permission.KEEP_BACKGROUND_RUNNING",
58+ "reason": "$string:permission_keep_background_running_reason",
59+ "usedScene": {
60+ "abilities": [
61+ "EntryAbility"
62+ ],
63+ "when": "inuse"
64+ }
65+ }
66+ ]
67+ }
68+}
@@ -0,0 +1,48 @@
1+{
2+ "color": [
3+ {
4+ "name": "start_window_background",
5+ "value": "#FFFFFF"
6+ },
7+ {
8+ "name": "white",
9+ "value": "#FFFFFF"
10+ },
11+ {
12+ "name": "black",
13+ "value": "#000000"
14+ },
15+ {
16+ "name": "primaryBgColor",
17+ "value": "#F1F3F5"
18+ },
19+ {
20+ "name": "titleColor",
21+ "value": "#182431"
22+ },
23+ {
24+ "name": "signatureColor",
25+ "value": "#66686A"
26+ },
27+ {
28+ "name": "tabTitleColor",
29+ "value": "#999999"
30+ },
31+ {
32+ "name": "mineBgColor",
33+ "value": "#EDF2F5"
34+ },
35+ {
36+ "name": "borderColor",
37+ "value": "#CCCCCC"
38+ },
39+ {
40+ "name": "leveColor",
41+ "value": "#C99411"
42+ },
43+ {
44+ "name": "leveBgColor",
45+ "value": "#D4E6F1"
46+ }
47+ ]
48+}