@talknote/create-talknote-h5 0.1.3 → 0.1.5

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 (102) hide show
  1. package/bin/create-talknote-h5.js +14 -0
  2. package/package.json +1 -1
  3. package/template/.agents/skills/add-h5-page/SKILL.md +116 -0
  4. package/template/.eslintrc.js +2 -0
  5. package/template/README.md +143 -117
  6. package/template/_gitignore +0 -3
  7. package/template/config/config.ts +49 -4
  8. package/template/config/proxy.ts +28 -31
  9. package/template/docker/nginx/error_page.conf +51 -0
  10. package/template/docker/nginx/nginx.conf +54 -0
  11. package/template/docker/nginx/webApp.conf +13 -0
  12. package/template/docker/webApp.Dockerfile +12 -0
  13. package/template/package.json +6 -2
  14. package/template/packages/xiaozhi-chat/README.md +53 -0
  15. package/template/packages/xiaozhi-chat/XiaozhiChatPage.less +50 -0
  16. package/template/packages/xiaozhi-chat/XiaozhiChatPage.tsx +172 -0
  17. package/template/packages/xiaozhi-chat/assets/images/bg.jpg +0 -0
  18. package/template/packages/xiaozhi-chat/assets/images/icon-arrow-back.png +0 -0
  19. package/template/packages/xiaozhi-chat/assets/images/icon-close.png +0 -0
  20. package/template/packages/xiaozhi-chat/assets/images/icon-voice.png +0 -0
  21. package/template/packages/xiaozhi-chat/assets/images/keyboard.png +0 -0
  22. package/template/packages/xiaozhi-chat/assets/images/send-icon.png +0 -0
  23. package/template/packages/xiaozhi-chat/assets/images/stop-icon.png +0 -0
  24. package/template/packages/xiaozhi-chat/components/AssistantRichContent/index.less +3 -0
  25. package/template/packages/xiaozhi-chat/components/AssistantRichContent/index.tsx +44 -0
  26. package/template/packages/xiaozhi-chat/components/ChatHeader/index.less +84 -0
  27. package/template/packages/xiaozhi-chat/components/ChatHeader/index.tsx +44 -0
  28. package/template/packages/xiaozhi-chat/components/ChatInput/HoldToTalk.less +59 -0
  29. package/template/packages/xiaozhi-chat/components/ChatInput/HoldToTalk.tsx +167 -0
  30. package/template/packages/xiaozhi-chat/components/ChatInput/index.less +189 -0
  31. package/template/packages/xiaozhi-chat/components/ChatInput/index.tsx +203 -0
  32. package/template/packages/xiaozhi-chat/components/DeepThinkingWrapper/index.less +80 -0
  33. package/template/packages/xiaozhi-chat/components/DeepThinkingWrapper/index.tsx +54 -0
  34. package/template/packages/xiaozhi-chat/components/MarkdownContent/index.less +209 -0
  35. package/template/packages/xiaozhi-chat/components/MarkdownContent/index.tsx +41 -0
  36. package/template/packages/xiaozhi-chat/components/MessageList/index.less +74 -0
  37. package/template/packages/xiaozhi-chat/components/MessageList/index.tsx +195 -0
  38. package/template/packages/xiaozhi-chat/components/ThinkingCompleteEntry/index.less +56 -0
  39. package/template/packages/xiaozhi-chat/components/ThinkingCompleteEntry/index.tsx +65 -0
  40. package/template/packages/xiaozhi-chat/components/ToolCallList/index.less +41 -0
  41. package/template/packages/xiaozhi-chat/components/ToolCallList/index.tsx +51 -0
  42. package/template/packages/xiaozhi-chat/components/TypingIndicator/index.less +35 -0
  43. package/template/packages/xiaozhi-chat/components/TypingIndicator/index.tsx +12 -0
  44. package/template/packages/xiaozhi-chat/components/Welcome/index.less +105 -0
  45. package/template/packages/xiaozhi-chat/components/Welcome/index.tsx +84 -0
  46. package/template/packages/xiaozhi-chat/hooks/useChatSession.ts +235 -0
  47. package/template/packages/xiaozhi-chat/hooks/useRealtimeTranscription.ts +226 -0
  48. package/template/packages/xiaozhi-chat/hooks/useTypewriter.ts +215 -0
  49. package/template/packages/xiaozhi-chat/hooks/useViewportHeight.ts +43 -0
  50. package/template/packages/xiaozhi-chat/hooks/useVoiceRecording.ts +128 -0
  51. package/template/packages/xiaozhi-chat/index.ts +11 -0
  52. package/template/packages/xiaozhi-chat/package.json +34 -0
  53. package/template/packages/xiaozhi-chat/services/chatApi.ts +585 -0
  54. package/template/packages/xiaozhi-chat/types.ts +80 -0
  55. package/template/packages/xiaozhi-chat/utils/bridge.ts +131 -0
  56. package/template/packages/xiaozhi-chat/utils/deepThinking.ts +200 -0
  57. package/template/packages/xiaozhi-chat/utils/parseAssistantContent.ts +233 -0
  58. package/template/packages/xiaozhi-chat/utils/typewriter.ts +265 -0
  59. package/template/scripts/dev.js +6 -7
  60. package/template/src/app.tsx +1 -73
  61. package/template/src/assets/error-network.png +0 -0
  62. package/template/src/assets/error.png +0 -0
  63. package/template/src/assets/success.png +0 -0
  64. package/template/src/assets/welcome.png +0 -0
  65. package/template/src/components/Header/index.less +70 -0
  66. package/template/src/components/Header/index.tsx +46 -0
  67. package/template/src/components/TabBar/index.less +13 -0
  68. package/template/src/components/TabBar/index.tsx +58 -0
  69. package/template/src/global.css +27 -0
  70. package/template/src/layouts/BlankLayout/index.less +5 -0
  71. package/template/src/layouts/BlankLayout/index.tsx +14 -0
  72. package/template/src/layouts/HeaderLayout/index.less +25 -0
  73. package/template/src/layouts/HeaderLayout/index.tsx +42 -0
  74. package/template/src/layouts/LayoutOutlet.tsx +7 -0
  75. package/template/src/layouts/SSOLoginLayout/index.less +28 -0
  76. package/template/src/layouts/SSOLoginLayout/index.tsx +92 -0
  77. package/template/src/layouts/TabBarLayout/index.less +21 -0
  78. package/template/src/layouts/TabBarLayout/index.tsx +29 -0
  79. package/template/src/layouts/index.tsx +30 -0
  80. package/template/src/layouts/usePageHeaderProps.ts +9 -0
  81. package/template/src/pages/index/index.css +5 -5
  82. package/template/src/pages/index/index.tsx +15 -2
  83. package/template/src/pages/list/index.tsx +5 -5
  84. package/template/src/pages/test/index.tsx +12 -0
  85. package/template/src/pages/xiaozhi-chat/index.less +4 -0
  86. package/template/src/pages/xiaozhi-chat/index.tsx +26 -0
  87. package/template/src/routes.ts +49 -0
  88. package/template/src/services/api.ts +7 -2
  89. package/template/src/theme/variables.css +5 -0
  90. package/template/src/utils/ensureSsoLogin.ts +28 -0
  91. package/template/src/utils/goBack.ts +10 -0
  92. package/template/src/utils/request.ts +106 -0
  93. package/template/tsconfig.json +4 -1
  94. package/template/typings.d.ts +15 -0
  95. package/template/yarn.lock +2168 -61
  96. package/template/src/assets/demoIcon/home.png +0 -0
  97. package/template/src/assets/demoIcon/home1.png +0 -0
  98. package/template/src/assets/demoIcon/list.png +0 -0
  99. package/template/src/assets/demoIcon/list1.png +0 -0
  100. package/template/src/assets/demoIcon/setting.png +0 -0
  101. package/template/src/assets/demoIcon/setting1.png +0 -0
  102. package/template/src/assets/yay.jpg +0 -0
@@ -0,0 +1,265 @@
1
+ const DEFAULT_TYPING_SPEED = 32;
2
+ const DEFAULT_CATCH_UP_SPEED = 6;
3
+ const DEFAULT_BATCH_SIZE = 2;
4
+ const DEFAULT_CATCH_UP_BATCH_SIZE = 10;
5
+ const DEFAULT_MAX_LAG = 80;
6
+ const DEFAULT_MAX_ADVANCE_PER_TICK = 32;
7
+ const DEFAULT_SMOOTH_LAG_THRESHOLD = 40;
8
+ const DEFAULT_SMOOTH_CHARS_PER_SECOND = 22;
9
+ const DEFAULT_SMOOTH_MAX_ADVANCE_PER_TICK = 1;
10
+ const DEFAULT_FINISH_TARGET_MS = 2500;
11
+ const DEFAULT_FINISH_MIN_MS = 2000;
12
+ const DEFAULT_FINISH_MAX_MS = 3000;
13
+ const FINISH_MAX_TICK_MS = 120;
14
+ const LAG_RAMP_MULTIPLIER = 4;
15
+
16
+ export const TYPEWRITER_DEFAULTS = {
17
+ typingSpeed: DEFAULT_TYPING_SPEED,
18
+ catchUpSpeed: DEFAULT_CATCH_UP_SPEED,
19
+ batchSize: DEFAULT_BATCH_SIZE,
20
+ catchUpBatchSize: DEFAULT_CATCH_UP_BATCH_SIZE,
21
+ maxLag: DEFAULT_MAX_LAG,
22
+ maxAdvancePerTick: DEFAULT_MAX_ADVANCE_PER_TICK,
23
+ smoothLagThreshold: DEFAULT_SMOOTH_LAG_THRESHOLD,
24
+ smoothCharsPerSecond: DEFAULT_SMOOTH_CHARS_PER_SECOND,
25
+ smoothMaxAdvancePerTick: DEFAULT_SMOOTH_MAX_ADVANCE_PER_TICK,
26
+ finishTargetMs: DEFAULT_FINISH_TARGET_MS,
27
+ finishMinMs: DEFAULT_FINISH_MIN_MS,
28
+ finishMaxMs: DEFAULT_FINISH_MAX_MS,
29
+ };
30
+
31
+ export type TypewriterPacing = {
32
+ advanceSize: number;
33
+ delayMs: number;
34
+ };
35
+
36
+ export const computeStreamFinishPacing = (
37
+ lag: number,
38
+ typingSpeed: number,
39
+ catchUpSpeed: number,
40
+ maxAdvancePerTick: number,
41
+ remainingMs: number,
42
+ ): TypewriterPacing => {
43
+ if (lag <= 0) {
44
+ return { advanceSize: 0, delayMs: typingSpeed };
45
+ }
46
+
47
+ if (remainingMs <= catchUpSpeed) {
48
+ return {
49
+ advanceSize: Math.min(lag, maxAdvancePerTick),
50
+ delayMs: catchUpSpeed,
51
+ };
52
+ }
53
+
54
+ const advanceSize = Math.max(
55
+ 1,
56
+ Math.min(maxAdvancePerTick, Math.round((lag * typingSpeed) / remainingMs)),
57
+ );
58
+ const tickCount = Math.ceil(lag / advanceSize);
59
+ const delayMs = Math.max(
60
+ catchUpSpeed,
61
+ Math.min(FINISH_MAX_TICK_MS, Math.round(remainingMs / tickCount)),
62
+ );
63
+
64
+ return {
65
+ advanceSize: Math.min(lag, advanceSize),
66
+ delayMs,
67
+ };
68
+ };
69
+
70
+ export const computeTypewriterPacing = (input: {
71
+ lag: number;
72
+ batchSize: number;
73
+ catchUpBatchSize: number;
74
+ maxLag: number;
75
+ maxAdvancePerTick: number;
76
+ typingSpeed: number;
77
+ catchUpSpeed: number;
78
+ smoothLagThreshold?: number;
79
+ smoothCharsPerSecond?: number;
80
+ smoothMaxAdvancePerTick?: number;
81
+ isStreamFinished?: boolean;
82
+ finishRemainingMs?: number;
83
+ }): TypewriterPacing => {
84
+ const {
85
+ lag,
86
+ batchSize,
87
+ catchUpBatchSize,
88
+ maxLag,
89
+ maxAdvancePerTick,
90
+ typingSpeed,
91
+ catchUpSpeed,
92
+ smoothLagThreshold = DEFAULT_SMOOTH_LAG_THRESHOLD,
93
+ smoothCharsPerSecond = DEFAULT_SMOOTH_CHARS_PER_SECOND,
94
+ smoothMaxAdvancePerTick = DEFAULT_SMOOTH_MAX_ADVANCE_PER_TICK,
95
+ isStreamFinished = false,
96
+ finishRemainingMs = DEFAULT_FINISH_TARGET_MS,
97
+ } = input;
98
+
99
+ if (lag <= 0) {
100
+ return { advanceSize: 0, delayMs: typingSpeed };
101
+ }
102
+
103
+ if (isStreamFinished) {
104
+ return computeStreamFinishPacing(
105
+ lag,
106
+ typingSpeed,
107
+ catchUpSpeed,
108
+ maxAdvancePerTick,
109
+ Math.max(catchUpSpeed, finishRemainingMs),
110
+ );
111
+ }
112
+
113
+ if (lag <= smoothLagThreshold) {
114
+ const tickMs = typingSpeed;
115
+ const charsPerTick = (smoothCharsPerSecond * tickMs) / 1000;
116
+ const cappedPerTick = Math.max(
117
+ 1,
118
+ Math.min(smoothMaxAdvancePerTick, Math.round(charsPerTick) || 1),
119
+ );
120
+ return {
121
+ advanceSize: Math.min(lag, cappedPerTick),
122
+ delayMs: tickMs,
123
+ };
124
+ }
125
+
126
+ if (lag <= maxLag) {
127
+ const span = Math.max(1, maxLag - smoothLagThreshold);
128
+ const t = (lag - smoothLagThreshold) / span;
129
+ const advanceSize = Math.min(
130
+ lag,
131
+ Math.max(1, Math.round(1 + t * (batchSize - 1))),
132
+ );
133
+ const delayMs = Math.round(typingSpeed * (1 - t * 0.25));
134
+ return { advanceSize, delayMs };
135
+ }
136
+
137
+ const rampStart = maxLag;
138
+ const rampEnd = Math.max(rampStart + 1, maxLag * LAG_RAMP_MULTIPLIER);
139
+ const boostedT = Math.min(1, (lag - rampStart) / (rampEnd - rampStart));
140
+
141
+ const advanceSize = Math.min(
142
+ lag,
143
+ maxAdvancePerTick,
144
+ Math.max(
145
+ catchUpBatchSize,
146
+ Math.round(catchUpBatchSize + boostedT * (maxAdvancePerTick - catchUpBatchSize)),
147
+ ),
148
+ );
149
+
150
+ const delayMs = Math.max(
151
+ catchUpSpeed,
152
+ Math.round(typingSpeed - boostedT * (typingSpeed - catchUpSpeed)),
153
+ );
154
+
155
+ return { advanceSize, delayMs };
156
+ };
157
+
158
+ const applyAtomicTokens = (
159
+ displayedLength: number,
160
+ received: string,
161
+ nextLength: number,
162
+ atomicTokens: string[],
163
+ ): number => {
164
+ if (!atomicTokens.length) {
165
+ return nextLength;
166
+ }
167
+
168
+ const receivedLength = received.length;
169
+ let resolved = nextLength;
170
+
171
+ const sortedTokens = [...atomicTokens]
172
+ .filter(Boolean)
173
+ .sort((a, b) => b.length - a.length);
174
+
175
+ sortedTokens.forEach((token) => {
176
+ let searchFrom = 0;
177
+ while (searchFrom < receivedLength) {
178
+ const tokenStart = received.indexOf(token, searchFrom);
179
+ if (tokenStart === -1) {
180
+ break;
181
+ }
182
+
183
+ const tokenEnd = tokenStart + token.length;
184
+
185
+ if (tokenEnd <= displayedLength) {
186
+ searchFrom = tokenEnd;
187
+ } else {
188
+ const cutsInsideToken = resolved > tokenStart && resolved < tokenEnd;
189
+ if (cutsInsideToken) {
190
+ resolved = tokenEnd <= receivedLength ? tokenEnd : Math.min(resolved, tokenStart);
191
+ }
192
+ searchFrom = tokenStart + 1;
193
+ }
194
+ }
195
+ });
196
+
197
+ return resolved;
198
+ };
199
+
200
+ const applyAtomicClosedTags = (
201
+ displayedLength: number,
202
+ received: string,
203
+ nextLength: number,
204
+ atomicClosedTags: string[],
205
+ ): number => {
206
+ if (!atomicClosedTags.length) {
207
+ return nextLength;
208
+ }
209
+
210
+ const receivedLength = received.length;
211
+ let resolved = nextLength;
212
+
213
+ [...new Set(atomicClosedTags.filter(Boolean))].forEach((tag) => {
214
+ const open = `<${tag}>`;
215
+ const close = `</${tag}>`;
216
+ let searchFrom = 0;
217
+
218
+ while (searchFrom < receivedLength) {
219
+ const openStart = received.indexOf(open, searchFrom);
220
+ if (openStart === -1) {
221
+ break;
222
+ }
223
+
224
+ const contentStart = openStart + open.length;
225
+ const closeStart = received.indexOf(close, contentStart);
226
+
227
+ if (closeStart === -1) {
228
+ resolved = displayedLength <= openStart
229
+ ? Math.min(resolved, openStart)
230
+ : displayedLength;
231
+ break;
232
+ }
233
+
234
+ const blockEnd = closeStart + close.length;
235
+ if (blockEnd <= displayedLength) {
236
+ searchFrom = blockEnd;
237
+ } else {
238
+ if (resolved > openStart && resolved < blockEnd) {
239
+ resolved = blockEnd;
240
+ }
241
+ searchFrom = blockEnd;
242
+ }
243
+ }
244
+ });
245
+
246
+ return resolved;
247
+ };
248
+
249
+ export const resolveTypewriterDisplayLength = (
250
+ displayedLength: number,
251
+ received: string,
252
+ batchSize: number,
253
+ atomicTokens: string[] = [],
254
+ atomicClosedTags: string[] = [],
255
+ ): number => {
256
+ const receivedLength = received.length;
257
+ if (displayedLength >= receivedLength) {
258
+ return receivedLength;
259
+ }
260
+
261
+ let nextLength = Math.min(displayedLength + Math.max(1, batchSize), receivedLength);
262
+ nextLength = applyAtomicTokens(displayedLength, received, nextLength, atomicTokens);
263
+ nextLength = applyAtomicClosedTags(displayedLength, received, nextLength, atomicClosedTags);
264
+ return Math.max(nextLength, displayedLength);
265
+ };
@@ -3,13 +3,14 @@
3
3
  * 开发启动脚本
4
4
  *
5
5
  * 用法:
6
- * yarn dev # 使用 proxy.ts 中的默认代理地址
7
- * yarn dev http://1.2.3.4:10000 # 把 process.env.BASE_URL 注入为该地址,代理过去
6
+ * yarn dev # 走 .env.local / 环境变量 / proxy.ts 默认代理地址
7
+ * yarn dev http://1.2.3.4:10000 # 把 process.env.DEV_SERVER 注入为该地址,代理过去
8
8
  * yarn dev http://1.2.3.4:10000 --port 8081 # 额外参数透传给 alita dev
9
9
  *
10
10
  * 说明:
11
11
  * 第一个以 http:// 或 https:// 开头的参数会被当作代理地址,
12
- * 注入 process.env.BASE_URL(proxy.ts 会读取它)。
12
+ * 注入 process.env.DEV_SERVER(proxy.ts 会读取它)。
13
+ * 未传地址时不设置,由 alita 自行加载 .env.local。
13
14
  * 其余参数原样透传给 alita dev。
14
15
  */
15
16
  const { spawn } = require('child_process');
@@ -21,10 +22,8 @@ const proxyIndex = args.findIndex((a) => /^https?:\/\//i.test(a));
21
22
  const proxyUrl = proxyIndex >= 0 ? args.splice(proxyIndex, 1)[0] : undefined;
22
23
 
23
24
  if (proxyUrl) {
24
- process.env.BASE_URL = proxyUrl;
25
- console.log(`\n[dev] 代理地址已注入 BASE_URL = ${proxyUrl}\n`);
26
- } else {
27
- console.log('\n[dev] 未指定代理地址,使用 proxy.ts 中的默认值\n');
25
+ process.env.DEV_SERVER = proxyUrl;
26
+ console.log(`\n[dev] 代理地址已注入 DEV_SERVER = ${proxyUrl}\n`);
28
27
  }
29
28
 
30
29
  // 启动 alita dev,剩余参数透传
@@ -1,15 +1,4 @@
1
- import HomeGary from '@/assets/demoIcon/home.png';
2
- import HomeBlue from '@/assets/demoIcon/home1.png';
3
- import ListGary from '@/assets/demoIcon/list.png';
4
- import ListBlue from '@/assets/demoIcon/list1.png';
5
- import type {
6
- NavBarListItem,
7
- NavBarProps,
8
- ResponseError,
9
- TabBarListItem,
10
- TabBarProps,
11
- TitleListItem,
12
- } from 'alita';
1
+ import type { ResponseError } from 'alita';
13
2
  import TalkNoteH5Bridge, { type RequestBaseInfo } from '@talknote/h5-bridge';
14
3
 
15
4
  // 缓存原生请求信息,避免每次请求都调用 bridge
@@ -48,64 +37,3 @@ export const request = {
48
37
  console.log(error);
49
38
  },
50
39
  };
51
-
52
- const titleList: TitleListItem[] = [
53
- {
54
- pagePath: '/',
55
- title: '首页',
56
- },
57
- {
58
- pagePath: '/list',
59
- title: '列表',
60
- },
61
- ];
62
- const navList: NavBarListItem[] = [];
63
- const navBar: NavBarProps = {
64
- navList,
65
- fixed: false,
66
- //默认顶部导航栏不隐藏
67
- hideNavBar: false,
68
- onLeftClick: () => {
69
- // 判断是否能够返回,如果不能返回调用 @talknote/h5-bridge goBack
70
- if (window.history.length > 1) {
71
- window.history.back(); // 有历史记录,浏览器返回
72
- } else {
73
- TalkNoteH5Bridge.goBack(); // 没有历史记录,交给原生端处理
74
- }
75
- },
76
- };
77
- const tabList: TabBarListItem[] = [
78
- {
79
- pagePath: '/',
80
- text: '首页',
81
- iconPath: HomeGary,
82
- selectedIconPath: HomeBlue,
83
- title: '首页',
84
- iconSize: '',
85
- badge: '',
86
- },
87
- {
88
- pagePath: '/list',
89
- text: '列表',
90
- iconPath: ListGary,
91
- selectedIconPath: ListBlue,
92
- title: '列表',
93
- iconSize: '',
94
- badge: '',
95
- },
96
- ];
97
-
98
- const tabBar: TabBarProps = {
99
- color: `#999999`,
100
- selectedColor: '#00A0FF',
101
- borderStyle: 'white',
102
- position: 'bottom',
103
- list: tabList,
104
- };
105
-
106
- export const mobileLayout = {
107
- documentTitle: '默认标题',
108
- navBar,
109
- tabBar,
110
- titleList,
111
- };
Binary file
Binary file
Binary file
@@ -0,0 +1,70 @@
1
+ /* Px 避免 alita px→rem 把顶栏压小;vibe-element=header 由原生注入吸顶与安全区 */
2
+ .header {
3
+ box-sizing: border-box;
4
+ position: relative;
5
+ z-index: 100;
6
+ display: flex;
7
+ flex-direction: row;
8
+ align-items: center;
9
+ justify-content: space-between;
10
+ width: 100%;
11
+ min-height: 44Px;
12
+ padding: 0 12Px;
13
+ background: var(--color-background-primary);
14
+ border-bottom: 1px solid var(--color-border);
15
+ flex-shrink: 0;
16
+ }
17
+
18
+ .backBtn {
19
+ position: relative;
20
+ z-index: 2;
21
+ width: 36Px;
22
+ height: 36Px;
23
+ border: none;
24
+ background: transparent;
25
+ display: flex;
26
+ align-items: center;
27
+ justify-content: center;
28
+ padding: 0;
29
+ flex-shrink: 0;
30
+ color: var(--color-text-primary);
31
+ }
32
+
33
+ .side {
34
+ width: 36Px;
35
+ height: 36Px;
36
+ flex-shrink: 0;
37
+ }
38
+
39
+ .title {
40
+ position: absolute;
41
+ left: 50%;
42
+ transform: translateX(-50%);
43
+ display: flex;
44
+ align-items: center;
45
+ justify-content: center;
46
+ height: 44Px;
47
+ max-width: 50%;
48
+ overflow: hidden;
49
+ text-overflow: ellipsis;
50
+ white-space: nowrap;
51
+ pointer-events: none;
52
+ font-size: 17Px;
53
+ font-weight: var(--font-weight-semibold);
54
+ line-height: 22Px;
55
+ color: var(--color-text-primary);
56
+ font-family: var(--font-family);
57
+ }
58
+
59
+ .right {
60
+ position: relative;
61
+ z-index: 2;
62
+ min-width: 36Px;
63
+ height: 36Px;
64
+ display: flex;
65
+ align-items: center;
66
+ justify-content: flex-end;
67
+ flex-shrink: 0;
68
+ font-size: var(--font-size-sm);
69
+ color: var(--color-text-primary);
70
+ }
@@ -0,0 +1,46 @@
1
+ import React, { useEffect } from 'react';
2
+ import { ChevronLeft } from 'lucide-react';
3
+ import { goBack as defaultGoBack } from '@/utils/goBack';
4
+ import styles from './index.less';
5
+
6
+ export type HeaderProps = {
7
+ title?: string;
8
+ showBack?: boolean;
9
+ onBack?: () => void;
10
+ extra?: React.ReactNode;
11
+ children?: React.ReactNode;
12
+ };
13
+
14
+ /**
15
+ * 页面顶栏。由 HeaderLayout 固定在滚动区域之外。
16
+ * vibe-element="header" 供原生容器注入吸顶与安全区。
17
+ */
18
+ const Header: React.FC<HeaderProps> = ({
19
+ title,
20
+ showBack = true,
21
+ onBack = defaultGoBack,
22
+ extra,
23
+ children,
24
+ }) => {
25
+ useEffect(() => {
26
+ if (title) {
27
+ document.title = title;
28
+ }
29
+ }, [title]);
30
+
31
+ return (
32
+ <header className={styles.header} vibe-element="header">
33
+ {showBack ? (
34
+ <button type="button" className={styles.backBtn} onClick={onBack} aria-label="返回">
35
+ <ChevronLeft size={24} strokeWidth={2} />
36
+ </button>
37
+ ) : (
38
+ <span className={styles.side} />
39
+ )}
40
+ <div className={styles.title}>{children ?? title}</div>
41
+ <div className={styles.right}>{extra}</div>
42
+ </header>
43
+ );
44
+ };
45
+
46
+ export default Header;
@@ -0,0 +1,13 @@
1
+ .wrap {
2
+ flex-shrink: 0;
3
+ background: var(--color-background-primary);
4
+ border-top: 1px solid var(--color-border);
5
+
6
+ :global {
7
+ .adm-tab-bar {
8
+ --adm-color-primary: var(--color-primary);
9
+ --adm-color-text-secondary: var(--color-text-tertiary);
10
+ background: var(--color-background-primary);
11
+ }
12
+ }
13
+ }
@@ -0,0 +1,58 @@
1
+ import React from 'react';
2
+ import { TabBar as TabBarComponent } from 'antd-mobile';
3
+ import { useLocation, useNavigate } from 'alita';
4
+ import { Home, List } from 'lucide-react';
5
+ import type { LucideIcon } from 'lucide-react';
6
+ import styles from './index.less';
7
+
8
+ export type TabItem = {
9
+ path: string;
10
+ text: string;
11
+ icon: LucideIcon;
12
+ };
13
+
14
+ export const navItems: TabItem[] = [
15
+ { path: '/', text: '首页', icon: Home },
16
+ { path: '/list', text: '列表', icon: List },
17
+ ];
18
+
19
+ /**
20
+ * 底部导航。由 TabBarLayout 引入;不在 TabItem 中的页面不会高亮。
21
+ */
22
+ const TabBar: React.FC = () => {
23
+ const { pathname } = useLocation();
24
+ const navigate = useNavigate();
25
+
26
+ return (
27
+ <div className={styles.wrap}>
28
+ <TabBarComponent
29
+ safeArea
30
+ activeKey={pathname}
31
+ onChange={(key) => {
32
+ if (key !== pathname) {
33
+ navigate(key, { replace: true });
34
+ }
35
+ }}
36
+ >
37
+ {navItems.map((item) => {
38
+ const Icon = item.icon;
39
+ return (
40
+ <TabBarComponent.Item
41
+ key={item.path}
42
+ title={item.text}
43
+ icon={(active) => (
44
+ <Icon
45
+ size={22}
46
+ strokeWidth={2}
47
+ color={active ? 'var(--color-primary)' : 'var(--color-text-tertiary)'}
48
+ />
49
+ )}
50
+ />
51
+ );
52
+ })}
53
+ </TabBarComponent>
54
+ </div>
55
+ );
56
+ };
57
+
58
+ export default TabBar;
@@ -1,5 +1,20 @@
1
1
  @import './theme/index.css';
2
2
 
3
+ * {
4
+ box-sizing: border-box;
5
+ -webkit-tap-highlight-color: transparent;
6
+ }
7
+
8
+ html {
9
+ width: 100%;
10
+ height: 100%;
11
+ text-size-adjust: 100%;
12
+ --alita-safe-area-top: env(safe-area-inset-top);
13
+ --alita-safe-area-bottom: env(safe-area-inset-bottom);
14
+ --alita-safe-area-left: env(safe-area-inset-left);
15
+ --alita-safe-area-right: env(safe-area-inset-right);
16
+ }
17
+
3
18
  html,
4
19
  body,
5
20
  #root {
@@ -8,7 +23,19 @@ body,
8
23
 
9
24
  body {
10
25
  margin: 0;
26
+ position: fixed;
27
+ inset: 0;
28
+ overflow: hidden;
11
29
  font-family: var(--font-family);
12
30
  background-color: var(--color-background-bg);
13
31
  color: var(--color-text-primary);
32
+ -webkit-font-smoothing: antialiased;
33
+ touch-action: manipulation;
34
+ overscroll-behavior-y: none;
35
+ }
36
+
37
+ /* keepalive 内层也禁止链式 overscroll,避免橡皮筋传到顶栏 */
38
+ .runtime-keep-alive-layout,
39
+ .runtime-keep-alive-layout-no {
40
+ overscroll-behavior-y: none;
14
41
  }
@@ -0,0 +1,5 @@
1
+ .layout {
2
+ height: 100%;
3
+ overflow: hidden;
4
+ background: var(--color-background-bg);
5
+ }
@@ -0,0 +1,14 @@
1
+ import React from 'react';
2
+ import { LayoutOutlet } from '../LayoutOutlet';
3
+ import styles from './index.less';
4
+
5
+ /**
6
+ * 空白布局:无底部导航。页面自行决定是否使用 Header。
7
+ */
8
+ const BlankLayout: React.FC<{ children?: React.ReactNode }> = ({ children }) => (
9
+ <div className={styles.layout}>
10
+ <LayoutOutlet>{children}</LayoutOutlet>
11
+ </div>
12
+ );
13
+
14
+ export default BlankLayout;
@@ -0,0 +1,25 @@
1
+ .layout {
2
+ display: flex;
3
+ flex-direction: column;
4
+ height: 100%;
5
+ min-height: 0;
6
+ flex: 1;
7
+ overflow: hidden;
8
+ background: var(--color-background-bg);
9
+ }
10
+
11
+ .headerWrap {
12
+ flex-shrink: 0;
13
+ /* 独立合成层,避免 iOS 橡皮筋把顶栏一起拉伸 */
14
+ transform: translateZ(0);
15
+ -webkit-transform: translateZ(0);
16
+ }
17
+
18
+ .content {
19
+ position: relative;
20
+ flex: 1;
21
+ min-height: 0;
22
+ overflow: auto;
23
+ overscroll-behavior: none;
24
+ -webkit-overflow-scrolling: touch;
25
+ }