weifuwu 0.63.0 → 0.64.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 +124 -2956
- package/dist/ai/client.d.ts +1 -1
- package/dist/ai/sse.d.ts +1 -1
- package/dist/ai/types.d.ts +2 -2
- package/dist/client/ai.d.ts +1 -1
- package/dist/client/index.js +3 -3
- package/dist/client/types.d.ts +108 -0
- package/dist/client/use-chat.d.ts +1 -1
- package/dist/components/AiChat/AiChat.d.ts +2 -0
- package/dist/components/ContextMenu/ContextMenu.d.ts +1 -1
- package/dist/components/Dropdown/Dropdown.d.ts +4 -1
- package/dist/components/HoverCard/HoverCard.d.ts +7 -1
- package/dist/components/Popover/Popover.d.ts +3 -0
- package/dist/components/Tooltip/Tooltip.d.ts +3 -0
- package/dist/components/index.js +13 -13
- package/dist/components/style.css +164 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -1
- package/dist/layout/weifuwu-layout.css +22 -0
- package/docs/components.md +314 -0
- package/docs/data.md +240 -0
- package/docs/environment.md +27 -0
- package/docs/examples.md +127 -0
- package/docs/frontend-middleware.md +435 -0
- package/docs/frontend.md +697 -0
- package/docs/layout.md +203 -0
- package/docs/mobile.md +109 -0
- package/docs/realtime.md +291 -0
- package/docs/saas.md +246 -0
- package/docs/server.md +356 -0
- package/docs/styling.md +130 -0
- package/package.json +4 -3
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
# 前端中间件与工具(weifuwu/client)
|
|
2
|
+
|
|
3
|
+
> 本页为 weifuwu 官方文档拆分页 · [返回 README](../README.md)
|
|
4
|
+
|
|
5
|
+
## router + RouteView — 前端路由
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { createApp, router, RouteView } from 'weifuwu/client'
|
|
9
|
+
import type { RouteDef, WfuiContext } from 'weifuwu/client'
|
|
10
|
+
|
|
11
|
+
const routes: RouteDef[] = [
|
|
12
|
+
{ path: '/', component: Home },
|
|
13
|
+
{ path: '/users', component: UserList },
|
|
14
|
+
{ path: '/users/:id', component: UserDetail },
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
createApp()
|
|
18
|
+
.use(router({
|
|
19
|
+
routes,
|
|
20
|
+
mode: 'history', // 或 'hash'
|
|
21
|
+
notFound: NotFoundPage,
|
|
22
|
+
}))
|
|
23
|
+
.mount('#root', () => () => <RouteView />) // 根组件也要两阶段:外层返回 render 函数
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### 嵌套布局
|
|
27
|
+
|
|
28
|
+
```tsx
|
|
29
|
+
const routes = [
|
|
30
|
+
{
|
|
31
|
+
path: '/dashboard',
|
|
32
|
+
layout: DashboardLayout, // 持久布局(包含 RouteView)
|
|
33
|
+
children: [
|
|
34
|
+
{ path: '/overview', component: Overview },
|
|
35
|
+
{ path: '/settings', component: Settings },
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
function DashboardLayout(_props: {}, ctx: WfuiContext) {
|
|
41
|
+
return (props) => (
|
|
42
|
+
<div style="display:flex">
|
|
43
|
+
<aside>导航菜单</aside>
|
|
44
|
+
<main><RouteView /></main> {/* 渲染子路由 */}
|
|
45
|
+
</div>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### 编程式导航
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
// 在任意组件中
|
|
54
|
+
ctx.app?.navigate('/users/123?tab=profile')
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
| ctx 注入 | 类型 | 说明 |
|
|
58
|
+
|----------|------|------|
|
|
59
|
+
| `ctx.route.path` | `string` | 当前路由路径 |
|
|
60
|
+
| `ctx.route.params` | `Record<string, string>` | URL 参数 |
|
|
61
|
+
| `ctx.route.query` | `Record<string, string>` | 查询参数 |
|
|
62
|
+
| `ctx.app.navigate(path)` | `(string) => void` | 编程式导航 |
|
|
63
|
+
|
|
64
|
+
| RouterOptions | 类型 | 默认值 | 说明 |
|
|
65
|
+
|---------------|------|--------|------|
|
|
66
|
+
| `routes` | `RouteDef[]` | — | 路由定义 |
|
|
67
|
+
| `mode` | `'history' \| 'hash'` | `'history'` | 路由模式 |
|
|
68
|
+
| `notFound` | `Component` | — | 404 页面 |
|
|
69
|
+
|
|
70
|
+
| RouteDef | 类型 | 说明 |
|
|
71
|
+
|----------|------|------|
|
|
72
|
+
| `path` | `string` | 路径(支持 `:param`) |
|
|
73
|
+
| `component` | `Component` | 页面组件 |
|
|
74
|
+
| `layout` | `Component` | 布局组件(内含 `<RouteView />`) |
|
|
75
|
+
| `children` | `RouteDef[]` | 子路由 |
|
|
76
|
+
| `auth` | `boolean` | 是否需要认证(配合 auth 中间件) |
|
|
77
|
+
| `title` | `string` | 页面标题(自动设置 `document.title`) |
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## api — HTTP 客户端中间件
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
import { createApp, api } from 'weifuwu/client'
|
|
85
|
+
|
|
86
|
+
createApp()
|
|
87
|
+
.use(api({ baseURL: '/api' }))
|
|
88
|
+
.mount('#root', App)
|
|
89
|
+
|
|
90
|
+
// 在组件中使用
|
|
91
|
+
async function loadUsers(ctx: WfuiContext) {
|
|
92
|
+
const users = await ctx.api?.get<User[]>('/users')
|
|
93
|
+
const user = await ctx.api?.get<User>('/users/1')
|
|
94
|
+
const created = await ctx.api?.post<User>('/users', { name: 'Alice' })
|
|
95
|
+
await ctx.api?.put('/users/1', { name: 'Bob' })
|
|
96
|
+
await ctx.api?.patch('/users/1', { name: 'Bob' })
|
|
97
|
+
await ctx.api?.delete('/users/1')
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
| 选项 | 类型 | 默认值 | 说明 |
|
|
102
|
+
|------|------|--------|------|
|
|
103
|
+
| `baseURL` | `string` | `''` | API 基础路径 |
|
|
104
|
+
| `headers` | `Record<string, string>` | `{ 'Content-Type': 'application/json' }` | 默认请求头 |
|
|
105
|
+
| `onRequest` | `(req) => { url, init }` | — | 请求拦截器 |
|
|
106
|
+
| `onResponse` | `(res) => Promise<T>` | — | 响应拦截器 |
|
|
107
|
+
| `timeout` | `number` | `0`(无超时) | 请求超时(ms)|
|
|
108
|
+
|
|
109
|
+
| ctx.api 方法 | 签名 | 说明 |
|
|
110
|
+
|-------------|------|------|
|
|
111
|
+
| `api.get(url, opts?)` | `<T>(string, ApiRequestOptions?) => Promise<T>` | GET |
|
|
112
|
+
| `api.post(url, body?, opts?)` | `<T>(string, unknown?, ApiRequestOptions?) => Promise<T>` | POST |
|
|
113
|
+
| `api.put(url, body?, opts?)` | `<T>(string, unknown?, ApiRequestOptions?) => Promise<T>` | PUT |
|
|
114
|
+
| `api.patch(url, body?, opts?)` | `<T>(string, unknown?, ApiRequestOptions?) => Promise<T>` | PATCH |
|
|
115
|
+
| `api.delete(url, opts?)` | `<T>(string, ApiRequestOptions?) => Promise<T>` | DELETE |
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
// 错误处理
|
|
119
|
+
try {
|
|
120
|
+
await ctx.api!.get('/users')
|
|
121
|
+
} catch (e) {
|
|
122
|
+
if (e instanceof ApiError) {
|
|
123
|
+
console.log(e.status, e.body) // e.g. 404, 'Not Found'
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
`ApiError`:`{ status: number, body: string }`,继承 `Error`。
|
|
129
|
+
|
|
130
|
+
| ApiRequestOptions | 类型 | 说明 |
|
|
131
|
+
|-------------------|------|------|
|
|
132
|
+
| `headers` | `Record<string, string>` | 本次请求自定义请求头 |
|
|
133
|
+
| `signal` | `AbortSignal` | 取消请求 |
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## auth — 认证中间件
|
|
138
|
+
|
|
139
|
+
```tsx
|
|
140
|
+
import { createApp, auth } from 'weifuwu/client'
|
|
141
|
+
|
|
142
|
+
createApp()
|
|
143
|
+
.use(auth())
|
|
144
|
+
.mount('#root', App)
|
|
145
|
+
|
|
146
|
+
// 在组件中
|
|
147
|
+
function Profile(_props: {}, ctx: WfuiContext) {
|
|
148
|
+
return (props) => {
|
|
149
|
+
if (!ctx.auth?.isLoggedIn) return <p>请登录</p>
|
|
150
|
+
return <p>欢迎, {ctx.auth?.user?.name}</p>
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 登录
|
|
155
|
+
ctx.auth?.login(token, { id: 1, name: 'Alice' }, refreshToken)
|
|
156
|
+
|
|
157
|
+
// 登出
|
|
158
|
+
ctx.auth?.logout()
|
|
159
|
+
|
|
160
|
+
// 更新用户信息
|
|
161
|
+
ctx.auth?.setUser({ id: 1, name: 'Bob' })
|
|
162
|
+
|
|
163
|
+
// 刷新 token
|
|
164
|
+
await ctx.auth?.refresh() // → boolean
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
| 选项 | 类型 | 默认值 | 说明 |
|
|
168
|
+
|------|------|--------|------|
|
|
169
|
+
| `storage` | `Storage` | `localStorage` | 存储方式 |
|
|
170
|
+
| `tokenKey` | `string` | `'weifuwu_token'` | Token 存储 key |
|
|
171
|
+
| `userKey` | `string` | `'weifuwu_user'` | 用户信息存储 key |
|
|
172
|
+
| `refreshTokenKey` | `string` | `'weifuwu_refresh'` | Refresh token 存储 key |
|
|
173
|
+
| `refreshEndpoint` | `string` | `'/api/auth/refresh'` | 刷新端点 |
|
|
174
|
+
|
|
175
|
+
| ctx.auth | 类型 | 说明 |
|
|
176
|
+
|----------|------|------|
|
|
177
|
+
| `.token` | `string \| null` | JWT token |
|
|
178
|
+
| `.user` | `any` | 用户对象 |
|
|
179
|
+
| `.isLoggedIn` | `boolean` | 是否已登录(基于 token 存在) |
|
|
180
|
+
| `.login(token, user, refreshToken?)` | `void` | 登录 |
|
|
181
|
+
| `.logout()` | `void` | 登出(清除存储) |
|
|
182
|
+
| `.setUser(user)` | `void` | 更新用户信息 |
|
|
183
|
+
| `.refresh()` | `Promise<boolean>` | 刷新 token(自动检测过期) |
|
|
184
|
+
|
|
185
|
+
启动时自动检测 token 是否过期(JWT `exp` 提前 30 秒),过期则自动调用 `refresh()`。
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## ws — WebSocket 客户端中间件
|
|
190
|
+
|
|
191
|
+
```tsx
|
|
192
|
+
import { createApp, ws } from 'weifuwu/client'
|
|
193
|
+
|
|
194
|
+
createApp()
|
|
195
|
+
.use(ws({ url: '/ws' }))
|
|
196
|
+
.mount('#root', App)
|
|
197
|
+
|
|
198
|
+
// 发送消息
|
|
199
|
+
ctx.ws?.send({ type: 'chat', body: 'hello' })
|
|
200
|
+
|
|
201
|
+
// 接收消息 — 返回 unsubscribe 函数
|
|
202
|
+
const unsubscribe = ctx.ws?.onMessage((msg) => {
|
|
203
|
+
console.log('收到:', msg)
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
// 清理
|
|
207
|
+
unsubscribe?.()
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
| 选项 | 类型 | 默认值 | 说明 |
|
|
211
|
+
|------|------|--------|------|
|
|
212
|
+
| `url` | `string` | `'/ws'` | WebSocket 连接地址 |
|
|
213
|
+
| `reconnectInterval` | `number` | `3000` | 重连间隔(ms) |
|
|
214
|
+
| `maxReconnect` | `number` | `10` | 最大重连次数 |
|
|
215
|
+
| `pingInterval` | `number` | `30000` | 心跳发送间隔 |
|
|
216
|
+
| `pingTimeout` | `number` | `10000` | 心跳超时断开 |
|
|
217
|
+
|
|
218
|
+
| ctx.ws | 类型 | 说明 |
|
|
219
|
+
|--------|------|------|
|
|
220
|
+
| `.send(msg)` | `(unknown) => void` | 发送 JSON 消息 |
|
|
221
|
+
| `.onMessage(fn)` | `(fn) => () => void` | 订阅消息,返回 unsubscribe |
|
|
222
|
+
| `.isConnected` | `boolean` | 连接状态 |
|
|
223
|
+
| `.close()` | `() => void` | 断开连接 |
|
|
224
|
+
|
|
225
|
+
自动重连(指数退避)、心跳保活、JSON 序列化/反序列化。
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## i18n — 国际化中间件
|
|
230
|
+
|
|
231
|
+
```tsx
|
|
232
|
+
import { createApp, i18n } from 'weifuwu/client'
|
|
233
|
+
|
|
234
|
+
createApp()
|
|
235
|
+
.use(i18n({
|
|
236
|
+
locale: 'zh-CN',
|
|
237
|
+
messages: {
|
|
238
|
+
'title': '仪表盘',
|
|
239
|
+
'welcome': '欢迎光临',
|
|
240
|
+
},
|
|
241
|
+
}))
|
|
242
|
+
.mount('#root', App)
|
|
243
|
+
|
|
244
|
+
// 组件中使用
|
|
245
|
+
<h1>{ctx.i18n?.t('title')}</h1>
|
|
246
|
+
<p>{ctx.i18n?.t('welcome')}</p>
|
|
247
|
+
|
|
248
|
+
// 运行时切换语言
|
|
249
|
+
ctx.i18n?.setLocale('en-US')
|
|
250
|
+
// → 自动触发根组件重渲染(所有组件使用新语言文案)
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
| I18nOptions | 类型 | 默认值 | 说明 |
|
|
254
|
+
|-------------|------|--------|------|
|
|
255
|
+
| `locale` | `string` | `'zh-CN'` | 初始语言 |
|
|
256
|
+
| `messages` | `Record<string, string>` | `{}` | 翻译键值对 |
|
|
257
|
+
| `components` | `Record<string, Record<string, string>>` | `{}` | 组件文案覆盖 |
|
|
258
|
+
|
|
259
|
+
| ctx.i18n | 类型 | 说明 |
|
|
260
|
+
|----------|------|------|
|
|
261
|
+
| `.t(key, fallback?)` | `(string, string?) => string` | 翻译 |
|
|
262
|
+
| `.locale` | `string` | 当前语言 |
|
|
263
|
+
| `.setLocale(lang)` | `(string) => void` | 切换语言(触发重渲染) |
|
|
264
|
+
| `.components` | `Record<string, Record<string, string>>` | 组件文案映射 |
|
|
265
|
+
|
|
266
|
+
内置语言包:
|
|
267
|
+
|
|
268
|
+
```ts
|
|
269
|
+
import { zhCN, enUS } from 'weifuwu/client'
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
- `zh-CN`:默认中文
|
|
273
|
+
- `en-US`:英文
|
|
274
|
+
|
|
275
|
+
组件文案(Button 的 `加载中...`、FileUpload 的 `点击或拖拽上传文件` 等)随语言自动切换。组件内部通过 `ctx.i18n?.components?.ComponentName.field` 读取。
|
|
276
|
+
|
|
277
|
+
组件支持 `props.locale` 局部覆盖语言。
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## ErrorBoundary — 错误边界
|
|
282
|
+
|
|
283
|
+
```tsx
|
|
284
|
+
import { ErrorBoundary } from 'weifuwu/client'
|
|
285
|
+
|
|
286
|
+
<ErrorBoundary fallback={<p>出错了,请刷新页面</p>}>
|
|
287
|
+
<UserProfile />
|
|
288
|
+
</ErrorBoundary>
|
|
289
|
+
|
|
290
|
+
// fallback 也可以是一个接收 error 的函数
|
|
291
|
+
<ErrorBoundary fallback={({ error }) => (
|
|
292
|
+
<div>
|
|
293
|
+
<p>出错了: {String(error)}</p>
|
|
294
|
+
<button onClick={() => location.reload()}>重试</button>
|
|
295
|
+
</div>
|
|
296
|
+
)}>
|
|
297
|
+
<UserProfile />
|
|
298
|
+
</ErrorBoundary>
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
| ErrorBoundaryProps | 类型 | 默认值 | 说明 |
|
|
302
|
+
|--------------------|------|--------|------|
|
|
303
|
+
| `fallback` | `VNode \| ((props: { error }) => VNode) \| null` | `null` | 错误时渲染的内容 |
|
|
304
|
+
| `children` | `any` | — | 子组件 |
|
|
305
|
+
|
|
306
|
+
捕获子组件 render 时的错误 → 渲染 fallback。清除 `error` 即可重试。
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
## confirm — 确认对话框
|
|
311
|
+
|
|
312
|
+
两种用法,共享同一视觉与行为(基于 Modal 封装):
|
|
313
|
+
|
|
314
|
+
**① 命令式 `ctx.confirm()`(推荐,操作前询问)**
|
|
315
|
+
|
|
316
|
+
```tsx
|
|
317
|
+
import { createApp } from 'weifuwu/client'
|
|
318
|
+
import { confirm } from 'weifuwu/components'
|
|
319
|
+
|
|
320
|
+
createApp()
|
|
321
|
+
.use(confirm())
|
|
322
|
+
.mount('#root', App)
|
|
323
|
+
|
|
324
|
+
// 任意代码中(组件事件、async 逻辑)
|
|
325
|
+
async function handleDelete(ctx: WfuiContext) {
|
|
326
|
+
const ok = await ctx.confirm?.('确定删除这条记录?', {
|
|
327
|
+
title: '确认删除',
|
|
328
|
+
confirmText: '删除',
|
|
329
|
+
cancelText: '取消',
|
|
330
|
+
variant: 'danger', // 'primary' | 'danger'
|
|
331
|
+
})
|
|
332
|
+
if (ok) {
|
|
333
|
+
// 执行删除...
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
**② 声明式 `<Confirm>`(需要受控状态时)**
|
|
339
|
+
|
|
340
|
+
```tsx
|
|
341
|
+
import { Confirm } from 'weifuwu/components'
|
|
342
|
+
|
|
343
|
+
<Confirm
|
|
344
|
+
open={confirming}
|
|
345
|
+
title="确认删除"
|
|
346
|
+
message="确定删除这条记录?"
|
|
347
|
+
confirmText="删除"
|
|
348
|
+
variant="danger"
|
|
349
|
+
onConfirm={() => doDelete()}
|
|
350
|
+
onCancel={() => setConfirming(false)}
|
|
351
|
+
/>
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
| ConfirmOptions | 类型 | 默认值 | 说明 |
|
|
355
|
+
|----------------|------|--------|------|
|
|
356
|
+
| `title` | `string` | `'确认操作'` | 对话框标题 |
|
|
357
|
+
| `confirmText` | `string` | `'确定'` | 确认按钮文字 |
|
|
358
|
+
| `cancelText` | `string` | `'取消'` | 取消按钮文字 |
|
|
359
|
+
| `variant` | `'primary' \| 'danger'` | `'primary'` | 按钮样式变体 |
|
|
360
|
+
| `width` | `string` | Modal 默认 | 对话框宽度 |
|
|
361
|
+
|
|
362
|
+
- `ctx.confirm()` 返回 `Promise<boolean>`,ESC / 点击遮罩 / 取消 → resolve(false)
|
|
363
|
+
- 组件化渲染(Modal + portal),自动锁定滚动 + 焦点陷阱,i18n 文案可配置
|
|
364
|
+
- 多次调用各自独立渲染(叠放语义),互不干扰
|
|
365
|
+
|
|
366
|
+
---
|
|
367
|
+
|
|
368
|
+
## toast — 命令式消息提示
|
|
369
|
+
|
|
370
|
+
`ctx.toast()` 是 `<Toast>` 组件的全局命令式封装:任意代码中一行调用,自动消失、自动清理,无需宿主状态。
|
|
371
|
+
|
|
372
|
+
```tsx
|
|
373
|
+
import { createApp } from 'weifuwu/client'
|
|
374
|
+
import { toast } from 'weifuwu/components'
|
|
375
|
+
|
|
376
|
+
createApp()
|
|
377
|
+
.use(toast({ position: 'top-right', duration: 3000, max: 3 }))
|
|
378
|
+
.mount('#root', App)
|
|
379
|
+
|
|
380
|
+
// 任意代码中(组件事件、api 拦截器、WS 回调、定时器)
|
|
381
|
+
ctx.toast?.('保存成功', 'success')
|
|
382
|
+
ctx.toast?.('请求失败', 'error')
|
|
383
|
+
ctx.toast?.('普通消息') // 默认 type = 'info'
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
| ToastOptions | 类型 | 默认值 | 说明 |
|
|
387
|
+
|-------------|------|--------|------|
|
|
388
|
+
| `position` | `ToastPosition` | `'top-right'` | 容器位置 |
|
|
389
|
+
| `duration` | `number` | `3000` | 默认自动消失时间(ms),0 = 不消失 |
|
|
390
|
+
| `max` | `number` | `3` | 最大显示条数,超出移除最早 |
|
|
391
|
+
|
|
392
|
+
单条可覆盖自动消失时间:`ctx.toast('慢一点消失', 'info', 5000)`。
|
|
393
|
+
|
|
394
|
+
与声明式 `<Toast toasts={...}/>` 共存:声明式用于局部列表(合并消息、自定义布局),命令式用于全局一次性反馈。
|
|
395
|
+
|
|
396
|
+
---
|
|
397
|
+
|
|
398
|
+
## ScrollLock / FocusTrap
|
|
399
|
+
|
|
400
|
+
```tsx
|
|
401
|
+
import { lockScroll, unlockScroll } from 'weifuwu/client'
|
|
402
|
+
import { trapFocus } from 'weifuwu/client'
|
|
403
|
+
|
|
404
|
+
// 锁定/解锁滚动(支持嵌套计数)
|
|
405
|
+
lockScroll()
|
|
406
|
+
unlockScroll()
|
|
407
|
+
|
|
408
|
+
// 焦点陷阱 — 返回 cleanup 函数
|
|
409
|
+
const cleanup = trapFocus(containerElement)
|
|
410
|
+
cleanup() // 恢复之前的焦点
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
| API | 说明 |
|
|
414
|
+
|-----|------|
|
|
415
|
+
| `lockScroll()` | 锁定 body 滚动(iOS 兼容) |
|
|
416
|
+
| `unlockScroll()` | 解锁滚动,恢复滚动位置 |
|
|
417
|
+
| `trapFocus(el)` | Tab/Shift+Tab 在容器内循环,返回 cleanup |
|
|
418
|
+
|
|
419
|
+
---
|
|
420
|
+
|
|
421
|
+
## extendCtx — 上下文扩展
|
|
422
|
+
|
|
423
|
+
```tsx
|
|
424
|
+
import { extendCtx } from 'weifuwu/client'
|
|
425
|
+
|
|
426
|
+
// 在 AppMiddleware 中创建新 ctx,原 ctx getter 通过原型链继承
|
|
427
|
+
function myMw(ctx: WfuiContext): WfuiContext {
|
|
428
|
+
return extendCtx(ctx, { myField: 'value' })
|
|
429
|
+
}
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
`extendCtx` 使用 `Object.create(ctx)` 保持原型链,再用 `Object.assign` 添加新字段。保证 getter 不被快照化。
|
|
433
|
+
|
|
434
|
+
---
|
|
435
|
+
|