create-vela-workflow 1.0.0
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 +136 -0
- package/bin/cli.js +188 -0
- package/docs/ai-workflow-tutorial.md +462 -0
- package/docs/official-site-tutorial.md +391 -0
- package/package.json +34 -0
- package/templates/.github/HARNESS-ENGINEERING-GUIDE.md +407 -0
- package/templates/.github/agents/vela-knowledge.agent.md +45 -0
- package/templates/.github/agents/vela-s1-prd.agent.md +69 -0
- package/templates/.github/agents/vela-s2-tech.agent.md +66 -0
- package/templates/.github/agents/vela-s3-coding.agent.md +301 -0
- package/templates/.github/agents/vela-workflow.agent.md +110 -0
- package/templates/.github/copilot-instructions.md +64 -0
- package/templates/.github/prompts/vela-apis.prompt.md +98 -0
- package/templates/.github/prompts/vela-best-practices.prompt.md +93 -0
- package/templates/.github/prompts/vela-components.prompt.md +118 -0
- package/templates/.github/prompts/vela-dev-guide.prompt.md +622 -0
- package/templates/.github/rules/project-init.md +45 -0
- package/templates/.github/rules/vela-coding-convention.md +324 -0
- package/templates/.github/rules/vela-css.md +217 -0
- package/templates/.github/rules/vela-design-driven.md +306 -0
- package/templates/.github/rules/vela-figma-mcp.md +198 -0
- package/templates/.github/rules/vela-format.md +119 -0
- package/templates/.github/rules/vela-layout.md +67 -0
- package/templates/.github/rules/vela-platform.md +46 -0
- package/templates/.github/rules/vela-quality.md +109 -0
- package/templates/.kiro/hooks/figma-design-check.kiro.hook +14 -0
- package/templates/.kiro/hooks/post-coding-validation.kiro.hook +13 -0
- package/templates/.kiro/hooks/validate-ux-files.kiro.hook +16 -0
- package/templates/.kiro/settings/mcp.json +7 -0
- package/templates/.kiro/skills/vela-js-app/SKILL.md +1072 -0
- package/templates/.kiro/steering/workflow-conventions.md +110 -0
- package/templates/.workflow/resource-paths.json +62 -0
- package/templates/.workflow/scripts/.gitkeep +0 -0
- package/templates/.workflow/scripts/checkpoint_manager.js +284 -0
- package/templates/.workflow/scripts/context_loader.js +841 -0
- package/templates/.workflow/scripts/figma_export.js +346 -0
- package/templates/.workflow/scripts/session_manager.js +438 -0
- package/templates/.workflow/stages/.gitkeep +0 -0
- package/templates/.workflow/stages/commands.md +171 -0
- package/templates/.workflow/stages/s1_prd.md +286 -0
- package/templates/.workflow/stages/s2_tech_design.md +302 -0
- package/templates/.workflow/stages/s3_coding.md +699 -0
- package/templates/.workflow/stages/s4_simulator.md +259 -0
- package/templates/.workflow/workflow-config.json +46 -0
- package/templates/.workflow/workflow_starter.md +912 -0
|
@@ -0,0 +1,1072 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: vela-js-app-development
|
|
3
|
+
description: Xiaomi Vela JS 应用开发技能。用于创建、开发和调试运行在小米 IoT 穿戴设备(手表)上的 Vela JS 应用。包含项目结构、manifest 配置、UX 文件编写(template/style/script)、组件使用、接口调用等完整知识。当用户需要开发 Vela JS 应用、创建页面、使用组件或调用系统接口时使用此技能。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Xiaomi Vela JS 应用开发指南
|
|
7
|
+
|
|
8
|
+
官方文档站点:https://iot.mi.com/vela/quickapp/
|
|
9
|
+
|
|
10
|
+
## 框架概述
|
|
11
|
+
|
|
12
|
+
Xiaomi Vela JS 是小米基于 Vela OS 的轻量级 JS 应用框架,面向智能穿戴设备(手表)。采用前端 MVVM 开发范式,使用 `.ux` 文件编写页面,`manifest.json` 配置应用,Flexbox 布局,支持数据绑定和组件化开发。
|
|
13
|
+
|
|
14
|
+
## 创建项目
|
|
15
|
+
|
|
16
|
+
### 方式一:使用脚手架(推荐)
|
|
17
|
+
|
|
18
|
+
检查是否安装了 aiot-toolkit,如果已安装则使用命令创建:
|
|
19
|
+
```bash
|
|
20
|
+
npm create aiot ux -- --name my-app --template vela-demo
|
|
21
|
+
cd my-app
|
|
22
|
+
npm install
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### 方式二:手动创建
|
|
26
|
+
|
|
27
|
+
如果未安装 aiot-toolkit,参考以下目录结构手动创建项目。应用图标 `logo.png` 从 skill 的 assets 目录拷贝到 `src/common/logo.png`(assets 路径:`.kiro/skills/vela-js-app/assets/logo.png`)。
|
|
28
|
+
|
|
29
|
+
## 项目结构
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
├── README.md # 项目说明
|
|
33
|
+
├── .gitignore # Git 忽略配置
|
|
34
|
+
├── package.json # npm 配置,定义构建脚本和依赖
|
|
35
|
+
└── src/
|
|
36
|
+
├── manifest.json # 应用配置(包名、路由、接口声明等)
|
|
37
|
+
├── app.ux # 应用级公共脚本和生命周期
|
|
38
|
+
├── config-watch.json # 手表设备配置(内容为 {})
|
|
39
|
+
├── pages/ # 页面目录
|
|
40
|
+
│ ├── index/
|
|
41
|
+
│ │ └── index.ux
|
|
42
|
+
│ └── detail/
|
|
43
|
+
│ └── detail.ux
|
|
44
|
+
├── i18n/ # 国际化资源
|
|
45
|
+
│ ├── defaults.json
|
|
46
|
+
│ ├── zh-CN.json
|
|
47
|
+
│ └── en.json
|
|
48
|
+
└── common/ # 公共资源
|
|
49
|
+
└── logo.png # 应用图标
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
注意:页面放在 `src/pages/` 下,`manifest.json` 中 `router.pages` 的 key 需带 `pages/` 前缀(如 `"pages/index"` 对应 `src/pages/index/`)。
|
|
53
|
+
|
|
54
|
+
构建后会自动生成 `build/`(中间产物)、`dist/`(rpk 包)目录;`sign/`(签名证书)由 release 命令引导生成。
|
|
55
|
+
|
|
56
|
+
### package.json 示例
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"name": "my-app",
|
|
61
|
+
"version": "1.0.0",
|
|
62
|
+
"scripts": {
|
|
63
|
+
"start": "aiot start --watch",
|
|
64
|
+
"build": "aiot build",
|
|
65
|
+
"release": "aiot release"
|
|
66
|
+
},
|
|
67
|
+
"devDependencies": {
|
|
68
|
+
"aiot-toolkit": "^2.0.5"
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### .gitignore
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
/node_modules
|
|
77
|
+
/dist
|
|
78
|
+
/build
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## manifest.json 配置
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
{
|
|
85
|
+
"package": "com.company.demo",
|
|
86
|
+
"name": "应用名称",
|
|
87
|
+
"icon": "/common/logo.png",
|
|
88
|
+
"versionName": "1.0",
|
|
89
|
+
"versionCode": 1,
|
|
90
|
+
"minPlatformVersion": 1200,
|
|
91
|
+
"deviceTypeList": ["watch"],
|
|
92
|
+
"features": [
|
|
93
|
+
{ "name": "system.router" },
|
|
94
|
+
{ "name": "system.configuration" }
|
|
95
|
+
],
|
|
96
|
+
"config": {
|
|
97
|
+
"logLevel": "log",
|
|
98
|
+
"designWidth": 480
|
|
99
|
+
},
|
|
100
|
+
"router": {
|
|
101
|
+
"entry": "pages/index",
|
|
102
|
+
"pages": {
|
|
103
|
+
"pages/index": {
|
|
104
|
+
"component": "index"
|
|
105
|
+
},
|
|
106
|
+
"pages/detail": {
|
|
107
|
+
"component": "detail"
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
关键字段:
|
|
115
|
+
- `package`:应用包名,格式 com.company.module
|
|
116
|
+
- `name`:应用名称,6个汉字以内
|
|
117
|
+
- `versionCode`:整数版本号,每次发布+1
|
|
118
|
+
- `features`:接口声明数组,使用接口前必须声明
|
|
119
|
+
- `config.designWidth`:设计基准宽度,默认480px
|
|
120
|
+
- `router.entry`:首页路径,如 `"pages/index"`
|
|
121
|
+
- `router.pages`:页面路由配置,key 为页面目录相对 src 的路径(如 `"pages/index"` 对应 `src/pages/index/`),component 为 ux 文件名
|
|
122
|
+
- `router.pages[].launchMode`:页面启动模式,支持 "standard"(默认)和 "singleTask"
|
|
123
|
+
|
|
124
|
+
## UX 文件格式
|
|
125
|
+
|
|
126
|
+
每个页面/组件由 `.ux` 文件编写,包含 template、style、script 三部分:
|
|
127
|
+
|
|
128
|
+
```html
|
|
129
|
+
<template>
|
|
130
|
+
<!-- 只能有一个根节点 -->
|
|
131
|
+
<div class="page">
|
|
132
|
+
<text class="title">{{title}}</text>
|
|
133
|
+
<input type="button" value="点击" onclick="handleClick" />
|
|
134
|
+
</div>
|
|
135
|
+
</template>
|
|
136
|
+
|
|
137
|
+
<style>
|
|
138
|
+
.page {
|
|
139
|
+
flex-direction: column;
|
|
140
|
+
justify-content: center;
|
|
141
|
+
align-items: center;
|
|
142
|
+
}
|
|
143
|
+
.title {
|
|
144
|
+
font-size: 30px;
|
|
145
|
+
color: #333333;
|
|
146
|
+
}
|
|
147
|
+
</style>
|
|
148
|
+
|
|
149
|
+
<script>
|
|
150
|
+
import router from '@system.router'
|
|
151
|
+
|
|
152
|
+
export default {
|
|
153
|
+
private: {
|
|
154
|
+
title: '示例页面'
|
|
155
|
+
},
|
|
156
|
+
onInit() {
|
|
157
|
+
console.log('页面初始化')
|
|
158
|
+
},
|
|
159
|
+
handleClick() {
|
|
160
|
+
router.push({ uri: '/pages/detail' })
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
</script>
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
也可拆分为独立文件(detail.ux + detail.css + detail.js),此时 ux 文件不包含 template 标签。
|
|
167
|
+
|
|
168
|
+
## Template 模板语法
|
|
169
|
+
|
|
170
|
+
### 数据绑定
|
|
171
|
+
```html
|
|
172
|
+
<text>{{message}}</text>
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### 条件渲染
|
|
176
|
+
```html
|
|
177
|
+
<!-- if/elif/else 必须是相邻兄弟节点 -->
|
|
178
|
+
<text if="{{show}}">显示</text>
|
|
179
|
+
<text elif="{{other}}">其他</text>
|
|
180
|
+
<text else>默认</text>
|
|
181
|
+
|
|
182
|
+
<!-- show 指令:不从DOM移除,仅隐藏 -->
|
|
183
|
+
<text show="{{visible}}">内容</text>
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### 列表渲染
|
|
187
|
+
```html
|
|
188
|
+
<!-- 基本用法 -->
|
|
189
|
+
<div for="{{list}}" tid="id">
|
|
190
|
+
<text>{{$idx}}: {{$item.name}}</text>
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
<!-- 自定义变量名 -->
|
|
194
|
+
<div for="(index, item) in list" tid="id">
|
|
195
|
+
<text>{{index}}: {{item.name}}</text>
|
|
196
|
+
</div>
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
- `tid` 属性指定数组元素唯一ID,用于优化渲染
|
|
200
|
+
- for 只能循环数组,不能循环对象
|
|
201
|
+
- `<block>` 标签可用于逻辑控制,不产生额外DOM节点
|
|
202
|
+
|
|
203
|
+
### 事件绑定
|
|
204
|
+
```html
|
|
205
|
+
<text onclick="handleClick">点击</text>
|
|
206
|
+
<text @click="handleClick">简写</text>
|
|
207
|
+
<text onclick="handleClick($idx, $item)">传参</text>
|
|
208
|
+
```
|
|
209
|
+
回调函数末尾自动添加 `evt` 参数。
|
|
210
|
+
|
|
211
|
+
## Style 样式系统
|
|
212
|
+
|
|
213
|
+
### 布局
|
|
214
|
+
- 采用 CSS Flexbox 布局,默认 `flex-direction: row`
|
|
215
|
+
- 盒模型为 `border-box`
|
|
216
|
+
- div 为 Flex 容器,text/span 为文本容器
|
|
217
|
+
|
|
218
|
+
### 长度单位
|
|
219
|
+
- `px`:相对于 designWidth 的适配单位(类似 rem),自动按屏幕宽度缩放
|
|
220
|
+
- `%`:百分比
|
|
221
|
+
- `dp`:设备独立像素(API Level 3+)
|
|
222
|
+
|
|
223
|
+
### 选择器(支持)
|
|
224
|
+
- `.class` / `#id` / `tag` / 并列选择 `.a, .b`
|
|
225
|
+
- 优先级:inline > #id > .class > tag
|
|
226
|
+
- 暂不支持后代选择器
|
|
227
|
+
|
|
228
|
+
### 样式预编译
|
|
229
|
+
支持 less 和 scss:
|
|
230
|
+
```html
|
|
231
|
+
<style lang="less">
|
|
232
|
+
@import './style.less';
|
|
233
|
+
</style>
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### 通用样式属性
|
|
237
|
+
width, height, min-width, min-height, max-width, max-height, padding, margin, border, border-radius, background-color, background-image, background-size, background-position, color, opacity, display(flex|none), visibility, position(relative|absolute), flex, flex-grow, flex-shrink, flex-direction, align-items, justify-content, box-shadow, left/top/right/bottom
|
|
238
|
+
|
|
239
|
+
## Script 脚本
|
|
240
|
+
|
|
241
|
+
### 页面数据对象
|
|
242
|
+
```javascript
|
|
243
|
+
export default {
|
|
244
|
+
// 页面级数据(三选一,不能与 data 同时使用)
|
|
245
|
+
public: {}, // 允许被外部传入数据覆盖
|
|
246
|
+
protected: {}, // 允许被应用内部页面传参覆盖
|
|
247
|
+
private: {}, // 不允许被覆盖
|
|
248
|
+
|
|
249
|
+
// 组件级数据
|
|
250
|
+
data: {},
|
|
251
|
+
|
|
252
|
+
// 计算属性
|
|
253
|
+
computed: {
|
|
254
|
+
fullName() {
|
|
255
|
+
return this.firstName + ' ' + this.lastName
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### 页面生命周期
|
|
262
|
+
- `onInit()`:数据准备好,可使用页面数据
|
|
263
|
+
- `onReady()`:模板编译完成,可获取DOM节点
|
|
264
|
+
- `onShow()`:页面显示
|
|
265
|
+
- `onHide()`:页面隐藏
|
|
266
|
+
- `onDestroy()`:页面销毁,应释放资源
|
|
267
|
+
- `onBackPress()`:返回按键,return true 阻止返回
|
|
268
|
+
- `onRefresh(query)`:singleTask模式页面重新打开
|
|
269
|
+
- `onConfigurationChanged(event)`:系统配置变化(如语言)
|
|
270
|
+
|
|
271
|
+
### APP 生命周期(app.ux)
|
|
272
|
+
- `onCreate()` / `onShow()` / `onHide()` / `onDestroy()` / `onError(e)`
|
|
273
|
+
|
|
274
|
+
### 全局对象和方法
|
|
275
|
+
```javascript
|
|
276
|
+
// 访问 app.ux 中定义的数据和方法
|
|
277
|
+
this.$app.$def.data1
|
|
278
|
+
this.$app.$def.method1()
|
|
279
|
+
|
|
280
|
+
// 退出应用
|
|
281
|
+
this.$app.exit()
|
|
282
|
+
|
|
283
|
+
// 页面信息
|
|
284
|
+
this.$page.name / this.$page.path
|
|
285
|
+
|
|
286
|
+
// 页面有效性
|
|
287
|
+
this.$valid
|
|
288
|
+
|
|
289
|
+
// DOM操作
|
|
290
|
+
this.$element('id')
|
|
291
|
+
|
|
292
|
+
// 数据监听
|
|
293
|
+
this.$watch('propName', 'handlerName')
|
|
294
|
+
|
|
295
|
+
// 能力查询
|
|
296
|
+
this.$canIUse('@system.router.push')
|
|
297
|
+
|
|
298
|
+
// 下次DOM更新后回调
|
|
299
|
+
this.$nextTick(() => { /* ... */ })
|
|
300
|
+
|
|
301
|
+
// 事件通信
|
|
302
|
+
this.$on('eventName', handler)
|
|
303
|
+
this.$off('eventName', handler)
|
|
304
|
+
this.$emit('eventName', { data: 1 }) // 触发当前组件事件
|
|
305
|
+
this.$dispatch('eventName', { data: 1 }) // 向上传递
|
|
306
|
+
this.$broadcast('eventName', { data: 1 }) // 向下传递
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
## 自定义组件
|
|
310
|
+
|
|
311
|
+
### 定义组件(comp.ux)
|
|
312
|
+
```html
|
|
313
|
+
<template>
|
|
314
|
+
<div>
|
|
315
|
+
<text>{{say}}</text>
|
|
316
|
+
</div>
|
|
317
|
+
</template>
|
|
318
|
+
<script>
|
|
319
|
+
export default {
|
|
320
|
+
props: ['say'], // 或 Object 形式设置默认值和类型校验
|
|
321
|
+
data: { localVal: '' }, // 组件只能用 data
|
|
322
|
+
onInit() {},
|
|
323
|
+
onReady() {},
|
|
324
|
+
onDestroy() {}
|
|
325
|
+
}
|
|
326
|
+
</script>
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### 引入组件
|
|
330
|
+
```html
|
|
331
|
+
<import name="my-comp" src="./comp"></import>
|
|
332
|
+
<template>
|
|
333
|
+
<div>
|
|
334
|
+
<my-comp say="{{message}}" prop-object="{{obj}}"></my-comp>
|
|
335
|
+
</div>
|
|
336
|
+
</template>
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
### Props
|
|
340
|
+
- 驼峰命名在模板中转为短横线:`propObject` → `prop-object`
|
|
341
|
+
- 数据单向流动:父→子
|
|
342
|
+
- 支持默认值和类型校验(String/Number/Boolean/Function/Object/Array)
|
|
343
|
+
|
|
344
|
+
### 父子通信
|
|
345
|
+
- 父→子:通过 props 传递数据
|
|
346
|
+
- 子→父:`this.$emit('eventName', data)` + 父组件 `onemit-evt="handler"`
|
|
347
|
+
- 向上传递:`this.$dispatch()` + 父组件 `this.$on()`
|
|
348
|
+
- 向下传递:`this.$broadcast()` + 子组件 `this.$on()`
|
|
349
|
+
|
|
350
|
+
## 页面切换
|
|
351
|
+
|
|
352
|
+
```javascript
|
|
353
|
+
import router from '@system.router'
|
|
354
|
+
|
|
355
|
+
// 跳转(保留当前页面)
|
|
356
|
+
router.push({ uri: '/pages/detail', params: { id: '1' } })
|
|
357
|
+
|
|
358
|
+
// 替换(销毁当前页面)
|
|
359
|
+
router.replace({ uri: '/pages/detail', params: { id: '1' } })
|
|
360
|
+
|
|
361
|
+
// 返回
|
|
362
|
+
router.back()
|
|
363
|
+
router.back({ path: '/pages/index' })
|
|
364
|
+
|
|
365
|
+
// 清空页面栈
|
|
366
|
+
router.clear()
|
|
367
|
+
|
|
368
|
+
// 获取页面信息
|
|
369
|
+
router.getLength()
|
|
370
|
+
router.getState() // { index, name, path }
|
|
371
|
+
router.getPages() // [{ name, path }, ...]
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
接收参数:在目标页面的 `protected`(应用内)或 `public`(应用外)中声明同名属性。
|
|
375
|
+
|
|
376
|
+
## 组件详解
|
|
377
|
+
|
|
378
|
+
官方组件文档:https://iot.mi.com/vela/quickapp/zh/components/
|
|
379
|
+
|
|
380
|
+
### 容器组件
|
|
381
|
+
|
|
382
|
+
**div** — 基础 Flex 容器
|
|
383
|
+
[文档](https://iot.mi.com/vela/quickapp/zh/components/container/div.html)
|
|
384
|
+
|
|
385
|
+
**list + list-item** — 高性能列表
|
|
386
|
+
[list 文档](https://iot.mi.com/vela/quickapp/zh/components/container/list.html) | [list-item 文档](https://iot.mi.com/vela/quickapp/zh/components/container/list-item.html)
|
|
387
|
+
```html
|
|
388
|
+
<list class="list" onscrollbottom="loadMore">
|
|
389
|
+
<list-item type="item" for="{{list}}" tid="id">
|
|
390
|
+
<text>{{$item.name}}</text>
|
|
391
|
+
</list-item>
|
|
392
|
+
</list>
|
|
393
|
+
```
|
|
394
|
+
- `list-item` 必须设置 `type` 属性,相同结构的 item 使用相同 type
|
|
395
|
+
- list 事件:`scroll`({scrollX, scrollY, scrollState})、`scrollbottom`、`scrolltop`
|
|
396
|
+
- list 方法:`scrollTo({index, behavior})` / `scrollBy({top, behavior})`
|
|
397
|
+
|
|
398
|
+
**scroll** — 滚动容器
|
|
399
|
+
[文档](https://iot.mi.com/vela/quickapp/zh/components/container/scroll.html)
|
|
400
|
+
```html
|
|
401
|
+
<scroll scroll-y="true" style="height: 300px;" onscroll="onScroll">
|
|
402
|
+
<!-- 内容 -->
|
|
403
|
+
</scroll>
|
|
404
|
+
```
|
|
405
|
+
- 属性:`scroll-x` / `scroll-y`(boolean)、`scroll-top` / `scroll-left`(设置滚动位置)
|
|
406
|
+
- 样式:`scroll-snap-type` / `scroll-snap-align`(滚动吸附)
|
|
407
|
+
|
|
408
|
+
**swiper** — 滑块视图
|
|
409
|
+
[文档](https://iot.mi.com/vela/quickapp/zh/components/container/swiper.html)
|
|
410
|
+
```html
|
|
411
|
+
<swiper index="0" autoplay="true" interval="3000" loop="true" onchange="onChange">
|
|
412
|
+
<div><text>页面1</text></div>
|
|
413
|
+
<div><text>页面2</text></div>
|
|
414
|
+
</swiper>
|
|
415
|
+
```
|
|
416
|
+
- 属性:`index` / `autoplay` / `interval` / `loop` / `vertical` / `indicator` / `previousmargin` / `nextmargin`
|
|
417
|
+
- 样式:`indicator-color` / `indicator-selected-color` / `indicator-size`
|
|
418
|
+
- 事件:`change`({index})
|
|
419
|
+
- 方法:`swipeTo({index})`
|
|
420
|
+
|
|
421
|
+
**stack** — 层叠容器
|
|
422
|
+
[文档](https://iot.mi.com/vela/quickapp/zh/components/container/stack.html)
|
|
423
|
+
|
|
424
|
+
### 基础组件
|
|
425
|
+
|
|
426
|
+
**text** — 文本
|
|
427
|
+
[文档](https://iot.mi.com/vela/quickapp/zh/components/basic/text.html)
|
|
428
|
+
- 样式:`lines`(行数)、`text-overflow`(clip/ellipsis,需配合 lines)、`color`、`font-size`、`font-weight`、`text-align`、`line-height`、`text-indent`
|
|
429
|
+
- 子组件仅支持 `<span>`
|
|
430
|
+
|
|
431
|
+
**image** — 图片
|
|
432
|
+
[文档](https://iot.mi.com/vela/quickapp/zh/components/basic/image.html)
|
|
433
|
+
```html
|
|
434
|
+
<image src="/common/logo.png" alt="blank" />
|
|
435
|
+
```
|
|
436
|
+
- 属性:`src`(本地/网络 uri)、`alt`(占位图,设为 "blank" 无占位图)
|
|
437
|
+
- 样式:`object-fit`(contain/cover/none/scale-down,默认 cover)
|
|
438
|
+
- 事件:`complete`({width, height})、`error`
|
|
439
|
+
|
|
440
|
+
**其他基础组件**:
|
|
441
|
+
- `span`:行内文本,仅作 text 子组件 [文档](https://iot.mi.com/vela/quickapp/zh/components/basic/span.html)
|
|
442
|
+
- `a`:链接 [文档](https://iot.mi.com/vela/quickapp/zh/components/basic/a.html)
|
|
443
|
+
- `progress`:进度条(type: horizontal/circular, percent) [文档](https://iot.mi.com/vela/quickapp/zh/components/basic/progress.html)
|
|
444
|
+
- `marquee`:跑马灯 [文档](https://iot.mi.com/vela/quickapp/zh/components/basic/marquee.html)
|
|
445
|
+
- `chart`:图表 [文档](https://iot.mi.com/vela/quickapp/zh/components/basic/chart.html)
|
|
446
|
+
- `qrcode`:二维码 [文档](https://iot.mi.com/vela/quickapp/zh/components/basic/qrcode.html)
|
|
447
|
+
- `barcode`:条形码 [文档](https://iot.mi.com/vela/quickapp/zh/components/basic/barcode.html)
|
|
448
|
+
- `image-animator`:帧动画 [文档](https://iot.mi.com/vela/quickapp/zh/components/basic/image-animator.html)
|
|
449
|
+
|
|
450
|
+
### 表单组件
|
|
451
|
+
|
|
452
|
+
**input** — 输入/按钮/选择
|
|
453
|
+
[文档](https://iot.mi.com/vela/quickapp/zh/components/form/input.html)
|
|
454
|
+
```html
|
|
455
|
+
<input type="button" value="点击" onclick="onClick" />
|
|
456
|
+
<input type="checkbox" checked="{{checked}}" onchange="onCheck" />
|
|
457
|
+
<input type="radio" name="group" value="1" onchange="onRadio" />
|
|
458
|
+
```
|
|
459
|
+
- type:`button` / `checkbox` / `radio`
|
|
460
|
+
- 属性:`checked`(checkbox/radio)、`name`、`value`
|
|
461
|
+
- 事件:`change`({name, value, checked}),button 无 change 事件
|
|
462
|
+
|
|
463
|
+
**picker** — 选择器
|
|
464
|
+
[文档](https://iot.mi.com/vela/quickapp/zh/components/form/picker.html)
|
|
465
|
+
- type:`text` / `date` / `time` / `multi-text`
|
|
466
|
+
|
|
467
|
+
**slider** — 滑块 [文档](https://iot.mi.com/vela/quickapp/zh/components/form/slider.html)
|
|
468
|
+
|
|
469
|
+
**switch** — 开关 [文档](https://iot.mi.com/vela/quickapp/zh/components/form/switch.html)
|
|
470
|
+
|
|
471
|
+
### 通用属性
|
|
472
|
+
所有组件支持:`id`, `style`, `class`, `for`, `if`, `show`, `data-*`
|
|
473
|
+
[文档](https://iot.mi.com/vela/quickapp/zh/components/general/properties.html)
|
|
474
|
+
|
|
475
|
+
### 通用事件
|
|
476
|
+
所有组件支持:`touchstart`, `touchmove`, `touchend`, `click`, `longpress`, `swipe`
|
|
477
|
+
[文档](https://iot.mi.com/vela/quickapp/zh/components/general/events.html)
|
|
478
|
+
|
|
479
|
+
### 通用样式
|
|
480
|
+
[文档](https://iot.mi.com/vela/quickapp/zh/components/general/style.html) | [颜色](https://iot.mi.com/vela/quickapp/zh/components/general/color.html) | [动画](https://iot.mi.com/vela/quickapp/zh/components/general/animation-style.html) | [背景图](https://iot.mi.com/vela/quickapp/zh/components/general/background-img-styles.html)
|
|
481
|
+
|
|
482
|
+
## 接口详解
|
|
483
|
+
|
|
484
|
+
官方接口文档:https://iot.mi.com/vela/quickapp/zh/features/
|
|
485
|
+
|
|
486
|
+
### 高频接口完整用法
|
|
487
|
+
|
|
488
|
+
**fetch — 网络请求**
|
|
489
|
+
[文档](https://iot.mi.com/vela/quickapp/zh/features/network/fetch.html)
|
|
490
|
+
声明:`{ "name": "system.fetch" }`
|
|
491
|
+
```javascript
|
|
492
|
+
import fetch from '@system.fetch'
|
|
493
|
+
|
|
494
|
+
// GET 请求
|
|
495
|
+
fetch.fetch({
|
|
496
|
+
url: 'https://api.example.com/data',
|
|
497
|
+
responseType: 'json',
|
|
498
|
+
success(res) {
|
|
499
|
+
console.log(res.data) // Object(json时)
|
|
500
|
+
console.log(res.code) // HTTP 状态码
|
|
501
|
+
console.log(res.headers)
|
|
502
|
+
},
|
|
503
|
+
fail(data, code) {
|
|
504
|
+
console.log(code)
|
|
505
|
+
}
|
|
506
|
+
})
|
|
507
|
+
|
|
508
|
+
// POST 请求
|
|
509
|
+
fetch.fetch({
|
|
510
|
+
url: 'https://api.example.com/submit',
|
|
511
|
+
method: 'POST',
|
|
512
|
+
header: { 'Content-Type': 'application/json' },
|
|
513
|
+
data: JSON.stringify({ key: 'value' }),
|
|
514
|
+
responseType: 'json',
|
|
515
|
+
success(res) { console.log(res.data) }
|
|
516
|
+
})
|
|
517
|
+
```
|
|
518
|
+
- 参数:`url`、`method`(GET/POST/PUT/DELETE...)、`header`、`data`(String/Object/ArrayBuffer)、`responseType`(text/json/file/arraybuffer)
|
|
519
|
+
- 返回:`{code, data, headers}`
|
|
520
|
+
|
|
521
|
+
**audio — 音频播放**
|
|
522
|
+
[文档](https://iot.mi.com/vela/quickapp/zh/features/other/audio.html)
|
|
523
|
+
声明:`{ "name": "system.audio" }`
|
|
524
|
+
```javascript
|
|
525
|
+
import audio from '@system.audio'
|
|
526
|
+
|
|
527
|
+
audio.src = '/common/music.mp3'
|
|
528
|
+
audio.autoplay = false
|
|
529
|
+
audio.loop = false
|
|
530
|
+
audio.volume = 0.8
|
|
531
|
+
|
|
532
|
+
audio.play()
|
|
533
|
+
audio.pause()
|
|
534
|
+
audio.stop()
|
|
535
|
+
|
|
536
|
+
audio.onended = function() { console.log('播放结束') }
|
|
537
|
+
audio.onerror = function() { console.log('播放出错') }
|
|
538
|
+
|
|
539
|
+
audio.getPlayState({
|
|
540
|
+
success(data) {
|
|
541
|
+
// data: { state, src, currentTime, duration, percent, autoplay, loop, volume, muted }
|
|
542
|
+
}
|
|
543
|
+
})
|
|
544
|
+
```
|
|
545
|
+
- 可读写属性:`src`、`currentTime`、`autoplay`、`loop`、`volume`、`muted`
|
|
546
|
+
- 只读属性:`duration`
|
|
547
|
+
- 事件:`onplay`、`onpause`、`onstop`、`onended`、`onerror`、`onloadeddata`、`ondurationchange`
|
|
548
|
+
|
|
549
|
+
**prompt — 弹窗提示**
|
|
550
|
+
[文档](https://iot.mi.com/vela/quickapp/zh/features/other/prompt.html)
|
|
551
|
+
声明:`{ "name": "system.prompt" }`
|
|
552
|
+
```javascript
|
|
553
|
+
import prompt from '@system.prompt'
|
|
554
|
+
|
|
555
|
+
prompt.showToast({ message: '操作成功', duration: 2000 })
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
**storage — 键值存储**
|
|
559
|
+
[文档](https://iot.mi.com/vela/quickapp/zh/features/data/storage.html)
|
|
560
|
+
声明:`{ "name": "system.storage" }`
|
|
561
|
+
```javascript
|
|
562
|
+
import storage from '@system.storage'
|
|
563
|
+
|
|
564
|
+
storage.set({ key: 'token', value: 'xxx' })
|
|
565
|
+
storage.get({ key: 'token', success(data) { console.log(data) } })
|
|
566
|
+
storage.delete({ key: 'token' })
|
|
567
|
+
storage.clear()
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
**router — 页面路由**(无需声明)
|
|
571
|
+
[文档](https://iot.mi.com/vela/quickapp/zh/features/basic/router.html)
|
|
572
|
+
用法见上方"页面切换"章节。
|
|
573
|
+
|
|
574
|
+
### 其他接口速查
|
|
575
|
+
|
|
576
|
+
**基本功能**:
|
|
577
|
+
- `@system.app`:应用管理 [文档](https://iot.mi.com/vela/quickapp/zh/features/basic/app.html)
|
|
578
|
+
- `@system.configuration`:应用配置 [文档](https://iot.mi.com/vela/quickapp/zh/features/basic/configuration.html)
|
|
579
|
+
- `@system.device`:设备信息 [文档](https://iot.mi.com/vela/quickapp/zh/features/basic/device.html)
|
|
580
|
+
|
|
581
|
+
**网络**:
|
|
582
|
+
- `@system.request`:下载管理(download/onDownloadComplete) [文档](https://iot.mi.com/vela/quickapp/zh/features/network/request.html)
|
|
583
|
+
- `@system.interconnect`:设备互联(instance/send/diagnosis) [文档](https://iot.mi.com/vela/quickapp/zh/features/network/interconnect.html)
|
|
584
|
+
- `@system.uploadtask`:上传任务 [文档](https://iot.mi.com/vela/quickapp/zh/features/network/uploadtask.html)
|
|
585
|
+
|
|
586
|
+
**系统能力**:
|
|
587
|
+
- `@system.network`:网络状态(subscribe/unsubscribe/getType) [文档](https://iot.mi.com/vela/quickapp/zh/features/system/network.html)
|
|
588
|
+
- `@system.vibrator`:振动 [文档](https://iot.mi.com/vela/quickapp/zh/features/system/vibrator.html)
|
|
589
|
+
- `@system.brightness`:屏幕亮度(getValue/setValue/getMode/setMode) [文档](https://iot.mi.com/vela/quickapp/zh/features/system/brightness.html)
|
|
590
|
+
- `@system.record`:录音 [文档](https://iot.mi.com/vela/quickapp/zh/features/system/record.html)
|
|
591
|
+
- `@system.geolocation`:地理位置(需权限 hapjs.permission.LOCATION) [文档](https://iot.mi.com/vela/quickapp/zh/features/system/geolocation.html)
|
|
592
|
+
- `@system.sensor`:传感器(加速度、陀螺仪、心率等) [文档](https://iot.mi.com/vela/quickapp/zh/features/system/sensor.html)
|
|
593
|
+
- `@system.event`:系统事件 [文档](https://iot.mi.com/vela/quickapp/zh/features/system/event.html)
|
|
594
|
+
- `@system.battery`:电池信息 [文档](https://iot.mi.com/vela/quickapp/zh/features/system/battery.html)
|
|
595
|
+
- `@system.volume`:音量控制 [文档](https://iot.mi.com/vela/quickapp/zh/features/system/volume.html)
|
|
596
|
+
- `@system.zip`:压缩解压 [文档](https://iot.mi.com/vela/quickapp/zh/features/system/zip.html)
|
|
597
|
+
- `@system.alarm`:闹钟(setAlarm/getAlarm/cancelAlarm) [文档](https://iot.mi.com/vela/quickapp/zh/features/system/alarm.html)
|
|
598
|
+
|
|
599
|
+
**安全**:
|
|
600
|
+
- `@system.crypto`:加密(rsa/aes/digest/hmac 等) [文档](https://iot.mi.com/vela/quickapp/zh/features/security/crypto.html)
|
|
601
|
+
- `@system.secureelement`:安全元件 [文档](https://iot.mi.com/vela/quickapp/zh/features/security/secureelement.html)
|
|
602
|
+
|
|
603
|
+
**其他**:
|
|
604
|
+
- `@system.file`:文件操作(readText/writeText/list/get/delete/move/copy/mkdir/rmdir) [文档](https://iot.mi.com/vela/quickapp/zh/features/data/file.html)
|
|
605
|
+
- `@system.protobuf`:Protobuf 编解码 [文档](https://iot.mi.com/vela/quickapp/zh/features/other/protobuf.html)
|
|
606
|
+
- `@system.serviceclient`:服务客户端 [文档](https://iot.mi.com/vela/quickapp/zh/features/other/serviceclient.html)
|
|
607
|
+
|
|
608
|
+
### 通用错误码
|
|
609
|
+
- 200:系统通用错误
|
|
610
|
+
- 201:用户拒绝
|
|
611
|
+
- 202:参数错误
|
|
612
|
+
- 203:功能不支持
|
|
613
|
+
- 204:请求超时
|
|
614
|
+
- 300:I/O 错误
|
|
615
|
+
|
|
616
|
+
## 文件存储分区
|
|
617
|
+
|
|
618
|
+
| 分区 | URI | 读写 | 说明 |
|
|
619
|
+
|------|-----|------|------|
|
|
620
|
+
| 应用资源 | /path | 只读 | 应用内置资源 |
|
|
621
|
+
| Cache | internal://cache/path | 读写 | 缓存文件,可能被系统清理 |
|
|
622
|
+
| Files | internal://files/path | 读写 | 永久小文件 |
|
|
623
|
+
| Mass | internal://mass/path | 读写 | 大文件,不保证可用 |
|
|
624
|
+
| Temp | internal://tmp/path | 只读 | 临时文件,应用重启后失效 |
|
|
625
|
+
|
|
626
|
+
## 多屏适配
|
|
627
|
+
|
|
628
|
+
- 使用 `config.designWidth` 设置设计基准宽度
|
|
629
|
+
- px 单位会根据实际屏幕宽度自动缩放
|
|
630
|
+
- 转换公式:`设计稿1px / 设计稿宽度 = 框架样式1px / designWidth`
|
|
631
|
+
- 支持 media query 进行条件样式
|
|
632
|
+
|
|
633
|
+
## 国际化(i18n)
|
|
634
|
+
|
|
635
|
+
在 `src/i18n/` 目录下定义 JSON 资源文件:
|
|
636
|
+
- 文件命名优先级:`zh-CN.json` > `zh.json` > `defaults.json`
|
|
637
|
+
- 支持基础文本、命名插值 `{msg}`、列表插值 `{0}`、单复数 `car | cars`
|
|
638
|
+
|
|
639
|
+
页面中使用:
|
|
640
|
+
```html
|
|
641
|
+
<text>{{ $t('message.hello') }}</text>
|
|
642
|
+
<text>{{ $t('message.greeting', { name: '小明' }) }}</text>
|
|
643
|
+
<text>{{ $tc('message.car', 2) }}</text>
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
```javascript
|
|
647
|
+
// script 中
|
|
648
|
+
this.$t('message.hello')
|
|
649
|
+
this.$tc('message.apple', 2, { count: 6 })
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
语言变化监听:
|
|
653
|
+
```javascript
|
|
654
|
+
onConfigurationChanged(event) {
|
|
655
|
+
if (event.type === 'locale') {
|
|
656
|
+
console.log('语言已切换')
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
## 后台运行
|
|
662
|
+
|
|
663
|
+
在 manifest.json 的 `config.background.features` 中声明需要后台运行的接口:
|
|
664
|
+
```json
|
|
665
|
+
{
|
|
666
|
+
"config": {
|
|
667
|
+
"background": {
|
|
668
|
+
"features": ["system.audio", "system.request", "system.geolocation"]
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
```
|
|
673
|
+
建议将后台运行逻辑放在 `app.ux` 中,避免页面销毁影响。
|
|
674
|
+
|
|
675
|
+
## 动态组件
|
|
676
|
+
|
|
677
|
+
使用 `<component>` 元素的 `is` 属性动态切换组件:
|
|
678
|
+
```html
|
|
679
|
+
<import src="./part1.ux" name="part1"></import>
|
|
680
|
+
<import src="./part2.ux" name="part2"></import>
|
|
681
|
+
<template>
|
|
682
|
+
<div>
|
|
683
|
+
<component is="{{'part' + status}}"></component>
|
|
684
|
+
</div>
|
|
685
|
+
</template>
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
## 动画样式
|
|
689
|
+
|
|
690
|
+
### transform
|
|
691
|
+
```css
|
|
692
|
+
div {
|
|
693
|
+
transform: translate(10px, 20px) rotate(45deg) scale(1.5);
|
|
694
|
+
transform-origin: 50% 50%;
|
|
695
|
+
}
|
|
696
|
+
```
|
|
697
|
+
支持:translate/translateX/translateY、scale/scaleX/scaleY、rotate
|
|
698
|
+
|
|
699
|
+
### animation + @keyframes
|
|
700
|
+
```css
|
|
701
|
+
.box {
|
|
702
|
+
animation-name: fadeIn;
|
|
703
|
+
animation-duration: 1s;
|
|
704
|
+
animation-timing-function: ease;
|
|
705
|
+
animation-iteration-count: infinite;
|
|
706
|
+
animation-delay: 0s;
|
|
707
|
+
}
|
|
708
|
+
@keyframes fadeIn {
|
|
709
|
+
from { opacity: 0; }
|
|
710
|
+
to { opacity: 1; }
|
|
711
|
+
}
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
### transition
|
|
715
|
+
```css
|
|
716
|
+
.box {
|
|
717
|
+
transition-property: width, background-color;
|
|
718
|
+
transition-duration: 0.3s;
|
|
719
|
+
transition-timing-function: ease-in-out;
|
|
720
|
+
transition-delay: 0s;
|
|
721
|
+
}
|
|
722
|
+
```
|
|
723
|
+
|
|
724
|
+
## Media Query 媒体查询
|
|
725
|
+
|
|
726
|
+
根据设备特征条件应用样式,单位为 dp(设备独立像素):
|
|
727
|
+
```css
|
|
728
|
+
@media (shape: circle) {
|
|
729
|
+
.box { border-radius: 50%; }
|
|
730
|
+
}
|
|
731
|
+
@media (device-type: watch) and (min-width: 200) {
|
|
732
|
+
.box { width: 100%; }
|
|
733
|
+
}
|
|
734
|
+
@media (shape: circle) or (shape: pill-shaped) {
|
|
735
|
+
.box { padding: 10px; }
|
|
736
|
+
}
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
支持的媒体特性:
|
|
740
|
+
- `shape`:circle / rect / pill-shaped
|
|
741
|
+
- `device-type`:watch / band / smartspeaker
|
|
742
|
+
- `width` / `min-width` / `max-width`(dp单位,不带单位书写)
|
|
743
|
+
- `height` / `min-height` / `max-height`
|
|
744
|
+
- `aspect-ratio` / `min-aspect-ratio` / `max-aspect-ratio`
|
|
745
|
+
|
|
746
|
+
逻辑操作符:`and` / `not` / `only` / `or` / `,`(逗号等同or)/ `<=` / `>=`
|
|
747
|
+
|
|
748
|
+
也支持 `@import` 方式:
|
|
749
|
+
```css
|
|
750
|
+
@import './circle.css' (shape: circle);
|
|
751
|
+
```
|
|
752
|
+
|
|
753
|
+
常见设备 dp 参考:
|
|
754
|
+
- Xiaomi Watch S1 Pro/S5:480×480px, DPR=2, 宽=240dp
|
|
755
|
+
- Xiaomi Watch S3/S4/H1:466×466px, DPR=2, 宽=233dp
|
|
756
|
+
- REDMI Watch 5:432×514px, DPR=2, 宽=216dp
|
|
757
|
+
- 小米手环9:192×490px, DPR=2, 宽=96dp
|
|
758
|
+
|
|
759
|
+
## 条件编译
|
|
760
|
+
|
|
761
|
+
基于设备特性在编译时选择性包含代码,支持 ux/js/css 文件:
|
|
762
|
+
```html
|
|
763
|
+
<!-- template 中 -->
|
|
764
|
+
<!-- if true: process.env.SHAPE === 'CIRCLE' -->
|
|
765
|
+
<div class="circle-layout"></div>
|
|
766
|
+
<!-- endif -->
|
|
767
|
+
|
|
768
|
+
<!-- script 中 -->
|
|
769
|
+
// if true: process.env.SHAPE === 'RECT'
|
|
770
|
+
console.log('矩形屏幕')
|
|
771
|
+
// endif
|
|
772
|
+
```
|
|
773
|
+
|
|
774
|
+
需安装 `conditional-compilation-webpack-plugin` 和 `cross-env`,在 package.json 中配置编译脚本:
|
|
775
|
+
```json
|
|
776
|
+
{
|
|
777
|
+
"scripts": {
|
|
778
|
+
"build:circle": "cross-env DEVICE_TYPE=WATCH SHAPE=CIRCLE aiot build",
|
|
779
|
+
"build:rect": "cross-env DEVICE_TYPE=WATCH SHAPE=RECT aiot build"
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
```
|
|
783
|
+
|
|
784
|
+
## hap 链接
|
|
785
|
+
|
|
786
|
+
router 支持 `hap://` 协议打开其他 JS 应用:
|
|
787
|
+
```
|
|
788
|
+
hap://app/<package>/[path][?key=value]
|
|
789
|
+
```
|
|
790
|
+
示例:`hap://app/com.example.demo/Detail?id=1`
|
|
791
|
+
|
|
792
|
+
## 颜色配置
|
|
793
|
+
|
|
794
|
+
支持的颜色格式:
|
|
795
|
+
- `#rgb` / `#rrggbb` / `#rgba` / `#rrggbbaa`
|
|
796
|
+
- `rgb(255, 0, 0)` / `rgba(255, 0, 0, 0.5)`
|
|
797
|
+
- `transparent`
|
|
798
|
+
- CSS 颜色名:`red`, `blue`, `black`, `white` 等
|
|
799
|
+
|
|
800
|
+
## 通用组件方法
|
|
801
|
+
|
|
802
|
+
通过 `this.$element('id')` 获取 DOM 节点后可调用:
|
|
803
|
+
|
|
804
|
+
### getBoundingClientRect(OBJECT)
|
|
805
|
+
获取元素位置和尺寸(需在 onShow 之后调用):
|
|
806
|
+
```javascript
|
|
807
|
+
this.$element('box').getBoundingClientRect({
|
|
808
|
+
success(data) {
|
|
809
|
+
// data: { left, right, top, bottom, width, height }
|
|
810
|
+
}
|
|
811
|
+
})
|
|
812
|
+
```
|
|
813
|
+
|
|
814
|
+
### focus(OBJECT)
|
|
815
|
+
```javascript
|
|
816
|
+
this.$element('input1').focus({ focus: true })
|
|
817
|
+
```
|
|
818
|
+
|
|
819
|
+
## 完整页面示例
|
|
820
|
+
|
|
821
|
+
```html
|
|
822
|
+
<template>
|
|
823
|
+
<div class="page">
|
|
824
|
+
<text class="title">{{title}}</text>
|
|
825
|
+
<div class="list-wrap">
|
|
826
|
+
<div for="{{items}}" tid="id" class="item" onclick="onItemClick($item)">
|
|
827
|
+
<image src="{{$item.icon}}" class="icon" />
|
|
828
|
+
<text class="name">{{$item.name}}</text>
|
|
829
|
+
</div>
|
|
830
|
+
</div>
|
|
831
|
+
<div if="{{items.length === 0}}" class="empty">
|
|
832
|
+
<text>暂无数据</text>
|
|
833
|
+
</div>
|
|
834
|
+
</div>
|
|
835
|
+
</template>
|
|
836
|
+
|
|
837
|
+
<style>
|
|
838
|
+
.page {
|
|
839
|
+
flex-direction: column;
|
|
840
|
+
padding: 20px;
|
|
841
|
+
}
|
|
842
|
+
.title {
|
|
843
|
+
font-size: 36px;
|
|
844
|
+
font-weight: bold;
|
|
845
|
+
margin-bottom: 20px;
|
|
846
|
+
}
|
|
847
|
+
.list-wrap {
|
|
848
|
+
flex-direction: column;
|
|
849
|
+
}
|
|
850
|
+
.item {
|
|
851
|
+
flex-direction: row;
|
|
852
|
+
align-items: center;
|
|
853
|
+
padding: 15px 0;
|
|
854
|
+
border-bottom: 1px solid #eeeeee;
|
|
855
|
+
}
|
|
856
|
+
.icon {
|
|
857
|
+
width: 60px;
|
|
858
|
+
height: 60px;
|
|
859
|
+
margin-right: 15px;
|
|
860
|
+
}
|
|
861
|
+
.name {
|
|
862
|
+
font-size: 28px;
|
|
863
|
+
color: #333333;
|
|
864
|
+
}
|
|
865
|
+
.empty {
|
|
866
|
+
justify-content: center;
|
|
867
|
+
align-items: center;
|
|
868
|
+
margin-top: 100px;
|
|
869
|
+
}
|
|
870
|
+
</style>
|
|
871
|
+
|
|
872
|
+
<script>
|
|
873
|
+
import router from '@system.router'
|
|
874
|
+
import storage from '@system.storage'
|
|
875
|
+
|
|
876
|
+
export default {
|
|
877
|
+
private: {
|
|
878
|
+
title: '我的应用',
|
|
879
|
+
items: []
|
|
880
|
+
},
|
|
881
|
+
onInit() {
|
|
882
|
+
this.loadData()
|
|
883
|
+
},
|
|
884
|
+
loadData() {
|
|
885
|
+
storage.get({
|
|
886
|
+
key: 'items',
|
|
887
|
+
success: (data) => {
|
|
888
|
+
if (data) {
|
|
889
|
+
this.items = JSON.parse(data)
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
})
|
|
893
|
+
},
|
|
894
|
+
onItemClick(item) {
|
|
895
|
+
router.push({
|
|
896
|
+
uri: '/pages/detail',
|
|
897
|
+
params: { id: item.id }
|
|
898
|
+
})
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
</script>
|
|
902
|
+
```
|
|
903
|
+
|
|
904
|
+
## 开发注意事项与最佳实践
|
|
905
|
+
|
|
906
|
+
### 内存优化(重要)
|
|
907
|
+
|
|
908
|
+
手表设备内存有限,必须严格控制内存占用:
|
|
909
|
+
|
|
910
|
+
1. **与 UI 无关的数据不要放在 ViewModel 中**,避免不必要的数据代理开销:
|
|
911
|
+
```javascript
|
|
912
|
+
const someObj = { a: 1 } // ✅ 放在 export default 外部
|
|
913
|
+
export default {
|
|
914
|
+
private: {
|
|
915
|
+
someObj: { a: 1 } // ❌ 不需要绑定到UI的数据不要放这里
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
```
|
|
919
|
+
|
|
920
|
+
2. **数据更新时原地修改,避免重新赋值整个对象/数组**:
|
|
921
|
+
```javascript
|
|
922
|
+
// ❌ this.list = [{ name: 'bb' }]
|
|
923
|
+
// ✅ this.list[0].name = 'bb'
|
|
924
|
+
```
|
|
925
|
+
|
|
926
|
+
3. **使用 `static` 属性标记不会变化的节点**,减少动态节点内存:
|
|
927
|
+
```html
|
|
928
|
+
<text static>{{title}}</text>
|
|
929
|
+
<image static src="/common/logo.png"/>
|
|
930
|
+
<!-- 属性级静态标记 -->
|
|
931
|
+
<text if.static="{{show}}" class.static="{{cls}}">内容</text>
|
|
932
|
+
<!-- block 级静态标记:内部所有节点只渲染一次 -->
|
|
933
|
+
<block static>
|
|
934
|
+
<text>{{fixedTitle}}</text>
|
|
935
|
+
</block>
|
|
936
|
+
```
|
|
937
|
+
|
|
938
|
+
4. **页面销毁时清除定时器**:
|
|
939
|
+
```javascript
|
|
940
|
+
onDestroy() {
|
|
941
|
+
if (this.timer) clearTimeout(this.timer)
|
|
942
|
+
}
|
|
943
|
+
```
|
|
944
|
+
|
|
945
|
+
5. **读取的文件/存储数据用完后及时置 null 释放**
|
|
946
|
+
|
|
947
|
+
6. **调用 `global.runGC()` 手动触发垃圾回收**(不要频繁调用)
|
|
948
|
+
|
|
949
|
+
7. **不要将页面属性和方法缓存到全局**,否则页面销毁后无法释放内存
|
|
950
|
+
|
|
951
|
+
### 启动性能优化
|
|
952
|
+
|
|
953
|
+
1. **避免 setTimeout 延迟跳转**,用 async/await 或回调直接跳转:
|
|
954
|
+
```javascript
|
|
955
|
+
// ✅ 推荐
|
|
956
|
+
async onInit() {
|
|
957
|
+
const data = await this.getData()
|
|
958
|
+
if (!data) router.push({ uri: '/pages/home' })
|
|
959
|
+
}
|
|
960
|
+
```
|
|
961
|
+
|
|
962
|
+
2. **logo 页避免 HTTP 请求**,防止弱网阻塞页面跳转
|
|
963
|
+
|
|
964
|
+
3. **首页数据做本地缓存**,进入时先读缓存展示,同时异步请求更新
|
|
965
|
+
|
|
966
|
+
4. **UI 先行**,不要等数据全部加载完才渲染页面
|
|
967
|
+
|
|
968
|
+
5. **减少 console 打印**,特别是长日志和 JSON 对象,release 包用 TerserPlugin 过滤 `console.debug`
|
|
969
|
+
|
|
970
|
+
### 渲染性能优化
|
|
971
|
+
|
|
972
|
+
1. **list 超过 10 条使用分页渲染**,避免一次性渲染大量数据
|
|
973
|
+
2. **长文案分块渲染**,监听 scroll 事件触底加载下一段
|
|
974
|
+
3. **Swiper 多图使用懒加载**,只保留 3 个子组件动态更新数据
|
|
975
|
+
4. **减少 border-radius 与背景图同时使用**
|
|
976
|
+
5. **图片尺寸与组件尺寸保持一致**,避免运行时缩放
|
|
977
|
+
6. **减少标签嵌套层级和动态样式修改**
|
|
978
|
+
|
|
979
|
+
### 圆形屏幕安全区域
|
|
980
|
+
|
|
981
|
+
手表屏幕多为圆形,四角为不可见区域,页面布局必须留出安全边距,避免内容被圆弧裁切:
|
|
982
|
+
|
|
983
|
+
- **上下安全边距**:约屏幕高度的 10%(如 480px 屏幕留 48-50px)
|
|
984
|
+
- **左右安全边距**:约屏幕宽度的 7-8%(如 480px 屏幕留 34-38px)
|
|
985
|
+
- 内容超出一屏时,使用 `<scroll scroll-y="true">` 包裹,确保可滚动
|
|
986
|
+
|
|
987
|
+
推荐写法:
|
|
988
|
+
```html
|
|
989
|
+
<scroll class="page" scroll-y="true">
|
|
990
|
+
<div class="container">
|
|
991
|
+
<!-- 页面内容 -->
|
|
992
|
+
</div>
|
|
993
|
+
</scroll>
|
|
994
|
+
```
|
|
995
|
+
```css
|
|
996
|
+
.page {
|
|
997
|
+
width: 100%;
|
|
998
|
+
height: 100%;
|
|
999
|
+
}
|
|
1000
|
+
.container {
|
|
1001
|
+
flex-direction: column;
|
|
1002
|
+
padding: 50px 36px;
|
|
1003
|
+
}
|
|
1004
|
+
```
|
|
1005
|
+
|
|
1006
|
+
对于方形屏幕(如 REDMI Watch 5),安全边距可适当减小(上下 20px,左右 16px)。可通过 media query 区分:
|
|
1007
|
+
```css
|
|
1008
|
+
@media (shape: circle) {
|
|
1009
|
+
.container { padding: 50px 36px; }
|
|
1010
|
+
}
|
|
1011
|
+
@media (shape: rect) {
|
|
1012
|
+
.container { padding: 20px 16px; }
|
|
1013
|
+
}
|
|
1014
|
+
```
|
|
1015
|
+
|
|
1016
|
+
### 图片使用规范
|
|
1017
|
+
|
|
1018
|
+
- 尽量使用本地图片,避免在线大图
|
|
1019
|
+
- 在线图片不超过 200KB,尺寸不超过屏幕尺寸
|
|
1020
|
+
- 首次加载大图时增加 loading,下载后缓存到 `internal://files/`
|
|
1021
|
+
- 优先使用 PNG8 格式降低体积
|
|
1022
|
+
- 使用 tinypng.com 等工具压缩图片
|
|
1023
|
+
|
|
1024
|
+
### 代码规范
|
|
1025
|
+
|
|
1026
|
+
1. `app.ux` 中的代码必须写在 `<script>` 标签内
|
|
1027
|
+
2. `template` 只能有一个根节点
|
|
1028
|
+
3. 角度相关 CSS 属性必须带单位:`rotate: 360deg`
|
|
1029
|
+
4. `list-item` 中谨慎使用 `if/else/show`,保证所有 list-item 结构一致
|
|
1030
|
+
5. `image` 的 `src` 不要用变量拼接路径(如 `src="/common/{{type}}"`),直接用完整变量 `src="{{imgPath}}"`
|
|
1031
|
+
6. `input` 等无子元素的标签必须自闭合(如 `<input />`),遵循 XML 规范
|
|
1032
|
+
|
|
1033
|
+
### 异常处理
|
|
1034
|
+
|
|
1035
|
+
1. 网络异常时给用户提示
|
|
1036
|
+
2. 数据异常(空数据/接口错误)要有兜底处理
|
|
1037
|
+
3. 添加 try-catch 捕获 JS 异常
|
|
1038
|
+
4. 按钮防重复点击,点击后加 loading 防止多次请求
|
|
1039
|
+
5. `onShow` 中的 fetch 请求注意:息屏亮屏会重新触发 onShow
|
|
1040
|
+
|
|
1041
|
+
### 包体积优化
|
|
1042
|
+
|
|
1043
|
+
1. 去除未使用的三方依赖,选用轻量替代
|
|
1044
|
+
2. 公共方法挂载到 `global` 上,避免多页面重复 import
|
|
1045
|
+
3. 去除未使用的 CSS 和 JS 代码
|
|
1046
|
+
4. 尽可能减少页面数量
|
|
1047
|
+
|
|
1048
|
+
### 通信(interconnect)注意
|
|
1049
|
+
|
|
1050
|
+
- 使用 `diagnosis()` 方法判断连接状态,不要轮询 `getApkStatus()`
|
|
1051
|
+
- 多条数据直接循环发送,不要加 setTimeout 延迟
|
|
1052
|
+
- 手表 rpk 和手机 app 必须使用相同证书签名
|
|
1053
|
+
|
|
1054
|
+
### 异步接口 Promise 封装
|
|
1055
|
+
|
|
1056
|
+
推荐将异步回调接口统一封装为 Promise,方便 async/await 使用:
|
|
1057
|
+
```javascript
|
|
1058
|
+
function promisify(fn) {
|
|
1059
|
+
return (opts = {}) => new Promise((resolve, reject) => {
|
|
1060
|
+
fn({
|
|
1061
|
+
...opts,
|
|
1062
|
+
success: data => resolve(data),
|
|
1063
|
+
fail: (data, code) => reject({ data, code })
|
|
1064
|
+
})
|
|
1065
|
+
})
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// 使用
|
|
1069
|
+
import storage from '@system.storage'
|
|
1070
|
+
const getItem = promisify(storage.get)
|
|
1071
|
+
const data = await getItem({ key: 'myKey' })
|
|
1072
|
+
```
|