@maiyunnet/kebab 8.6.5 → 9.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.
@@ -0,0 +1,426 @@
1
+ /**
2
+ * --- Kebab React BrowserRouter 全页面示例组件 ---
3
+ *
4
+ * node ./source/main build -d ./source/www/example/stc/view
5
+ *
6
+ * 【特性】
7
+ * 本组件演示 _loadReactPage 的 router: 'browser' 模式,实现地址栏与路由联动:
8
+ * - 服务端:框架自动用 StaticRouter 包裹,按当前请求 URL 渲染对应路由
9
+ * - 客户端:框架水合脚本用 BrowserRouter 包裹,Link/NavLink 导航会修改真实地址栏
10
+ * - 直接访问深链接(如 /test/react-router-page/user/42)开箱即用,无需前端 fallback
11
+ *
12
+ * 【要点】
13
+ * 1. 组件本身不包含任何 Router 包裹层,只使用 Routes/Route/Link/NavLink/useParams 等
14
+ * 2. BrowserRouter 的 basename 由框架通过 _routerBase prop 传入
15
+ * 3. 嵌套路由通过 Outlet 渲染子路由内容
16
+ *
17
+ * 【编译方式】
18
+ * tsc watch 会自动编译本文件为同路径的 .js,无需额外命令。
19
+ * 如需打包为 .bundle.js,执行:node ./source/main build
20
+ *
21
+ * 【Tailwind CSS 构建】
22
+ * bundle 模式下不再加载 CDN,需提前构建 CSS 产物。
23
+ * 执行 node ./source/main build 时会自动构建同名 .css,无需单独执行。
24
+ * 框架通过 _urlStc 和 _staticVer 自动拼接正确的带版本号 URL。
25
+ */
26
+
27
+ import { useState, useEffect } from 'react';
28
+ import { Routes, Route, Link, NavLink, useParams, useNavigate, useLocation, Outlet } from 'react-router-dom';
29
+
30
+ // --- 用户数据接口 ---
31
+ interface IUser {
32
+ 'id': string;
33
+ 'name': string;
34
+ 'email': string;
35
+ }
36
+
37
+ // --- 组件接收的 props 接口 ---
38
+ interface IProps {
39
+ 'title': string;
40
+ 'serverTime': string;
41
+ 'node': string;
42
+ /** --- 用户列表数据,SSR 时由后端提供,SPA 导航时前端 fetch --- */
43
+ 'users'?: IUser[];
44
+ /** --- 单个用户数据,SSR 时由后端提供,SPA 导航时前端 fetch --- */
45
+ 'user'?: IUser;
46
+ '_urlBase': string;
47
+ '_urlStc': string;
48
+ '_urlFull': string;
49
+ '_staticVer': string;
50
+ /** --- 框架注入:BrowserRouter 的 basename,如 /test/react-router-page --- */
51
+ '_routerBase'?: string;
52
+ /** --- 框架注入:import map JSON 字符串 --- */
53
+ '_importMapJson'?: string;
54
+ /** --- 框架注入:水合脚本 --- */
55
+ '_hydrateScript'?: string;
56
+ /** --- 框架注入:fullProps 序列化 JSON --- */
57
+ '_propsJson'?: string;
58
+ }
59
+
60
+ // --- 基础控件 ---
61
+
62
+ /** --- 卡片容器 --- */
63
+ function Card({ children, className = '' }: { 'children': React.ReactNode; 'className'?: string }) {
64
+ return (
65
+ <div className={`bg-white rounded-xl shadow-sm border border-slate-200 p-6 ${className}`}>
66
+ {children}
67
+ </div>
68
+ );
69
+ }
70
+
71
+ /** --- 标签徽章 --- */
72
+ function Badge({ children, variant = 'default' }: {
73
+ 'children': string;
74
+ 'variant'?: 'default' | 'success' | 'info';
75
+ }) {
76
+ const styles = {
77
+ 'default': 'bg-slate-100 text-slate-600',
78
+ 'success': 'bg-green-100 text-green-700',
79
+ 'info': 'bg-blue-100 text-blue-700',
80
+ };
81
+ return (
82
+ <span className={`inline-flex px-2.5 py-0.5 rounded-md text-xs font-semibold ${styles[variant]}`}>
83
+ {children}
84
+ </span>
85
+ );
86
+ }
87
+
88
+ // --- 导航栏 ---
89
+
90
+ /** --- 顶部导航栏,NavLink 自动高亮当前路由 --- */
91
+ function NavBar() {
92
+ /** --- NavLink className 回调:激活时高亮 --- */
93
+ const cls = ({ isActive }: { 'isActive': boolean }): string =>
94
+ isActive
95
+ ? 'px-3 py-1.5 rounded-lg bg-blue-500 text-white text-sm font-medium transition-colors'
96
+ : 'px-3 py-1.5 rounded-lg text-slate-600 hover:bg-slate-100 text-sm font-medium transition-colors';
97
+ return (
98
+ <nav className="flex items-center gap-2 flex-wrap">
99
+ <NavLink to="/" end className={cls}>Home</NavLink>
100
+ <NavLink to="/about" className={cls}>About</NavLink>
101
+ <NavLink to="/user" className={cls}>Users</NavLink>
102
+ </nav>
103
+ );
104
+ }
105
+
106
+ // --- 路由页面 ---
107
+
108
+ /** --- 首页 --- */
109
+ function PageHome({ serverTime, node }: { 'serverTime': string; 'node': string }) {
110
+ const location = useLocation();
111
+ const [hydrated, setHydrated] = useState(false);
112
+ useEffect(() => {
113
+ setHydrated(true);
114
+ }, []);
115
+ return (
116
+ <div className="space-y-4">
117
+ <p className="text-slate-600 text-sm">Home page demonstrating SSR + BrowserRouter integration.</p>
118
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-3 text-sm">
119
+ <div className="bg-slate-50 rounded-lg p-3">
120
+ <div className="text-slate-500 text-xs mb-1">Server Render Time</div>
121
+ <div className="font-mono text-slate-800">{serverTime}</div>
122
+ </div>
123
+ <div className="bg-slate-50 rounded-lg p-3">
124
+ <div className="text-slate-500 text-xs mb-1">Node.js Version</div>
125
+ <div className="font-mono text-slate-800">{node}</div>
126
+ </div>
127
+ <div className="bg-slate-50 rounded-lg p-3">
128
+ <div className="text-slate-500 text-xs mb-1">Current pathname (useLocation)</div>
129
+ <div className="font-mono text-slate-800">{location.pathname}</div>
130
+ </div>
131
+ <div className="bg-slate-50 rounded-lg p-3">
132
+ <div className="text-slate-500 text-xs mb-1">Hydration Status</div>
133
+ <div suppressHydrationWarning>
134
+ {hydrated
135
+ ? <Badge variant="success">Hydrated</Badge>
136
+ : <Badge>SSR</Badge>}
137
+ </div>
138
+ </div>
139
+ </div>
140
+ <div className="flex gap-2 flex-wrap mt-2">
141
+ <Link
142
+ to="/about"
143
+ className="inline-flex items-center px-3 py-1.5 rounded-lg bg-blue-500 hover:bg-blue-600 text-white text-xs font-medium transition-colors"
144
+ >
145
+ Go to About
146
+ </Link>
147
+ <Link
148
+ to="/user"
149
+ className="inline-flex items-center px-3 py-1.5 rounded-lg border border-slate-300 hover:bg-slate-50 text-slate-700 text-xs font-medium transition-colors"
150
+ >
151
+ Go to Users
152
+ </Link>
153
+ </div>
154
+ </div>
155
+ );
156
+ }
157
+
158
+ /** --- 关于页 --- */
159
+ function PageAbout() {
160
+ const navigate = useNavigate();
161
+ const location = useLocation();
162
+ return (
163
+ <div className="space-y-4">
164
+ <p className="text-slate-600 text-sm">About page: demonstrates <code className="bg-slate-100 px-1 rounded">useNavigate()</code> programmatic navigation.</p>
165
+ <div className="bg-slate-50 rounded-lg p-3 text-sm">
166
+ <div className="text-slate-500 text-xs mb-1">Current pathname</div>
167
+ <div className="font-mono text-slate-800">{location.pathname}</div>
168
+ </div>
169
+ <div className="flex gap-2 flex-wrap">
170
+ <button
171
+ onClick={() => navigate('/')}
172
+ className="inline-flex items-center px-3 py-1.5 rounded-lg border border-slate-300 hover:bg-slate-50 text-slate-700 text-xs font-medium cursor-pointer transition-colors"
173
+ >
174
+ Back to Home
175
+ </button>
176
+ <button
177
+ onClick={() => navigate('/user/42')}
178
+ className="inline-flex items-center px-3 py-1.5 rounded-lg bg-blue-500 hover:bg-blue-600 text-white text-xs font-medium cursor-pointer transition-colors"
179
+ >
180
+ Go to User #42
181
+ </button>
182
+ </div>
183
+ </div>
184
+ );
185
+ }
186
+
187
+ /** --- 用户列表页 --- */
188
+ function PageUsers({ users: initialUsers, urlBase }: { 'users'?: IUser[]; 'urlBase': string }) {
189
+ const location = useLocation();
190
+ const [users, setUsers] = useState(initialUsers);
191
+ const [loading, setLoading] = useState(false);
192
+ useEffect(() => {
193
+ // --- SSR 已提供数据则跳过 fetch ---
194
+ if (users) {
195
+ return;
196
+ }
197
+ setLoading(true);
198
+ fetch(`${urlBase}test/react-router-page-data?path=/user`)
199
+ .then(r => r.json())
200
+ .then((res) => {
201
+ if (res.result > 0 && res.users) {
202
+ setUsers(res.users);
203
+ }
204
+ setLoading(false);
205
+ })
206
+ .catch(() => setLoading(false));
207
+ }, []);
208
+ return (
209
+ <div className="space-y-4">
210
+ <p className="text-slate-600 text-sm">User list — click to view details (with nested /profile route).</p>
211
+ <div className="bg-slate-50 rounded-lg p-3 text-sm">
212
+ <div className="text-slate-500 text-xs mb-1">Current pathname</div>
213
+ <div className="font-mono text-slate-800">{location.pathname}</div>
214
+ </div>
215
+ {loading && <p className="text-slate-400 text-sm">Loading...</p>}
216
+ {users && (
217
+ <ul className="space-y-2">
218
+ {users.map(u => (
219
+ <li key={u.id} className="flex items-center gap-3">
220
+ <Link
221
+ to={`/user/${u.id}`}
222
+ className="inline-flex items-center px-3 py-1.5 rounded-lg border border-slate-200 hover:bg-slate-50 text-slate-700 text-xs font-medium transition-colors"
223
+ >
224
+ {u.name} (id={u.id}) — /user/{u.id}
225
+ </Link>
226
+ </li>
227
+ ))}
228
+ </ul>
229
+ )}
230
+ </div>
231
+ );
232
+ }
233
+
234
+ /**
235
+ * --- 用户详情页(含嵌套路由 Outlet) ---
236
+ * 子路由 /profile 通过 <Outlet /> 渲染在此处
237
+ */
238
+ function PageUserDetail({ user: initialUser, urlBase }: { 'user'?: IUser; 'urlBase': string }) {
239
+ const { id } = useParams<{ 'id': string }>();
240
+ const navigate = useNavigate();
241
+ const location = useLocation();
242
+ const [user, setUser] = useState<IUser | undefined>(
243
+ (initialUser?.id === id) ? initialUser : undefined
244
+ );
245
+ const [loading, setLoading] = useState(false);
246
+ useEffect(() => {
247
+ // --- SSR 数据匹配当前 id 则跳过 fetch ---
248
+ if (user?.id === id) {
249
+ return;
250
+ }
251
+ setLoading(true);
252
+ fetch(`${urlBase}test/react-router-page-data?path=/user/${encodeURIComponent(id ?? '')}`)
253
+ .then(r => r.json())
254
+ .then((res) => {
255
+ if (res.result > 0 && res.user) {
256
+ setUser(res.user);
257
+ }
258
+ setLoading(false);
259
+ })
260
+ .catch(() => setLoading(false));
261
+ }, [id]);
262
+ return (
263
+ <div className="space-y-4">
264
+ {loading && <p className="text-slate-400 text-sm">Loading...</p>}
265
+ {user && (
266
+ <p className="text-slate-700 text-sm font-medium">
267
+ User Detail: <code className="bg-slate-100 px-1.5 rounded font-mono">{user.name}</code>
268
+ &nbsp;<span className="text-slate-400 text-xs">(id=&quot;{id}&quot;, email={user.email})</span>
269
+ </p>
270
+ )}
271
+ <div className="bg-slate-50 rounded-lg p-3 text-sm">
272
+ <div className="text-slate-500 text-xs mb-1">Current pathname</div>
273
+ <div className="font-mono text-slate-800">{location.pathname}</div>
274
+ </div>
275
+ {/* --- 嵌套路由区域:/user/:id/profile --- */}
276
+ <div className="border border-dashed border-slate-300 rounded-lg p-4">
277
+ <p className="text-slate-500 text-xs mb-3">
278
+ Nested route <code className="bg-slate-100 px-1 rounded">/user/:id/profile</code> (rendered via Outlet):
279
+ </p>
280
+ <Outlet />
281
+ {location.pathname === `/user/${id}` && (
282
+ <Link
283
+ to={`/user/${id}/profile`}
284
+ className="inline-flex items-center px-3 py-1.5 rounded-lg bg-blue-500 hover:bg-blue-600 text-white text-xs font-medium transition-colors"
285
+ >
286
+ View Profile
287
+ </Link>
288
+ )}
289
+ </div>
290
+ <button
291
+ onClick={() => navigate('/user')}
292
+ className="inline-flex items-center px-3 py-1.5 rounded-lg border border-slate-300 hover:bg-slate-50 text-slate-700 text-xs font-medium cursor-pointer transition-colors"
293
+ >
294
+ Back to Users
295
+ </button>
296
+ </div>
297
+ );
298
+ }
299
+
300
+ /** --- 嵌套子路由:用户 Profile --- */
301
+ function PageUserProfile() {
302
+ const { id } = useParams<{ 'id': string }>();
303
+ const location = useLocation();
304
+ return (
305
+ <div className="bg-blue-50 rounded-lg p-3 mb-3 space-y-2 text-sm">
306
+ <Badge variant="info">Nested Route Active</Badge>
307
+ <div className="text-slate-700">
308
+ Profile of user <strong>{id}</strong> (rendered via Outlet)
309
+ </div>
310
+ <div className="font-mono text-slate-500 text-xs">{location.pathname}</div>
311
+ </div>
312
+ );
313
+ }
314
+
315
+ /** --- 404 兜底页 --- */
316
+ function PageNotFound() {
317
+ const location = useLocation();
318
+ return (
319
+ <div className="space-y-3">
320
+ <p className="text-red-600 font-medium">Page Not Found</p>
321
+ <div className="text-slate-500 text-xs font-mono">{location.pathname}</div>
322
+ <Link to="/" className="inline-flex items-center px-3 py-1.5 rounded-lg bg-blue-500 text-white text-xs font-medium transition-colors hover:bg-blue-600">
323
+ Back to Home
324
+ </Link>
325
+ </div>
326
+ );
327
+ }
328
+
329
+ // --- 页面主组件 ---
330
+
331
+ /**
332
+ * --- Kebab React BrowserRouter 全页面演示 ---
333
+ * 框架负责用 StaticRouter(服务端)/ BrowserRouter(客户端)包裹本组件,
334
+ * 组件内部只需使用 Routes/Route/Link 等,无需自行包裹 Router。
335
+ */
336
+ export default function ReactRouterPage({
337
+ title, serverTime, node, users, user, _urlBase, _urlStc, _staticVer, _importMapJson, _hydrateScript, _propsJson,
338
+ }: IProps) {
339
+ return (
340
+ <html lang="en">
341
+ <head>
342
+ <meta charSet="UTF-8" />
343
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
344
+ {/* eslint-disable-next-line @typescript-eslint/naming-convention */}
345
+ <title suppressHydrationWarning>{title}</title>
346
+ {/* --- CSS:dev 模式(无 bundle)用 Tailwind CDN;bundle 模式加载本地构建产物 --- */}
347
+ {_importMapJson
348
+ ? <script src="https://cdn.tailwindcss.com" />
349
+ : <link rel="stylesheet" href={`${_urlStc}view/react-router-page.css?v=${_staticVer}`} />}
350
+ {/* --- import map:让浏览器识别 bare import,esm.sh 自动解析依赖 --- */}
351
+ {_importMapJson && (
352
+ <script type="importmap" dangerouslySetInnerHTML={{ '__html': _importMapJson }} />
353
+ )}
354
+ </head>
355
+ <body className="bg-slate-50 min-h-screen">
356
+ <div className="max-w-2xl mx-auto px-4 py-10 space-y-6">
357
+ {/* --- 页头 --- */}
358
+ <div className="flex items-start justify-between">
359
+ <div>
360
+ <h1 className="text-2xl font-bold text-slate-900">Kebab React Router</h1>
361
+ <p className="text-slate-500 mt-1 text-sm">
362
+ <code className="bg-slate-100 px-1.5 py-0.5 rounded font-mono text-xs">router: &apos;browser&apos;</code>
363
+ &nbsp;mode — URL synced with routes
364
+ </p>
365
+ </div>
366
+ <a
367
+ href={`${_urlBase}`}
368
+ className="text-xs text-slate-400 hover:text-slate-600 transition-colors mt-1"
369
+ >
370
+ Back to Index
371
+ </a>
372
+ </div>
373
+
374
+ {/* --- 导航卡片 --- */}
375
+ <div className="bg-white rounded-xl shadow-sm border border-slate-200 px-4 py-3">
376
+ <NavBar />
377
+ </div>
378
+
379
+ {/* --- 路由内容区 --- */}
380
+ <div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
381
+ <Routes>
382
+ <Route
383
+ path="/"
384
+ element={<PageHome serverTime={serverTime} node={node} />}
385
+ />
386
+ <Route path="/about" element={<PageAbout />} />
387
+ <Route path="/user" element={<PageUsers users={users} urlBase={_urlBase} />} />
388
+ <Route path="/user/:id" element={<PageUserDetail user={user} urlBase={_urlBase} />}>
389
+ {/* --- 嵌套路由,对应 /user/:id/profile --- */}
390
+ <Route path="profile" element={<PageUserProfile />} />
391
+ </Route>
392
+ <Route path="*" element={<PageNotFound />} />
393
+ </Routes>
394
+ </div>
395
+
396
+ {/* --- 说明卡片 --- */}
397
+ <Card className="text-xs text-slate-500 space-y-1.5">
398
+ <p className="font-semibold text-slate-700 text-sm">How It Works</p>
399
+ <p>• Server: framework wraps with <code className="bg-slate-100 px-1 rounded">StaticRouter</code> to SSR the matching route</p>
400
+ <p>• Client: hydration script wraps with <code className="bg-slate-100 px-1 rounded">BrowserRouter</code> for URL-synced navigation</p>
401
+ <p>• Data: one backend method (<code className="bg-slate-100 px-1 rounded">_getRouteData</code>) serves both SSR props and SPA API</p>
402
+ <p>• Deep links like <code className="bg-slate-100 px-1 rounded">/test/react-router-page/user/42</code> work out of the box</p>
403
+ </Card>
404
+ </div>
405
+
406
+ {/* --- 框架注入:props JSON,供客户端水合读取 --- */}
407
+ {_propsJson && (
408
+ <script
409
+ id="__kebab_props__"
410
+ type="application/json"
411
+ suppressHydrationWarning
412
+ dangerouslySetInnerHTML={{ '__html': _propsJson }}
413
+ />
414
+ )}
415
+ {/* --- 框架注入:水合脚本 --- */}
416
+ {_hydrateScript && (
417
+ <script
418
+ type="module"
419
+ suppressHydrationWarning
420
+ dangerouslySetInnerHTML={{ '__html': _hydrateScript }}
421
+ />
422
+ )}
423
+ </body>
424
+ </html>
425
+ );
426
+ }