rvis-aiui-kit 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.
Files changed (44) hide show
  1. package/README.md +22 -0
  2. package/components/image/index.ink +44 -0
  3. package/components/list/index.ink +198 -0
  4. package/components/list/readme.md +71 -0
  5. package/components/markdown/index.ink +109 -0
  6. package/components/markdown/parser.js +149 -0
  7. package/components/markdown/readme.md +50 -0
  8. package/components/model-list/index.ink +181 -0
  9. package/components/model-list/readme.md +111 -0
  10. package/components/paragraph/index.ink +61 -0
  11. package/components/paragraph/readme.md +17 -0
  12. package/components/table/index.ink +272 -0
  13. package/components/table/readme.md +83 -0
  14. package/package.json +35 -0
  15. package/sdk/README.md +1013 -0
  16. package/sdk/api-map.js +25 -0
  17. package/sdk/core/client.js +641 -0
  18. package/sdk/core/constants.js +10 -0
  19. package/sdk/core/transport.js +55 -0
  20. package/sdk/index.js +19 -0
  21. package/sdk/modules/audio/index.js +177 -0
  22. package/sdk/modules/audio/readme.md +136 -0
  23. package/sdk/modules/camera/index.js +380 -0
  24. package/sdk/modules/camera/readme.md +302 -0
  25. package/sdk/modules/device-context/index.js +206 -0
  26. package/sdk/modules/device-context/readme.md +243 -0
  27. package/sdk/modules/face/index.js +108 -0
  28. package/sdk/modules/face/readme.md +196 -0
  29. package/sdk/modules/motion/index.js +116 -0
  30. package/sdk/modules/motion/readme.md +148 -0
  31. package/sdk/modules/notification/index.js +192 -0
  32. package/sdk/modules/notification/readme.md +175 -0
  33. package/sdk/modules/offline-command/index.js +265 -0
  34. package/sdk/modules/offline-command/readme.md +143 -0
  35. package/sdk/modules/screen/index.js +64 -0
  36. package/sdk/modules/screen/readme.md +110 -0
  37. package/sdk/modules/tts/index.js +75 -0
  38. package/sdk/modules/tts/readme.md +162 -0
  39. package/sdk/utils/api-builder.js +16 -0
  40. package/sdk/utils/case.js +19 -0
  41. package/sdk/utils/errors.js +32 -0
  42. package/sdk/utils/events.js +22 -0
  43. package/sdk/utils/message.js +63 -0
  44. package/sdk/utils/request-id.js +18 -0
package/README.md ADDED
@@ -0,0 +1,22 @@
1
+ # rvis-aiui-kit
2
+
3
+ RVIS AIUI components and the Glass3 extension SDK.
4
+
5
+ ## SDK
6
+
7
+ ```js
8
+ import glass3 from 'rvis-aiui-kit';
9
+ ```
10
+
11
+ ## Components
12
+
13
+ ```json
14
+ {
15
+ "usingComponents": {
16
+ "rvis-markdown": "rvis-aiui-kit/markdown",
17
+ "rvis-list": "rvis-aiui-kit/list"
18
+ }
19
+ }
20
+ ```
21
+
22
+ Available component exports: `rvis-aiui-kit/image`, `rvis-aiui-kit/list`, `rvis-aiui-kit/markdown`, `rvis-aiui-kit/model-list`, `rvis-aiui-kit/paragraph`, `rvis-aiui-kit/table`.
@@ -0,0 +1,44 @@
1
+ <script def>
2
+ {
3
+ "component": true
4
+ }
5
+ </script>
6
+
7
+ <script setup>
8
+ export default {
9
+ properties: {
10
+ src: {
11
+ type: String,
12
+ value: ''
13
+ },
14
+ width: {
15
+ type: String,
16
+ value: '66px'
17
+ },
18
+ height: {
19
+ type: String,
20
+ value: '66px'
21
+ },
22
+ objectFit: {
23
+ type: String,
24
+ value: 'fill'
25
+ }
26
+ }
27
+ };
28
+ </script>
29
+
30
+ <page>
31
+ <image
32
+ class="image-content"
33
+ src="{{ src }}"
34
+ mode="{{ objectFit === 'contain' ? 'aspectFit' : objectFit === 'cover' ? 'aspectFill' : 'scaleToFill' }}"
35
+ style="width: {{ width }}; height: {{ height }};"
36
+ ></image>
37
+ </page>
38
+
39
+ <style>
40
+ .image-content {
41
+ border-radius: 4px;
42
+ overflow: hidden;
43
+ }
44
+ </style>
@@ -0,0 +1,198 @@
1
+ <script def>
2
+ {
3
+ "component": true
4
+ }
5
+ </script>
6
+
7
+ <script setup>
8
+ export default {
9
+ properties: {
10
+ title: {
11
+ type: String,
12
+ value: ''
13
+ },
14
+ description: {
15
+ type: String,
16
+ value: ''
17
+ },
18
+ items: {
19
+ type: String,
20
+ value: '[]'
21
+ }
22
+ }
23
+ };
24
+ </script>
25
+
26
+ <page>
27
+ <view class="list-card">
28
+ <view class="list-header">
29
+ <text class="list-title">{{ title }}</text>
30
+ <text class="list-description">{{ description }}</text>
31
+ </view>
32
+
33
+ <view class="list-header-divider"></view>
34
+
35
+ <view class="list-body">
36
+ <view
37
+ class="list-item"
38
+ ink:for="{{ JSON.parse(items) }}"
39
+ ink:key="id"
40
+ >
41
+ <view class="list-row">
42
+ <view class="list-status-cell">
43
+ <text
44
+ class="list-checkmark"
45
+ ink:if="{{ item.completed }}"
46
+ >✓</text>
47
+ </view>
48
+
49
+ <view class="list-vertical-divider">
50
+ <view class="list-vertical-dash"></view>
51
+ <view class="list-vertical-dash"></view>
52
+ <view class="list-vertical-dash"></view>
53
+ <view class="list-vertical-dash"></view>
54
+ <view class="list-vertical-dash"></view>
55
+ <view class="list-vertical-dash"></view>
56
+ <view class="list-vertical-dash"></view>
57
+ <view class="list-vertical-dash"></view>
58
+ <view class="list-vertical-dash"></view>
59
+ <view class="list-vertical-dash"></view>
60
+ </view>
61
+
62
+ <view class="list-item-copy">
63
+ <text class="list-item-text">{{ item.text }}</text>
64
+ </view>
65
+ </view>
66
+
67
+ <view
68
+ class="list-row-divider"
69
+ ink:if="{{ index !== JSON.parse(items).length - 1 }}"
70
+ ></view>
71
+ </view>
72
+ </view>
73
+ </view>
74
+ </page>
75
+
76
+ <style>
77
+ .list-card {
78
+ display: flex;
79
+ flex-direction: column;
80
+ width: auto;
81
+ max-width: 424px;
82
+ box-sizing: border-box;
83
+ background-color: #000000;
84
+ border: 2px solid rgba(64, 255, 94, 0.6);
85
+ border-radius: 12px;
86
+ overflow: hidden;
87
+ }
88
+
89
+ .list-header {
90
+ display: flex;
91
+ flex-direction: row;
92
+ align-items: center;
93
+ justify-content: flex-start;
94
+ gap: 8px;
95
+ width: auto;
96
+ height: 30px;
97
+ padding: 0 12px;
98
+ box-sizing: border-box;
99
+ }
100
+
101
+ .list-title {
102
+ color: #40ff5e;
103
+ font-family: monospace;
104
+ font-size: 17px;
105
+ font-weight: 700;
106
+ line-height: 22px;
107
+ }
108
+
109
+ .list-description {
110
+ color: rgba(64, 255, 94, 0.6);
111
+ font-size: 13px;
112
+ line-height: 18px;
113
+ }
114
+
115
+ .list-header-divider {
116
+ width: auto;
117
+ align-self: stretch;
118
+ height: 2px;
119
+ background-color: rgba(64, 255, 94, 0.6);
120
+ }
121
+
122
+ .list-body {
123
+ display: flex;
124
+ flex-direction: column;
125
+ width: auto;
126
+ }
127
+
128
+ .list-item {
129
+ display: flex;
130
+ flex-direction: column;
131
+ width: auto;
132
+ }
133
+
134
+ .list-row {
135
+ display: flex;
136
+ flex-direction: row;
137
+ align-items: center;
138
+ width: auto;
139
+ height: 30px;
140
+ }
141
+
142
+ .list-status-cell {
143
+ display: flex;
144
+ align-items: center;
145
+ justify-content: center;
146
+ width: 42px;
147
+ height: 30px;
148
+ box-sizing: border-box;
149
+ }
150
+
151
+ .list-vertical-divider {
152
+ display: flex;
153
+ flex-direction: column;
154
+ justify-content: space-between;
155
+ width: 2px;
156
+ height: 30px;
157
+ flex-shrink: 0;
158
+ }
159
+
160
+ .list-vertical-dash {
161
+ width: 2px;
162
+ height: 2px;
163
+ background-color: rgba(64, 255, 94, 0.6);
164
+ }
165
+
166
+ .list-checkmark {
167
+ color: #40ff5e;
168
+ font-size: 16px;
169
+ font-weight: 400;
170
+ line-height: 20px;
171
+ }
172
+
173
+ .list-item-copy {
174
+ display: flex;
175
+ flex-direction: row;
176
+ align-items: center;
177
+ padding: 0 12px;
178
+ box-sizing: border-box;
179
+ }
180
+
181
+ .list-item-text {
182
+ color: #40ff5e;
183
+ font-family: sans-serif;
184
+ font-size: 16px;
185
+ font-weight: 400;
186
+ line-height: 20px;
187
+ }
188
+
189
+ .list-row-divider {
190
+ width: auto;
191
+ align-self: stretch;
192
+ height: 1px;
193
+ box-sizing: border-box;
194
+ border-width: 1px 0 0 0;
195
+ border-style: dashed;
196
+ border-color: rgba(64, 255, 94, 0.6);
197
+ }
198
+ </style>
@@ -0,0 +1,71 @@
1
+ # List 列表组件
2
+
3
+ `rvis-list` 用于展示带标题、描述、完成状态和分隔线的紧凑列表。
4
+
5
+ ## 引入组件
6
+
7
+ 在页面的 `<script def>` 中注册组件:
8
+
9
+ ```json
10
+ {
11
+ "usingComponents": {
12
+ "rvis-list": "rvis-aiui-kit/list"
13
+ }
14
+ }
15
+ ```
16
+
17
+ ## API
18
+
19
+ | 属性 | 类型 | 默认值 | 必填 | 说明 |
20
+ | --- | --- | --- | --- | --- |
21
+ | `title` | `String` | `''` | 否 | 列表标题 |
22
+ | `description` | `String` | `''` | 否 | 标题后的补充描述 |
23
+ | `items` | `String` | `'[]'` | 否 | 由列表数据序列化得到的 JSON 字符串 |
24
+
25
+ ### Item 数据结构
26
+
27
+ `items` 解析后应为数组,数组中每一项支持以下字段:
28
+
29
+ | 字段 | 类型 | 必填 | 说明 |
30
+ | --- | --- | --- | --- |
31
+ | `id` | `String` | 是 | 列表项唯一标识,用于 `ink:key` |
32
+ | `text` | `String` | 是 | 列表项文案 |
33
+ | `completed` | `Boolean` | 是 | 是否显示完成勾选标记 |
34
+
35
+ 组件不提供事件或插槽。
36
+
37
+ ## 使用示例
38
+
39
+ 页面数据中需要先使用 `JSON.stringify` 序列化列表:
40
+
41
+ ```js
42
+ export default {
43
+ data: {
44
+ title: '检查清单',
45
+ description: '共4项(已完成1项)',
46
+ items: JSON.stringify([
47
+ { id: 'item-01', text: '检查项1', completed: true },
48
+ { id: 'item-02', text: '检查项2', completed: false },
49
+ { id: 'item-03', text: '检查项3', completed: false },
50
+ { id: 'item-04', text: '检查项4', completed: false }
51
+ ])
52
+ }
53
+ };
54
+ ```
55
+
56
+ 在页面模板中传入属性:
57
+
58
+ ```html
59
+ <rvis-list
60
+ title="{{ title }}"
61
+ description="{{ description }}"
62
+ items="{{ items }}"
63
+ ></rvis-list>
64
+ ```
65
+
66
+ ## 注意事项
67
+
68
+ - `items` 必须是可被 `JSON.parse` 解析的 JSON 数组字符串,不要直接传入 JavaScript 数组。
69
+ - 建议为每个列表项提供稳定且唯一的 `id`。
70
+ - 当 `completed` 为 `true` 时,列表项左侧显示勾选标记。
71
+ - 当前运行时中,不要使用 `ink:if` / `ink:else` 反复卸载和重新显示 `rvis-list` 组件。组件内部的 `ink:for` 迭代节点可能无法在重新显示时正确恢复,导致只显示表头。动态显示或隐藏时,应保持组件挂载;隐藏状态使用 `position: absolute; left: -1000px;` 将组件移出屏幕,显示状态再恢复到原位置。不要通过 `display` 属性隐藏,否则内部文字可能无法同步隐藏并堆叠到左上角。
@@ -0,0 +1,109 @@
1
+ <script def>
2
+ {
3
+ "component": true,
4
+ "usingComponents": {
5
+ "rvis-image": "rvis-aiui-kit/image",
6
+ "rvis-paragraph": "rvis-aiui-kit/paragraph",
7
+ "rvis-table": "rvis-aiui-kit/table"
8
+ }
9
+ }
10
+ </script>
11
+
12
+ <script setup>
13
+ import { parseMarkdown } from './parser.js';
14
+
15
+ export default {
16
+ properties: {
17
+ content: { type: String, value: '' }
18
+ },
19
+ data: {},
20
+ lifetimes: {
21
+ created() {
22
+ let previousContent;
23
+ let blocks = [];
24
+ // AIUI serializes component data when resolving template scopes. A cached
25
+ // getter derives blocks from the latest host properties, including initial
26
+ // property sync after created(). No unsupported property observer needed.
27
+ Object.defineProperty(this.data, 'blocks', {
28
+ enumerable: true,
29
+ get: () => {
30
+ const content = this.properties.content;
31
+ if (content !== previousContent) {
32
+ blocks = parseMarkdown(content);
33
+ previousContent = content;
34
+ }
35
+ return blocks;
36
+ }
37
+ });
38
+ }
39
+ }
40
+ };
41
+ </script>
42
+
43
+ <page>
44
+ <view class="markdown-document">
45
+ <view class="markdown-block" ink:for="{{ blocks }}" ink:key="id">
46
+ <view class="markdown-image" ink:if="{{ item.type === 'image' }}">
47
+ <rvis-image src="{{ item.src }}" width="424px" height="200px" objectFit="contain"></rvis-image>
48
+ <rvis-paragraph text="{{ item.alt }}" width="424px"></rvis-paragraph>
49
+ </view>
50
+ <view class="markdown-text" ink:if="{{ item.type === 'paragraph' }}">
51
+ <rvis-paragraph text="{{ item.text }}" segments="{{ item.segments }}" width="424px"></rvis-paragraph>
52
+ </view>
53
+ <view class="markdown-table" ink:if="{{ item.type === 'table' }}">
54
+ <rvis-table column="{{ item.column }}" data="{{ item.data }}" compact="true" columnWidths="{{ item.columnWidths }}"></rvis-table>
55
+ </view>
56
+ <view class="markdown-list-item" ink:if="{{ item.type === 'list-item' }}">
57
+ <text class="markdown-list-marker">{{ item.marker }}</text>
58
+ <rvis-paragraph text="{{ item.text }}" segments="{{ item.segments }}" width="384px"></rvis-paragraph>
59
+ </view>
60
+ </view>
61
+ </view>
62
+ </page>
63
+
64
+ <style>
65
+ .markdown-document {
66
+ display: flex;
67
+ flex-direction: column;
68
+ align-items: flex-start;
69
+ gap: 12px;
70
+ width: 424px;
71
+ background-color: #000000;
72
+ }
73
+ .markdown-block {
74
+ position: relative;
75
+ display: flex;
76
+ flex-direction: column;
77
+ align-items: flex-start;
78
+ width: 424px;
79
+ flex-shrink: 0;
80
+ }
81
+ .markdown-text,
82
+ .markdown-table {
83
+ display: flex;
84
+ flex-direction: column;
85
+ align-items: flex-start;
86
+ width: 424px;
87
+ }
88
+ .markdown-image {
89
+ display: flex;
90
+ flex-direction: column;
91
+ align-items: flex-start;
92
+ gap: 6px;
93
+ width: 424px;
94
+ }
95
+ .markdown-list-item {
96
+ display: flex;
97
+ flex-direction: row;
98
+ align-items: flex-start;
99
+ gap: 8px;
100
+ width: 424px;
101
+ }
102
+ .markdown-list-marker {
103
+ width: 32px;
104
+ flex-shrink: 0;
105
+ color: #40ff5e;
106
+ font-size: 16px;
107
+ line-height: 24px;
108
+ }
109
+ </style>
@@ -0,0 +1,149 @@
1
+ // A deliberately small Markdown subset: images, paragraphs, **strong**,
2
+ // pipe tables and numbered list items. Unsupported syntax remains literal text.
3
+ function unescapeText(text) {
4
+ return text.replace(/\\([\\`*{}\[\]()#+.!_|>~-])/g, '$1');
5
+ }
6
+
7
+ export function parseInline(text) {
8
+ const runs = [];
9
+ let plain = '';
10
+ const flush = () => {
11
+ if (plain) runs.push({ text: plain, bold: false });
12
+ plain = '';
13
+ };
14
+ for (let index = 0; index < text.length;) {
15
+ if (text[index] === '\\' && /[\\`*{}\[\]()#+.!_|>~-]/.test(text[index + 1] || '')) {
16
+ plain += text[index + 1];
17
+ index += 2;
18
+ continue;
19
+ }
20
+ if (text.slice(index, index + 2) === '**') {
21
+ let end = index + 2;
22
+ while (end < text.length) {
23
+ if (text[end] === '\\') { end += 2; continue; }
24
+ if (text.slice(end, end + 2) === '**') break;
25
+ end += 1;
26
+ }
27
+ if (end < text.length && end > index + 2) {
28
+ flush();
29
+ runs.push({ text: unescapeText(text.slice(index + 2, end)), bold: true });
30
+ index = end + 2;
31
+ continue;
32
+ }
33
+ }
34
+ plain += text[index++];
35
+ }
36
+ flush();
37
+ return runs;
38
+ }
39
+
40
+ function textProps(text) {
41
+ const runs = parseInline(text);
42
+ return {
43
+ text: runs.map(run => run.text).join(''),
44
+ segments: runs.some(run => run.bold) ? JSON.stringify(runs) : '[]'
45
+ };
46
+ }
47
+
48
+ export function splitTableRow(line) {
49
+ const cells = [];
50
+ let cell = '';
51
+ const source = line.trim();
52
+ for (let index = 0; index < source.length; index += 1) {
53
+ const char = source[index];
54
+ if (char === '\\' && index + 1 < source.length) {
55
+ cell += char + source[++index];
56
+ } else if (char === '|') {
57
+ cells.push(cell.trim());
58
+ cell = '';
59
+ } else {
60
+ cell += char;
61
+ }
62
+ }
63
+ cells.push(cell.trim());
64
+ if (source.startsWith('|')) cells.shift();
65
+ // An escaped final pipe belongs to the cell, not the table border.
66
+ if (source.endsWith('|') && cells[cells.length - 1] === '') cells.pop();
67
+ return cells;
68
+ }
69
+
70
+ function tableColumns(lines, index) {
71
+ if (!lines[index].includes('|') || index + 1 >= lines.length) return null;
72
+ const headers = splitTableRow(lines[index]);
73
+ const separators = splitTableRow(lines[index + 1]);
74
+ if (headers.length < 2 || headers.length !== separators.length ||
75
+ !separators.every(cell => /^:?-{3,}:?$/.test(cell))) return null;
76
+ return headers.map((title, columnIndex) => ({
77
+ title: unescapeText(title),
78
+ dataIndex: 'column' + columnIndex,
79
+ align: separators[columnIndex].startsWith(':') && separators[columnIndex].endsWith(':')
80
+ ? 'center' : separators[columnIndex].endsWith(':') ? 'right' : 'left'
81
+ }));
82
+ }
83
+
84
+ function imageLine(line) {
85
+ const match = line.trim().match(/^!\[([^\]]*)\]\((\S+)\)$/);
86
+ if (!match || !/^(https?:\/\/|\/(?!\/)|\.\.?\/)/i.test(match[2])) return null;
87
+ return { alt: unescapeText(match[1]), src: match[2] };
88
+ }
89
+
90
+ function listLine(line) {
91
+ return line.match(/^ {0,3}(\d+)\.\s+(.+)$/);
92
+ }
93
+
94
+ export function parseMarkdown(content) {
95
+ if (typeof content !== 'string' || !content.trim()) return [];
96
+ const lines = content.replace(/\r\n?/g, '\n').split('\n');
97
+ const blocks = [];
98
+ for (let index = 0; index < lines.length;) {
99
+ if (!lines[index].trim()) { index += 1; continue; }
100
+ const id = 'line-' + index;
101
+ const image = imageLine(lines[index]);
102
+ if (image) {
103
+ blocks.push({ id, type: 'image', ...image });
104
+ index += 1;
105
+ continue;
106
+ }
107
+ const columns = tableColumns(lines, index);
108
+ if (columns) {
109
+ index += 2;
110
+ const rows = [];
111
+ while (index < lines.length && lines[index].includes('|')) {
112
+ const cells = splitTableRow(lines[index]);
113
+ if (cells.length !== columns.length) break;
114
+ const row = { key: 'line-' + index };
115
+ columns.forEach((column, cellIndex) => {
116
+ row[column.dataIndex] = unescapeText(cells[cellIndex]);
117
+ });
118
+ rows.push(row);
119
+ index += 1;
120
+ }
121
+ blocks.push({
122
+ id, type: 'table', column: JSON.stringify(columns), data: JSON.stringify(rows),
123
+ columnWidths: columns.length === 3 ? '120px 156px 144px' : ''
124
+ });
125
+ continue;
126
+ }
127
+ const numbered = listLine(lines[index]);
128
+ if (numbered) {
129
+ let text = numbered[2];
130
+ index += 1;
131
+ while (index < lines.length && /^ {2,}\S/.test(lines[index]) && !listLine(lines[index]) && !imageLine(lines[index]) && !tableColumns(lines, index)) {
132
+ text += ' ' + lines[index++].trim();
133
+ }
134
+ blocks.push({ id, type: 'list-item', marker: numbered[1] + '.', ...textProps(text) });
135
+ continue;
136
+ }
137
+ const paragraph = [lines[index++].trim()];
138
+ while (index < lines.length && lines[index].trim() && !imageLine(lines[index]) && !listLine(lines[index]) && !tableColumns(lines, index)) {
139
+ paragraph.push(lines[index++].trim());
140
+ }
141
+ blocks.push({ id, type: 'paragraph', ...textProps(paragraph.join(' ')) });
142
+ }
143
+ // Keep a uniform block shape and valid fallback props while AIUI resolves
144
+ // template bindings. The renderer creates only the matching type branch.
145
+ return blocks.map(block => ({
146
+ text: '', segments: '[]', src: '', alt: '', marker: '',
147
+ column: '[]', data: '[]', columnWidths: '', ...block
148
+ }));
149
+ }
@@ -0,0 +1,50 @@
1
+ # Markdown 组件
2
+
3
+ 使用项目 `image`、`paragraph`、`table` 组件渲染轻量 Markdown。没有 `streamdown`、HTML 注入、浏览器 DOM 或第三方解析库依赖。
4
+
5
+ ```json
6
+ {
7
+ "usingComponents": {
8
+ "rvis-markdown": "rvis-aiui-kit/markdown"
9
+ }
10
+ }
11
+ ```
12
+
13
+ ```xml
14
+ <rvis-markdown content="{{ markdownContent }}"></rvis-markdown>
15
+ ```
16
+
17
+ `content` 为原始 Markdown 字符串,默认 `''`。组件宽度为 `424px`,不包含滚动容器;页面在完整的 `480px × 640px` 屏幕内从 y=0 布局,外层使用 `scroll-view`。Demo 入口为「组件示例 → Markdown 组件 Demo」。
18
+
19
+ ## 流式效果实验 Demo
20
+
21
+ 当前 demo 从空字符串开始,每 `120ms` 追加 `12` 个 Unicode 码点,直到完整输出同一份 `result.md`。这是本地定时模拟,不调用模型或流式网络接口;没有缓存到完整段落或表格后才显示,所以可观察未闭合语法的真实表现。
22
+
23
+ 镜腿上下滑动仍每次滚动 `120px`,不自动跟随到底部。离开到其他页面时暂停、返回时继续,退出页面时清理定时器;重新进入 demo 从头开始。
24
+
25
+ 当前 `ink-open` 验证中,表格能逐行增长,完成后六行数据及四条编号列表可见;但后续追加过程中曾出现图片说明和开头段落消失,未闭合语法也可能暂时显示为原文并引起重新排版。本 demo 用于暴露流式问题,不表示组件已具备稳定流式支持;本次未调整解析器或共享组件的渲染实现。
26
+
27
+ ## 第一版支持范围
28
+
29
+ - 独立一行的 `![说明](URL)` 图片。保留完整 URL(含查询参数),使用共享图片组件以 `contain` 放入 `424px × 200px` 区域,并显示说明文字。支持 HTTP(S)、`/`、`./`、`../` 路径;图片是否可加载取决于地址和设备网络。当前没有图片加载状态或失败事件,不承诺失败自动替换占位图。
30
+ - 普通段落,空行分段;同段落的源文件换行合并为空格。
31
+ - 段落和列表中的 `**加粗**`,支持同段内多处加粗。共享段落组件使用原生 `p` 的行内排版能力,正确处理普通文字和粗体跨行;不经过 Markdown 原生渲染器。
32
+ - 管道表格、表头、`---` 分隔行,支持首尾管道省略、空单元格、`\|`、冒号对齐。表格单元格目前仅支持纯文本。三列时使用 `120px / 156px / 144px` 列宽,其他列数等分;不支持跨行/跨列或宽表自动改成卡片。
33
+ - `1. 内容` 形式的编号列表,保留原编号,续行与正文对齐;支持缩进的续行。
34
+ - 保留中文和 emoji 原字符;emoji 的字形和颜色取决于 Host 字体。
35
+
36
+ 不支持标题、嵌套列表、斜体、代码、公式、HTML、可点击链接等完整 Markdown 功能,未识别的行按普通文本显示。不是完整 CommonMark/GFM 实现。第一版针对完整文档展示,不保证逐 token 流式解析时的稳定布局。
37
+
38
+ ## 实现与更新
39
+
40
+ `parser.js` 输出平铺的图片、段落、表格、列表项 blocks;模板只使用单层 `ink:for`,表格单元格由共享表格组件单独平铺。父组件与子组件的循环处于独立组件作用域。
41
+
42
+ 每个 block 使用 `ink:if` 按类型创建对应分支,不预先挂载其他类型的组件。这份 `result.md` 只需要一个表格,不会为其他段落创建隐藏的空表格。旧版全部挂载后用 `position: absolute; left: -1000px` 移出屏幕的方案,会在此 demo 中触发表格压成横线的问题;已通过固定 `448px × 502px` 视口、`352px` 高滚动区域的 `ink-open` 对照验证,勿恢复该方案。
43
+
44
+ AIUI 当前组件运行时没有实现小程序式 property observers。组件在 `created` 中给本地 `data.blocks` 定义可枚举 getter,模板读取数据时按最新 `properties.content` 解析;相同内容复用缓存,无轮询计时器。当前验证范围为一次性传入完整文档,并在展示期间保持内容不变;滚动只改变页面的 `scrollTop`。getter 能重新解析更新后的字符串,但这不等于条件节点销毁、重建后的显示已得到保证;跨类型内容替换和逐 token 流式更新不在本版保证范围。不要对组件内部 `blocks` 调用 `setData`。
45
+
46
+ 更新运行时后,应复查组件作用域、getter 的数据序列化及 `p` 行内排版;Node 测试不代替原生布局验证。
47
+
48
+ ```sh
49
+ npm run test:components
50
+ ```