@maiyunnet/kebab 9.14.3 → 9.15.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/doc/kebab-rag.md +364 -560
- package/index.d.ts +1 -1
- package/index.js +1 -1
- package/lib/core.js +19 -6
- package/lib/crypto.js +1 -1
- package/lib/fs.js +1 -2
- package/lib/kv.d.ts +5 -0
- package/lib/kv.js +21 -1
- package/lib/net/formdata.d.ts +2 -81
- package/lib/net/formdata.js +2 -164
- package/lib/net.js +16 -2
- package/lib/ratelimit.js +6 -6
- package/lib/scan.js +18 -5
- package/lib/session.js +2 -0
- package/lib/text.d.ts +6 -0
- package/lib/text.js +39 -6
- package/lib/undici/response.d.ts +1 -1
- package/lib/undici/response.js +0 -2
- package/lib/undici.js +17 -2
- package/package.json +1 -1
- package/sys/child.js +0 -8
- package/sys/ctr.d.ts +6 -0
- package/sys/master.js +158 -48
- package/sys/route.d.ts +6 -0
- package/sys/route.js +87 -25
- package/www/example/ctr/test.js +5 -3
- package/www/example/stc/view/hello.page.js +1 -1
- package/www/example/stc/view/hello.page.tsx +2 -2
- package/www/example/stc/view/react-router.page.js +4 -4
- package/www/example/stc/view/react-router.page.tsx +15 -16
- package/www/example/stc/view/react.page.js +7 -7
- package/www/example/stc/view/react.page.tsx +27 -22
package/www/example/ctr/test.js
CHANGED
|
@@ -2386,7 +2386,7 @@ result: <pre>${lText.htmlescape(JSON.stringify(json2, null, 4))}</pre>`);
|
|
|
2386
2386
|
}];
|
|
2387
2387
|
}
|
|
2388
2388
|
/** --- 测试 ProxyAgent --- */
|
|
2389
|
-
_undiciProxyAgent = lUndici.getProxyAgent('http://192.168.
|
|
2389
|
+
_undiciProxyAgent = lUndici.getProxyAgent('http://192.168.31.10:20809');
|
|
2390
2390
|
async undiciProxy() {
|
|
2391
2391
|
const echo = [];
|
|
2392
2392
|
const res = await lUndici.get('https://www.google.com/', { 'reuse': this._undiciProxyAgent });
|
|
@@ -3685,7 +3685,8 @@ rtn.push(reader.readBCDString());</pre>${JSON.stringify(rtn)}`);
|
|
|
3685
3685
|
});</pre>`);
|
|
3686
3686
|
if (imgResult && imgResult.list?.length) {
|
|
3687
3687
|
for (const img of imgResult.list) {
|
|
3688
|
-
|
|
3688
|
+
const url = img.url.startsWith('http') ? img.url : 'data:image/png;base64,' + img.url;
|
|
3689
|
+
echo.push(`<div><img src="${lText.htmlescape(url)}" style="max-width: 512px;" /></div>${lText.htmlescape(img.text)}`);
|
|
3689
3690
|
}
|
|
3690
3691
|
echo.push('<br>request: ' + imgResult.request, ', seed: ' + imgResult.seed);
|
|
3691
3692
|
}
|
|
@@ -3736,7 +3737,8 @@ rtn.push(reader.readBCDString());</pre>${JSON.stringify(rtn)}`);
|
|
|
3736
3737
|
<img src="https://img.alicdn.com/imgextra/i3/O1CN01SfG4J41UYn9WNt4X1_!!6000000002530-49-tps-1696-960.webp" style="max-width: 256px;" />
|
|
3737
3738
|
</div>`);
|
|
3738
3739
|
for (const img of imgResult.list) {
|
|
3739
|
-
|
|
3740
|
+
const url = img.url.startsWith('http') ? img.url : 'data:image/png;base64,' + img.url;
|
|
3741
|
+
echo.push(`<div><img src="${lText.htmlescape(url)}" style="max-width: 512px;" /></div>${lText.htmlescape(img.text || prompt)}`);
|
|
3740
3742
|
}
|
|
3741
3743
|
echo.push('<br>request: ' + imgResult.request, ', seed: ' + imgResult.seed);
|
|
3742
3744
|
}
|
|
@@ -11,5 +11,5 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
11
11
|
import { useState } from 'react';
|
|
12
12
|
export default function HelloPage({ _urlStc, _staticVer, greeting }) {
|
|
13
13
|
const [count, setCount] = useState(0);
|
|
14
|
-
return (_jsxs("html", { lang: "zh-CN", children: [_jsxs("head", { children: [_jsx("meta", { charSet: "UTF-8" }), _jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1.0" }), _jsx("title", { children: "Hello - Kebab" }), _jsx("link", { href: `${_urlStc}view/hello.page.css?v=${_staticVer}`, rel: "stylesheet" })] }), _jsx("body", { children: _jsxs("div", { className: "p-8", children: [_jsx("h1", { className: "text-2xl font-bold mb-4", children: greeting }), _jsx("p", { className: "text-slate-500 mb-4", children: "\u70B9\u51FB\u6309\u94AE\u6D4B\u8BD5\u5BA2\u6237\u7AEF\u6C34\u5408\u662F\u5426\u6B63\u5E38\u5DE5\u4F5C\uFF1A" }), _jsxs("div", { className: "flex items-center gap-4", children: [_jsx("button", { onClick: () => setCount(c => c - 1), className: "w-10 h-10 rounded-lg bg-slate-200 hover:bg-slate-300 font-bold text-xl cursor-pointer", children: "-" }), _jsx("span", { className: "w-10 text-center tabular-nums text-lg", children: count }), _jsx("button", { onClick: () => setCount(c => c + 1), className: "w-10 h-10 rounded-lg bg-blue-500 hover:bg-blue-600 text-white font-bold text-xl cursor-pointer", children: "+" })] })] }) })] }));
|
|
14
|
+
return (_jsxs("html", { lang: "zh-CN", children: [_jsxs("head", { children: [_jsx("meta", { charSet: "UTF-8" }), _jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1.0" }), _jsx("title", { children: "Hello - Kebab" }), _jsx("link", { href: `${_urlStc}view/hello.page.css?v=${_staticVer}`, rel: "stylesheet" })] }), _jsx("body", { children: _jsxs("div", { className: "p-8", children: [_jsx("h1", { className: "text-2xl font-bold mb-4", children: greeting }), _jsx("p", { className: "text-slate-500 mb-4", children: "\u70B9\u51FB\u6309\u94AE\u6D4B\u8BD5\u5BA2\u6237\u7AEF\u6C34\u5408\u662F\u5426\u6B63\u5E38\u5DE5\u4F5C\uFF1A" }), _jsxs("div", { className: "flex items-center gap-4", children: [_jsx("button", { onClick: () => { setCount(c => c - 1); }, className: "w-10 h-10 rounded-lg bg-slate-200 hover:bg-slate-300 font-bold text-xl cursor-pointer", children: "-" }), _jsx("span", { className: "w-10 text-center tabular-nums text-lg", children: count }), _jsx("button", { onClick: () => { setCount(c => c + 1); }, className: "w-10 h-10 rounded-lg bg-blue-500 hover:bg-blue-600 text-white font-bold text-xl cursor-pointer", children: "+" })] })] }) })] }));
|
|
15
15
|
}
|
|
@@ -33,12 +33,12 @@ export default function HelloPage({ _urlStc, _staticVer, greeting }: IProps): Re
|
|
|
33
33
|
<p className="text-slate-500 mb-4">点击按钮测试客户端水合是否正常工作:</p>
|
|
34
34
|
<div className="flex items-center gap-4">
|
|
35
35
|
<button
|
|
36
|
-
onClick={() => setCount(c => c - 1)}
|
|
36
|
+
onClick={() => { setCount(c => c - 1); }}
|
|
37
37
|
className="w-10 h-10 rounded-lg bg-slate-200 hover:bg-slate-300 font-bold text-xl cursor-pointer"
|
|
38
38
|
>-</button>
|
|
39
39
|
<span className="w-10 text-center tabular-nums text-lg">{count}</span>
|
|
40
40
|
<button
|
|
41
|
-
onClick={() => setCount(c => c + 1)}
|
|
41
|
+
onClick={() => { setCount(c => c + 1); }}
|
|
42
42
|
className="w-10 h-10 rounded-lg bg-blue-500 hover:bg-blue-600 text-white font-bold text-xl cursor-pointer"
|
|
43
43
|
>+</button>
|
|
44
44
|
</div>
|
|
@@ -65,7 +65,7 @@ function PageHome({ serverTime, node }) {
|
|
|
65
65
|
function PageAbout() {
|
|
66
66
|
const navigate = useNavigate();
|
|
67
67
|
const location = useLocation();
|
|
68
|
-
return (_jsxs("div", { className: "space-y-4", children: [_jsxs("p", { className: "text-slate-600 text-sm", children: ["About page: demonstrates ", _jsx("code", { className: "bg-slate-100 px-1 rounded", children: "useNavigate()" }), " programmatic navigation."] }), _jsxs("div", { className: "bg-slate-50 rounded-lg p-3 text-sm", children: [_jsx("div", { className: "text-slate-500 text-xs mb-1", children: "Current pathname" }), _jsx("div", { className: "font-mono text-slate-800", children: location.pathname })] }), _jsxs("div", { className: "flex gap-2 flex-wrap", children: [_jsx("button", { onClick: () => navigate('/'), 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", children: "Back to Home" }), _jsx("button", { onClick: () => navigate('/user/42'), 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", children: "Go to User #42" })] })] }));
|
|
68
|
+
return (_jsxs("div", { className: "space-y-4", children: [_jsxs("p", { className: "text-slate-600 text-sm", children: ["About page: demonstrates ", _jsx("code", { className: "bg-slate-100 px-1 rounded", children: "useNavigate()" }), " programmatic navigation."] }), _jsxs("div", { className: "bg-slate-50 rounded-lg p-3 text-sm", children: [_jsx("div", { className: "text-slate-500 text-xs mb-1", children: "Current pathname" }), _jsx("div", { className: "font-mono text-slate-800", children: location.pathname })] }), _jsxs("div", { className: "flex gap-2 flex-wrap", children: [_jsx("button", { onClick: () => { Promise.resolve(navigate('/')).catch(() => { }); }, 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", children: "Back to Home" }), _jsx("button", { onClick: () => { Promise.resolve(navigate('/user/42')).catch(() => { }); }, 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", children: "Go to User #42" })] })] }));
|
|
69
69
|
}
|
|
70
70
|
/** --- 用户列表页 --- */
|
|
71
71
|
function PageUsers({ users: initialUsers, urlBase }) {
|
|
@@ -86,7 +86,7 @@ function PageUsers({ users: initialUsers, urlBase }) {
|
|
|
86
86
|
}
|
|
87
87
|
setLoading(false);
|
|
88
88
|
})
|
|
89
|
-
.catch(() => setLoading(false));
|
|
89
|
+
.catch(() => { setLoading(false); });
|
|
90
90
|
}, []);
|
|
91
91
|
return (_jsxs("div", { className: "space-y-4", children: [_jsx("p", { className: "text-slate-600 text-sm", children: "User list \u2014 click to view details (with nested /profile route)." }), _jsxs("div", { className: "bg-slate-50 rounded-lg p-3 text-sm", children: [_jsx("div", { className: "text-slate-500 text-xs mb-1", children: "Current pathname" }), _jsx("div", { className: "font-mono text-slate-800", children: location.pathname })] }), loading && _jsx("p", { className: "text-slate-400 text-sm", children: "Loading..." }), users && (_jsx("ul", { className: "space-y-2", children: users.map(u => (_jsx("li", { className: "flex items-center gap-3", children: _jsxs(Link, { to: `/user/${u.id}`, 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", children: [u.name, " (id=", u.id, ") \u2014 /user/", u.id] }) }, u.id))) }))] }));
|
|
92
92
|
}
|
|
@@ -114,9 +114,9 @@ function PageUserDetail({ user: initialUser, urlBase }) {
|
|
|
114
114
|
}
|
|
115
115
|
setLoading(false);
|
|
116
116
|
})
|
|
117
|
-
.catch(() => setLoading(false));
|
|
117
|
+
.catch(() => { setLoading(false); });
|
|
118
118
|
}, [id]);
|
|
119
|
-
return (_jsxs("div", { className: "space-y-4", children: [loading && _jsx("p", { className: "text-slate-400 text-sm", children: "Loading..." }), user && (_jsxs("p", { className: "text-slate-700 text-sm font-medium", children: ["User Detail: ", _jsx("code", { className: "bg-slate-100 px-1.5 rounded font-mono", children: user.name }), "\u00A0", _jsxs("span", { className: "text-slate-400 text-xs", children: ["(id=\"", id, "\", email=", user.email, ")"] })] })), _jsxs("div", { className: "bg-slate-50 rounded-lg p-3 text-sm", children: [_jsx("div", { className: "text-slate-500 text-xs mb-1", children: "Current pathname" }), _jsx("div", { className: "font-mono text-slate-800", children: location.pathname })] }), _jsxs("div", { className: "border border-dashed border-slate-300 rounded-lg p-4", children: [_jsxs("p", { className: "text-slate-500 text-xs mb-3", children: ["Nested route ", _jsx("code", { className: "bg-slate-100 px-1 rounded", children: "/user/:id/profile" }), " (rendered via Outlet):"] }), _jsx(Outlet, {}), location.pathname === `/user/${id}` && (_jsx(Link, { to: `/user/${id}/profile`, 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", children: "View Profile" }))] }), _jsx("button", { onClick: () => navigate('/user'), 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", children: "Back to Users" })] }));
|
|
119
|
+
return (_jsxs("div", { className: "space-y-4", children: [loading && _jsx("p", { className: "text-slate-400 text-sm", children: "Loading..." }), user && (_jsxs("p", { className: "text-slate-700 text-sm font-medium", children: ["User Detail: ", _jsx("code", { className: "bg-slate-100 px-1.5 rounded font-mono", children: user.name }), "\u00A0", _jsxs("span", { className: "text-slate-400 text-xs", children: ["(id=\"", id, "\", email=", user.email, ")"] })] })), _jsxs("div", { className: "bg-slate-50 rounded-lg p-3 text-sm", children: [_jsx("div", { className: "text-slate-500 text-xs mb-1", children: "Current pathname" }), _jsx("div", { className: "font-mono text-slate-800", children: location.pathname })] }), _jsxs("div", { className: "border border-dashed border-slate-300 rounded-lg p-4", children: [_jsxs("p", { className: "text-slate-500 text-xs mb-3", children: ["Nested route ", _jsx("code", { className: "bg-slate-100 px-1 rounded", children: "/user/:id/profile" }), " (rendered via Outlet):"] }), _jsx(Outlet, {}), location.pathname === `/user/${id ?? ''}` && (_jsx(Link, { to: `/user/${id ?? ''}/profile`, 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", children: "View Profile" }))] }), _jsx("button", { onClick: () => { Promise.resolve(navigate('/user')).catch(() => { }); }, 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", children: "Back to Users" })] }));
|
|
120
120
|
}
|
|
121
121
|
/** --- 嵌套子路由:用户 Profile --- */
|
|
122
122
|
function PageUserProfile() {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* --- Kebab React BrowserRouter 全页面示例组件 ---
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* node ./source/main build -d source/www/example/stc
|
|
5
5
|
*
|
|
6
6
|
* 【特性】
|
|
@@ -54,7 +54,7 @@ interface IProps {
|
|
|
54
54
|
// --- 基础控件 ---
|
|
55
55
|
|
|
56
56
|
/** --- 卡片容器 --- */
|
|
57
|
-
function Card({ children, className = '' }: { 'children': React.ReactNode; 'className'?: string }) {
|
|
57
|
+
function Card({ children, className = '' }: { 'children': React.ReactNode; 'className'?: string; }) {
|
|
58
58
|
return (
|
|
59
59
|
<div className={`bg-white rounded-xl shadow-sm border border-slate-200 p-6 ${className}`}>
|
|
60
60
|
{children}
|
|
@@ -84,7 +84,7 @@ function Badge({ children, variant = 'default' }: {
|
|
|
84
84
|
/** --- 顶部导航栏,NavLink 自动高亮当前路由 --- */
|
|
85
85
|
function NavBar() {
|
|
86
86
|
/** --- NavLink className 回调:激活时高亮 --- */
|
|
87
|
-
const cls = ({ isActive }: { 'isActive': boolean }): string =>
|
|
87
|
+
const cls = ({ isActive }: { 'isActive': boolean; }): string =>
|
|
88
88
|
isActive
|
|
89
89
|
? 'px-3 py-1.5 rounded-lg bg-blue-500 text-white text-sm font-medium transition-colors'
|
|
90
90
|
: 'px-3 py-1.5 rounded-lg text-slate-600 hover:bg-slate-100 text-sm font-medium transition-colors';
|
|
@@ -100,7 +100,7 @@ function NavBar() {
|
|
|
100
100
|
// --- 路由页面 ---
|
|
101
101
|
|
|
102
102
|
/** --- 首页 --- */
|
|
103
|
-
function PageHome({ serverTime, node }: { 'serverTime': string; 'node': string }) {
|
|
103
|
+
function PageHome({ serverTime, node }: { 'serverTime': string; 'node': string; }) {
|
|
104
104
|
const location = useLocation();
|
|
105
105
|
const [hydrated, setHydrated] = useState(false);
|
|
106
106
|
useEffect(() => {
|
|
@@ -162,13 +162,13 @@ function PageAbout() {
|
|
|
162
162
|
</div>
|
|
163
163
|
<div className="flex gap-2 flex-wrap">
|
|
164
164
|
<button
|
|
165
|
-
onClick={() => navigate('/')}
|
|
165
|
+
onClick={() => { Promise.resolve(navigate('/')).catch(() => {}); }}
|
|
166
166
|
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"
|
|
167
167
|
>
|
|
168
168
|
Back to Home
|
|
169
169
|
</button>
|
|
170
170
|
<button
|
|
171
|
-
onClick={() => navigate('/user/42')}
|
|
171
|
+
onClick={() => { Promise.resolve(navigate('/user/42')).catch(() => {}); }}
|
|
172
172
|
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"
|
|
173
173
|
>
|
|
174
174
|
Go to User #42
|
|
@@ -179,7 +179,7 @@ function PageAbout() {
|
|
|
179
179
|
}
|
|
180
180
|
|
|
181
181
|
/** --- 用户列表页 --- */
|
|
182
|
-
function PageUsers({ users: initialUsers, urlBase }: { 'users'?: IUser[]; 'urlBase': string }) {
|
|
182
|
+
function PageUsers({ users: initialUsers, urlBase }: { 'users'?: IUser[]; 'urlBase': string; }) {
|
|
183
183
|
const location = useLocation();
|
|
184
184
|
const [users, setUsers] = useState(initialUsers);
|
|
185
185
|
const [loading, setLoading] = useState(false);
|
|
@@ -197,7 +197,7 @@ function PageUsers({ users: initialUsers, urlBase }: { 'users'?: IUser[]; 'urlBa
|
|
|
197
197
|
}
|
|
198
198
|
setLoading(false);
|
|
199
199
|
})
|
|
200
|
-
.catch(() => setLoading(false));
|
|
200
|
+
.catch(() => { setLoading(false); });
|
|
201
201
|
}, []);
|
|
202
202
|
return (
|
|
203
203
|
<div className="space-y-4">
|
|
@@ -229,8 +229,8 @@ function PageUsers({ users: initialUsers, urlBase }: { 'users'?: IUser[]; 'urlBa
|
|
|
229
229
|
* --- 用户详情页(含嵌套路由 Outlet) ---
|
|
230
230
|
* 子路由 /profile 通过 <Outlet /> 渲染在此处
|
|
231
231
|
*/
|
|
232
|
-
function PageUserDetail({ user: initialUser, urlBase }: { 'user'?: IUser; 'urlBase': string }) {
|
|
233
|
-
const { id } = useParams<{ 'id': string }>();
|
|
232
|
+
function PageUserDetail({ user: initialUser, urlBase }: { 'user'?: IUser; 'urlBase': string; }) {
|
|
233
|
+
const { id } = useParams<{ 'id': string; }>();
|
|
234
234
|
const navigate = useNavigate();
|
|
235
235
|
const location = useLocation();
|
|
236
236
|
const [user, setUser] = useState<IUser | undefined>(
|
|
@@ -251,7 +251,7 @@ function PageUserDetail({ user: initialUser, urlBase }: { 'user'?: IUser; 'urlBa
|
|
|
251
251
|
}
|
|
252
252
|
setLoading(false);
|
|
253
253
|
})
|
|
254
|
-
.catch(() => setLoading(false));
|
|
254
|
+
.catch(() => { setLoading(false); });
|
|
255
255
|
}, [id]);
|
|
256
256
|
return (
|
|
257
257
|
<div className="space-y-4">
|
|
@@ -272,9 +272,9 @@ function PageUserDetail({ user: initialUser, urlBase }: { 'user'?: IUser; 'urlBa
|
|
|
272
272
|
Nested route <code className="bg-slate-100 px-1 rounded">/user/:id/profile</code> (rendered via Outlet):
|
|
273
273
|
</p>
|
|
274
274
|
<Outlet />
|
|
275
|
-
{location.pathname === `/user/${id}` && (
|
|
275
|
+
{location.pathname === `/user/${id ?? ''}` && (
|
|
276
276
|
<Link
|
|
277
|
-
to={`/user/${id}/profile`}
|
|
277
|
+
to={`/user/${id ?? ''}/profile`}
|
|
278
278
|
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"
|
|
279
279
|
>
|
|
280
280
|
View Profile
|
|
@@ -282,7 +282,7 @@ function PageUserDetail({ user: initialUser, urlBase }: { 'user'?: IUser; 'urlBa
|
|
|
282
282
|
)}
|
|
283
283
|
</div>
|
|
284
284
|
<button
|
|
285
|
-
onClick={() => navigate('/user')}
|
|
285
|
+
onClick={() => { Promise.resolve(navigate('/user')).catch(() => {}); }}
|
|
286
286
|
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"
|
|
287
287
|
>
|
|
288
288
|
Back to Users
|
|
@@ -293,7 +293,7 @@ function PageUserDetail({ user: initialUser, urlBase }: { 'user'?: IUser; 'urlBa
|
|
|
293
293
|
|
|
294
294
|
/** --- 嵌套子路由:用户 Profile --- */
|
|
295
295
|
function PageUserProfile() {
|
|
296
|
-
const { id } = useParams<{ 'id': string }>();
|
|
296
|
+
const { id } = useParams<{ 'id': string; }>();
|
|
297
297
|
const location = useLocation();
|
|
298
298
|
return (
|
|
299
299
|
<div className="bg-blue-50 rounded-lg p-3 mb-3 space-y-2 text-sm">
|
|
@@ -335,7 +335,6 @@ export default function ReactRouterPage({
|
|
|
335
335
|
<head>
|
|
336
336
|
<meta charSet="UTF-8" />
|
|
337
337
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
338
|
-
{/* eslint-disable-next-line @typescript-eslint/naming-convention */}
|
|
339
338
|
<title suppressHydrationWarning>{title}</title>
|
|
340
339
|
{/* --- import map 由框架自动注入在此标签前,无需手动添加 --- */}
|
|
341
340
|
{/* --- dev: 需先执行 node ./source/main build -d source/www/example/stc 生成 CSS --- */}
|
|
@@ -73,13 +73,13 @@ function RouterHome() {
|
|
|
73
73
|
/** --- 路由 About 页 --- */
|
|
74
74
|
function RouterAbout() {
|
|
75
75
|
const navigate = useNavigate();
|
|
76
|
-
return (_jsxs("div", { children: [_jsxs("p", { className: "text-slate-700 text-sm font-medium mb-3", children: ["Current route: ", _jsx("code", { className: "bg-slate-100 px-1 rounded", children: "/about" })] }), _jsx("p", { className: "text-slate-500 text-xs mb-3", children: "useNavigate() programmatic navigation (without Link component):" }), _jsxs("div", { className: "flex gap-2", children: [_jsx("button", { onClick: () => navigate('/'), 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", children: "\u2190 Back to /" }), _jsx("button", { onClick: () => navigate('/user/99'), 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", children: "\u2192 /user/99" })] })] }));
|
|
76
|
+
return (_jsxs("div", { children: [_jsxs("p", { className: "text-slate-700 text-sm font-medium mb-3", children: ["Current route: ", _jsx("code", { className: "bg-slate-100 px-1 rounded", children: "/about" })] }), _jsx("p", { className: "text-slate-500 text-xs mb-3", children: "useNavigate() programmatic navigation (without Link component):" }), _jsxs("div", { className: "flex gap-2", children: [_jsx("button", { onClick: () => { Promise.resolve(navigate('/')).catch(() => { }); }, 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", children: "\u2190 Back to /" }), _jsx("button", { onClick: () => { Promise.resolve(navigate('/user/99')).catch(() => { }); }, 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", children: "\u2192 /user/99" })] })] }));
|
|
77
77
|
}
|
|
78
78
|
/** --- 路由 User 动态参数页 --- */
|
|
79
79
|
function RouterUser() {
|
|
80
80
|
const { id } = useParams();
|
|
81
81
|
const navigate = useNavigate();
|
|
82
|
-
return (_jsxs("div", { children: [_jsxs("p", { className: "text-slate-700 text-sm font-medium mb-1", children: ["Current route: ", _jsx("code", { className: "bg-slate-100 px-1 rounded", children: "/user/:id" })] }), _jsxs("p", { className: "text-slate-500 text-xs mb-3", children: ["useParams() dynamic segment: ", _jsxs("code", { className: "bg-slate-100 px-1 rounded", children: ["id = \"", id, "\""] })] }), _jsx("button", { onClick: () => navigate('/'), 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", children: "\u2190 Back to /" })] }));
|
|
82
|
+
return (_jsxs("div", { children: [_jsxs("p", { className: "text-slate-700 text-sm font-medium mb-1", children: ["Current route: ", _jsx("code", { className: "bg-slate-100 px-1 rounded", children: "/user/:id" })] }), _jsxs("p", { className: "text-slate-500 text-xs mb-3", children: ["useParams() dynamic segment: ", _jsxs("code", { className: "bg-slate-100 px-1 rounded", children: ["id = \"", id, "\""] })] }), _jsx("button", { onClick: () => { Promise.resolve(navigate('/')).catch(() => { }); }, 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", children: "\u2190 Back to /" })] }));
|
|
83
83
|
}
|
|
84
84
|
// ─── shadcn/ui 演示区 ────────────────────────────────────────────────────────
|
|
85
85
|
/**
|
|
@@ -102,7 +102,7 @@ function ShadcnDemo() {
|
|
|
102
102
|
e.preventDefault();
|
|
103
103
|
setSubmitted(true);
|
|
104
104
|
}
|
|
105
|
-
return (_jsxs("div", { className: "space-y-4", children: [_jsxs(Card, { children: [_jsx("h2", { className: "font-semibold text-slate-900 mb-2", children: "shadcn/ui Components" }), _jsxs("p", { className: "text-slate-500 text-xs leading-relaxed", children: ["All components below are imported from ", _jsx("code", { className: "bg-slate-100 px-1 rounded", children: "stc/lib/ui/" }), ", following the same ", _jsx("code", { className: "bg-slate-100 px-1 rounded", children: "components/ui/" }), " structure recommended by shadcn/ui. Built on Radix UI primitives, styled with Tailwind CSS. In dev mode, the framework auto-scans all bare imports and generates the import map automatically."] })] }), _jsxs(Card, { children: [_jsx("h2", { className: "font-semibold text-slate-900 mb-4", children: "Label + Input" }), _jsxs("div", { className: "grid gap-4", children: [_jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { htmlFor: nameId, children: "Username" }), _jsx(Input, { id: nameId, type: "text", placeholder: "Enter your username", value: name, onChange: e => setName(e.target.value) })] }), _jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { htmlFor: emailId, children: "Email" }), _jsx(Input, { id: emailId, type: "email", placeholder: "you@example.com", value: email, onChange: e => setEmail(e.target.value) })] })] }), _jsx("p", { className: "text-xs text-slate-400 mt-3", children: "Click the Label text to focus the corresponding input (Radix UI a11y semantics)" })] }), _jsxs(Card, { children: [_jsx("h2", { className: "font-semibold text-slate-900 mb-4", children: "Checkbox" }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Checkbox, { id: agreeId, checked: agree, onCheckedChange: setAgree }), _jsx(Label, { htmlFor: agreeId, children: "I have read and agree to the Terms of Service" })] }), _jsxs("p", { className: "text-xs text-slate-400 mt-3", children: ["Value: ", _jsx("code", { className: "bg-slate-100 px-1 rounded", children: String(agree) }), "\u00A0(supports ", _jsx("code", { className: "bg-slate-100 px-1 rounded", children: "true | false | 'indeterminate'" }), " tri-state)"] })] }), _jsxs(Card, { children: [_jsx("h2", { className: "font-semibold text-slate-900 mb-4", children: "Switch" }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { id: newsletterId, checked: newsletter, onCheckedChange: setNewsletter }), _jsx(Label, { htmlFor: newsletterId, children: "Subscribe to product updates" })] }), _jsxs("p", { className: "text-xs text-slate-400 mt-3", children: ["Value: ", _jsx("code", { className: "bg-slate-100 px-1 rounded", children: newsletter ? 'true' : 'false' })] })] }), _jsxs(Card, { children: [_jsx("h2", { className: "font-semibold text-slate-900 mb-4", children: "Combined Form (Submit Demo)" }), submitted ? (_jsxs("div", { className: "space-y-2 text-sm", children: [_jsx("p", { className: "text-green-700 font-medium", children: "\u2713 Submitted! Received data:" }), _jsx("pre", { className: "bg-slate-50 border border-slate-200 rounded-lg p-3 text-xs text-slate-700 overflow-auto", children: JSON.stringify({ name, email, agree: Boolean(agree), newsletter }, null, 2) }), _jsx("button", { onClick: () => setSubmitted(false), className: "text-xs text-blue-500 hover:underline cursor-pointer", children: "Reset" })] })) : (_jsxs("form", { onSubmit: handleSubmit, className: "space-y-4", children: [_jsxs("div", { className: "grid gap-1.5", children: [_jsxs(Label, { htmlFor: `${nameId}-form`, children: ["Username ", _jsx("span", { className: "text-red-500", children: "*" })] }), _jsx(Input, { id: `${nameId}-form`, required: true, placeholder: "At least 2 characters", value: name, onChange: e => setName(e.target.value) })] }), _jsxs("div", { className: "grid gap-1.5", children: [_jsxs(Label, { htmlFor: `${emailId}-form`, children: ["Email ", _jsx("span", { className: "text-red-500", children: "*" })] }), _jsx(Input, { id: `${emailId}-form`, type: "email", required: true, placeholder: "you@example.com", value: email, onChange: e => setEmail(e.target.value) })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Checkbox, { id: `${agreeId}-form`, checked: agree, onCheckedChange: setAgree, required: true }), _jsx(Label, { htmlFor: `${agreeId}-form`, children: "Agree to terms" })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { id: `${newsletterId}-form`, checked: newsletter, onCheckedChange: setNewsletter }), _jsx(Label, { htmlFor: `${newsletterId}-form`, children: "Subscribe" })] }), _jsx("button", { type: "submit", className: "inline-flex items-center px-4 py-2 rounded-lg bg-blue-500 hover:bg-blue-600 text-white text-sm font-medium cursor-pointer", children: "Submit" })] }))] })] }));
|
|
105
|
+
return (_jsxs("div", { className: "space-y-4", children: [_jsxs(Card, { children: [_jsx("h2", { className: "font-semibold text-slate-900 mb-2", children: "shadcn/ui Components" }), _jsxs("p", { className: "text-slate-500 text-xs leading-relaxed", children: ["All components below are imported from ", _jsx("code", { className: "bg-slate-100 px-1 rounded", children: "stc/lib/ui/" }), ", following the same ", _jsx("code", { className: "bg-slate-100 px-1 rounded", children: "components/ui/" }), " structure recommended by shadcn/ui. Built on Radix UI primitives, styled with Tailwind CSS. In dev mode, the framework auto-scans all bare imports and generates the import map automatically."] })] }), _jsxs(Card, { children: [_jsx("h2", { className: "font-semibold text-slate-900 mb-4", children: "Label + Input" }), _jsxs("div", { className: "grid gap-4", children: [_jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { htmlFor: nameId, children: "Username" }), _jsx(Input, { id: nameId, type: "text", placeholder: "Enter your username", value: name, onChange: e => { setName(e.target.value); } })] }), _jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { htmlFor: emailId, children: "Email" }), _jsx(Input, { id: emailId, type: "email", placeholder: "you@example.com", value: email, onChange: e => { setEmail(e.target.value); } })] })] }), _jsx("p", { className: "text-xs text-slate-400 mt-3", children: "Click the Label text to focus the corresponding input (Radix UI a11y semantics)" })] }), _jsxs(Card, { children: [_jsx("h2", { className: "font-semibold text-slate-900 mb-4", children: "Checkbox" }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Checkbox, { id: agreeId, checked: agree, onCheckedChange: setAgree }), _jsx(Label, { htmlFor: agreeId, children: "I have read and agree to the Terms of Service" })] }), _jsxs("p", { className: "text-xs text-slate-400 mt-3", children: ["Value: ", _jsx("code", { className: "bg-slate-100 px-1 rounded", children: String(agree) }), "\u00A0(supports ", _jsx("code", { className: "bg-slate-100 px-1 rounded", children: "true | false | 'indeterminate'" }), " tri-state)"] })] }), _jsxs(Card, { children: [_jsx("h2", { className: "font-semibold text-slate-900 mb-4", children: "Switch" }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { id: newsletterId, checked: newsletter, onCheckedChange: setNewsletter }), _jsx(Label, { htmlFor: newsletterId, children: "Subscribe to product updates" })] }), _jsxs("p", { className: "text-xs text-slate-400 mt-3", children: ["Value: ", _jsx("code", { className: "bg-slate-100 px-1 rounded", children: newsletter ? 'true' : 'false' })] })] }), _jsxs(Card, { children: [_jsx("h2", { className: "font-semibold text-slate-900 mb-4", children: "Combined Form (Submit Demo)" }), submitted ? (_jsxs("div", { className: "space-y-2 text-sm", children: [_jsx("p", { className: "text-green-700 font-medium", children: "\u2713 Submitted! Received data:" }), _jsx("pre", { className: "bg-slate-50 border border-slate-200 rounded-lg p-3 text-xs text-slate-700 overflow-auto", children: JSON.stringify({ name, email, agree: Boolean(agree), newsletter }, null, 2) }), _jsx("button", { onClick: () => { setSubmitted(false); }, className: "text-xs text-blue-500 hover:underline cursor-pointer", children: "Reset" })] })) : (_jsxs("form", { onSubmit: handleSubmit, className: "space-y-4", children: [_jsxs("div", { className: "grid gap-1.5", children: [_jsxs(Label, { htmlFor: `${nameId}-form`, children: ["Username ", _jsx("span", { className: "text-red-500", children: "*" })] }), _jsx(Input, { id: `${nameId}-form`, required: true, placeholder: "At least 2 characters", value: name, onChange: e => { setName(e.target.value); } })] }), _jsxs("div", { className: "grid gap-1.5", children: [_jsxs(Label, { htmlFor: `${emailId}-form`, children: ["Email ", _jsx("span", { className: "text-red-500", children: "*" })] }), _jsx(Input, { id: `${emailId}-form`, type: "email", required: true, placeholder: "you@example.com", value: email, onChange: e => { setEmail(e.target.value); } })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Checkbox, { id: `${agreeId}-form`, checked: agree, onCheckedChange: setAgree, required: true }), _jsx(Label, { htmlFor: `${agreeId}-form`, children: "Agree to terms" })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { id: `${newsletterId}-form`, checked: newsletter, onCheckedChange: setNewsletter }), _jsx(Label, { htmlFor: `${newsletterId}-form`, children: "Subscribe" })] }), _jsx("button", { type: "submit", className: "inline-flex items-center px-4 py-2 rounded-lg bg-blue-500 hover:bg-blue-600 text-white text-sm font-medium cursor-pointer", children: "Submit" })] }))] })] }));
|
|
106
106
|
}
|
|
107
107
|
// ─── 页面主组件 ────────────────────────────────────────────────────────────────
|
|
108
108
|
/** --- Kebab React 全页面演示 --- */
|
|
@@ -136,10 +136,10 @@ export default function ReactPage({ title, serverTime, node, _urlBase, _urlStc,
|
|
|
136
136
|
setIsFetching(false);
|
|
137
137
|
});
|
|
138
138
|
}
|
|
139
|
-
return (_jsxs("html", { lang: "en", children: [_jsxs("head", { children: [_jsx("meta", { charSet: "UTF-8" }), _jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1.0" }), _jsx("title", { children: title }), _jsx("link", { href: `${_urlStc}view/react.page.css?v=${_staticVer}`, rel: "stylesheet" })] }), _jsx("body", { className: "bg-slate-50 min-h-screen font-sans", children: _jsxs("div", { className: "max-w-3xl mx-auto px-4 py-10 space-y-6", children: [_jsxs("div", { children: [_jsxs("div", { className: "flex gap-2 mb-2", children: [_jsx(Badge, { children: "SSR \u00B7 Kebab" }), _jsx(Badge, { variant: hydrated ? 'success' : 'warn', children: hydrated ? 'Hydrated ✓' : 'Rendering...' })] }), _jsx("h1", { className: "text-2xl font-bold text-slate-900", children: title }), _jsxs("p", { className: "text-slate-500 text-sm mt-1", children: [serverTime, " \u00B7 ", node] })] }), _jsx("div", { className: "flex gap-1 bg-slate-100 p-1 rounded-lg w-fit", children: ['overview', 'routing', 'fetch', 'shadcn'].map(t => (_jsx("button", { onClick: () => setTab(t), className: [
|
|
139
|
+
return (_jsxs("html", { lang: "en", children: [_jsxs("head", { children: [_jsx("meta", { charSet: "UTF-8" }), _jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1.0" }), _jsx("title", { children: title }), _jsx("link", { href: `${_urlStc}view/react.page.css?v=${_staticVer}`, rel: "stylesheet" })] }), _jsx("body", { className: "bg-slate-50 min-h-screen font-sans", children: _jsxs("div", { className: "max-w-3xl mx-auto px-4 py-10 space-y-6", children: [_jsxs("div", { children: [_jsxs("div", { className: "flex gap-2 mb-2", children: [_jsx(Badge, { children: "SSR \u00B7 Kebab" }), _jsx(Badge, { variant: hydrated ? 'success' : 'warn', children: hydrated ? 'Hydrated ✓' : 'Rendering...' })] }), _jsx("h1", { className: "text-2xl font-bold text-slate-900", children: title }), _jsxs("p", { className: "text-slate-500 text-sm mt-1", children: [serverTime, " \u00B7 ", node] })] }), _jsx("div", { className: "flex gap-1 bg-slate-100 p-1 rounded-lg w-fit", children: ['overview', 'routing', 'fetch', 'shadcn'].map(t => (_jsx("button", { onClick: () => { setTab(t); }, className: [
|
|
140
140
|
'px-4 py-2 rounded-md text-sm font-medium transition-colors cursor-pointer',
|
|
141
141
|
tab === t ? 'bg-white shadow text-slate-900' : 'text-slate-500 hover:text-slate-900',
|
|
142
|
-
].join(' '), children: t.charAt(0).toUpperCase() + t.slice(1) }, t))) }), tab === 'overview' && (_jsxs("div", { className: "space-y-4", children: [_jsxs(Card, { children: [_jsx("h2", { className: "font-semibold text-slate-900 mb-1", children: "Counter\uFF08useState + hydration\uFF09" }), _jsx("p", { className: "text-slate-500 text-xs mb-4", children: "SSR renders with initial value 0; props are serialized as inline JSON. After hydration the state becomes interactive \u2014 click the buttons to test:" }), _jsxs("div", { className: "flex items-center gap-4", children: [_jsx("button", { onClick: () => setCount(c => c - 1), className: "w-10 h-10 rounded-lg border border-slate-300 hover:bg-slate-50 font-bold text-slate-700 text-xl cursor-pointer", children: "\u2212" }), _jsx("span", { className: "text-3xl font-bold text-slate-900 w-10 text-center tabular-nums", children: count }), _jsx("button", { onClick: () => setCount(c => c + 1), className: "w-10 h-10 rounded-lg bg-blue-500 hover:bg-blue-600 text-white font-bold text-xl cursor-pointer", children: "+" })] })] }), _jsxs(Card, { children: [_jsx("h2", { className: "font-semibold text-slate-900 mb-3", children: "Server Props (from Ctr method)" }), _jsxs("p", { className: "text-slate-500 text-xs mb-3", children: ["Passed via", ' ', _jsx("code", { className: "bg-slate-100 px-1 rounded", children: "_loadReactPage(path, props)" }), ". The framework auto-injects constants like _urlBase and serializes all props as inline JSON, reused directly during client hydration \u2014 no extra request needed."] }), _jsx("div", { className: "space-y-2", children: [
|
|
142
|
+
].join(' '), children: t.charAt(0).toUpperCase() + t.slice(1) }, t))) }), tab === 'overview' && (_jsxs("div", { className: "space-y-4", children: [_jsxs(Card, { children: [_jsx("h2", { className: "font-semibold text-slate-900 mb-1", children: "Counter\uFF08useState + hydration\uFF09" }), _jsx("p", { className: "text-slate-500 text-xs mb-4", children: "SSR renders with initial value 0; props are serialized as inline JSON. After hydration the state becomes interactive \u2014 click the buttons to test:" }), _jsxs("div", { className: "flex items-center gap-4", children: [_jsx("button", { onClick: () => { setCount(c => c - 1); }, className: "w-10 h-10 rounded-lg border border-slate-300 hover:bg-slate-50 font-bold text-slate-700 text-xl cursor-pointer", children: "\u2212" }), _jsx("span", { className: "text-3xl font-bold text-slate-900 w-10 text-center tabular-nums", children: count }), _jsx("button", { onClick: () => { setCount(c => c + 1); }, className: "w-10 h-10 rounded-lg bg-blue-500 hover:bg-blue-600 text-white font-bold text-xl cursor-pointer", children: "+" })] })] }), _jsxs(Card, { children: [_jsx("h2", { className: "font-semibold text-slate-900 mb-3", children: "Server Props (from Ctr method)" }), _jsxs("p", { className: "text-slate-500 text-xs mb-3", children: ["Passed via", ' ', _jsx("code", { className: "bg-slate-100 px-1 rounded", children: "_loadReactPage(path, props)" }), ". The framework auto-injects constants like _urlBase and serializes all props as inline JSON, reused directly during client hydration \u2014 no extra request needed."] }), _jsx("div", { className: "space-y-2", children: [
|
|
143
143
|
['serverTime', serverTime],
|
|
144
144
|
['node', node],
|
|
145
145
|
['_urlBase', _urlBase],
|
|
@@ -153,7 +153,7 @@ export default function ReactPage({ title, serverTime, node, _urlBase, _urlStc,
|
|
|
153
153
|
checkbox.tsx # shadcn Checkbox
|
|
154
154
|
switch.tsx # shadcn Switch
|
|
155
155
|
view/
|
|
156
|
-
react-page.tsx # Page component (import + compose)` }), _jsxs("p", { className: "text-slate-500 text-xs mt-3", children: ["Mirrors the shadcn/ui
|
|
156
|
+
react-page.tsx # Page component (import + compose)` }), _jsxs("p", { className: "text-slate-500 text-xs mt-3", children: ["Mirrors the shadcn/ui", ' ', _jsx("code", { className: "bg-slate-100 px-1 rounded", children: "components/ui/" }), " convention. In dev mode, the framework auto-scans imports \u2014 no manual import map configuration needed."] })] })] })), tab === 'routing' && (_jsxs(MemoryRouter, { children: [_jsxs(Card, { children: [_jsx("h2", { className: "font-semibold text-slate-900 mb-1", children: "React Router Demo" }), _jsxs("p", { className: "text-slate-500 text-xs mb-4", children: ["Uses ", _jsx("code", { className: "bg-slate-100 px-1 rounded", children: "MemoryRouter" }), ' ', "to demonstrate route navigation, dynamic params (useParams), and programmatic navigation (useNavigate). Works on both server SSR and client hydration with no extra server configuration."] }), _jsxs(Routes, { children: [_jsx(Route, { path: "/", element: _jsx(RouterHome, {}) }), _jsx(Route, { path: "/about", element: _jsx(RouterAbout, {}) }), _jsx(Route, { path: "/user/:id", element: _jsx(RouterUser, {}) })] })] }), _jsxs(Card, { className: "mt-4", children: [_jsx("h2", { className: "font-semibold text-slate-900 mb-2", children: "Full-Page BrowserRouter Setup" }), _jsx("p", { className: "text-slate-500 text-xs mb-3", children: "To let React Router manage the real URL (e.g. /app, /app/about), replace MemoryRouter with BrowserRouter and route all sub-paths to the same Ctr method on the server:" }), _jsx("pre", { className: "bg-slate-50 border border-slate-200 rounded-lg p-4 text-xs overflow-auto leading-relaxed text-slate-700", children: `// 1. route.json: 将所有子路径路由到同一 Ctr 方法
|
|
157
157
|
{
|
|
158
158
|
"app": "ctr/app@reactPage",
|
|
159
159
|
"app\\/.*": "ctr/app@reactPage"
|
|
@@ -169,5 +169,5 @@ await this._loadReactPage('view/my', { ...props }, {
|
|
|
169
169
|
<Routes>
|
|
170
170
|
<Route path="/" element={<Home />} />
|
|
171
171
|
<Route path="/user/:id" element={<User />} />
|
|
172
|
-
</Routes>` })] })] })), tab === 'shadcn' && (_jsx(ShadcnDemo, {})), tab === 'fetch' && (_jsxs(Card, { children: [_jsx("h2", { className: "font-semibold text-slate-900 mb-3", children: "Client Fetch Demo" }), _jsx("p", { className: "text-slate-500 text-xs mb-4", children: "After hydration, click a button to send a GET request. setState triggers a partial re-render \u2014 no page refresh." }), _jsxs("div", { className: "flex gap-3 flex-wrap", children: [_jsx(Btn, { onClick: () => doFetch(`${_urlBase}test/json?type=4`), disabled: isFetching, children: isFetching ? 'Fetching...' : 'GET /test/json?type=4' }), _jsx(Btn, { outline: true, onClick: () => doFetch(`${_urlBase}test/json?type=2`), disabled: isFetching, children: "GET type=2 (error resp)" })] }), fetchResult !== null ? (_jsx("pre", { className: "mt-4 bg-slate-50 border border-slate-200 rounded-lg p-4 text-xs overflow-auto leading-relaxed text-slate-700", children: fetchResult })) : (_jsx("p", { className: "mt-3 text-slate-400 text-xs", children: "Click a button above to see the response" }))] }))] }) })] }));
|
|
172
|
+
</Routes>` })] })] })), tab === 'shadcn' && (_jsx(ShadcnDemo, {})), tab === 'fetch' && (_jsxs(Card, { children: [_jsx("h2", { className: "font-semibold text-slate-900 mb-3", children: "Client Fetch Demo" }), _jsx("p", { className: "text-slate-500 text-xs mb-4", children: "After hydration, click a button to send a GET request. setState triggers a partial re-render \u2014 no page refresh." }), _jsxs("div", { className: "flex gap-3 flex-wrap", children: [_jsx(Btn, { onClick: () => { doFetch(`${_urlBase}test/json?type=4`); }, disabled: isFetching, children: isFetching ? 'Fetching...' : 'GET /test/json?type=4' }), _jsx(Btn, { outline: true, onClick: () => { doFetch(`${_urlBase}test/json?type=2`); }, disabled: isFetching, children: "GET type=2 (error resp)" })] }), fetchResult !== null ? (_jsx("pre", { className: "mt-4 bg-slate-50 border border-slate-200 rounded-lg p-4 text-xs overflow-auto leading-relaxed text-slate-700", children: fetchResult })) : (_jsx("p", { className: "mt-3 text-slate-400 text-xs", children: "Click a button above to see the response" }))] }))] }) })] }));
|
|
173
173
|
}
|
|
@@ -59,7 +59,7 @@ interface IProps {
|
|
|
59
59
|
// ─── 页面内部组件(仅本页演示使用,无需提取到 lib/)────────────────────────────
|
|
60
60
|
|
|
61
61
|
/** --- 卡片容器 --- */
|
|
62
|
-
function Card({ children, className = '' }: { 'children': ReactNode; 'className'?: string }) {
|
|
62
|
+
function Card({ children, className = '' }: { 'children': ReactNode; 'className'?: string; }) {
|
|
63
63
|
return (
|
|
64
64
|
<div className={`bg-white rounded-xl shadow-sm border border-slate-200 p-6 ${className}`}>
|
|
65
65
|
{children}
|
|
@@ -75,7 +75,7 @@ function Badge({ children, variant = 'default' }: {
|
|
|
75
75
|
const styles = {
|
|
76
76
|
'default': 'bg-blue-100 text-blue-700',
|
|
77
77
|
'success': 'bg-green-100 text-green-700',
|
|
78
|
-
'warn':
|
|
78
|
+
'warn': 'bg-amber-100 text-amber-700',
|
|
79
79
|
};
|
|
80
80
|
return (
|
|
81
81
|
<span className={`inline-flex px-2.5 py-0.5 rounded-md text-xs font-semibold ${styles[variant]}`}>
|
|
@@ -137,13 +137,13 @@ function RouterAbout() {
|
|
|
137
137
|
<p className="text-slate-500 text-xs mb-3">useNavigate() programmatic navigation (without Link component):</p>
|
|
138
138
|
<div className="flex gap-2">
|
|
139
139
|
<button
|
|
140
|
-
onClick={() => navigate('/')}
|
|
140
|
+
onClick={() => { Promise.resolve(navigate('/')).catch(() => {}); }}
|
|
141
141
|
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"
|
|
142
142
|
>
|
|
143
143
|
← Back to /
|
|
144
144
|
</button>
|
|
145
145
|
<button
|
|
146
|
-
onClick={() => navigate('/user/99')}
|
|
146
|
+
onClick={() => { Promise.resolve(navigate('/user/99')).catch(() => {}); }}
|
|
147
147
|
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"
|
|
148
148
|
>
|
|
149
149
|
→ /user/99
|
|
@@ -155,7 +155,7 @@ function RouterAbout() {
|
|
|
155
155
|
|
|
156
156
|
/** --- 路由 User 动态参数页 --- */
|
|
157
157
|
function RouterUser() {
|
|
158
|
-
const { id } = useParams<{ 'id': string }>();
|
|
158
|
+
const { id } = useParams<{ 'id': string; }>();
|
|
159
159
|
const navigate = useNavigate();
|
|
160
160
|
return (
|
|
161
161
|
<div>
|
|
@@ -166,7 +166,7 @@ function RouterUser() {
|
|
|
166
166
|
useParams() dynamic segment: <code className="bg-slate-100 px-1 rounded">id = "{id}"</code>
|
|
167
167
|
</p>
|
|
168
168
|
<button
|
|
169
|
-
onClick={() => navigate('/')}
|
|
169
|
+
onClick={() => { Promise.resolve(navigate('/')).catch(() => {}); }}
|
|
170
170
|
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"
|
|
171
171
|
>
|
|
172
172
|
← Back to /
|
|
@@ -195,7 +195,7 @@ function ShadcnDemo() {
|
|
|
195
195
|
const agreeId = useId();
|
|
196
196
|
const newsletterId = useId();
|
|
197
197
|
|
|
198
|
-
function handleSubmit(e: React.
|
|
198
|
+
function handleSubmit(e: React.SyntheticEvent<HTMLFormElement>): void {
|
|
199
199
|
e.preventDefault();
|
|
200
200
|
setSubmitted(true);
|
|
201
201
|
}
|
|
@@ -226,7 +226,7 @@ function ShadcnDemo() {
|
|
|
226
226
|
type="text"
|
|
227
227
|
placeholder="Enter your username"
|
|
228
228
|
value={name}
|
|
229
|
-
onChange={e => setName(e.target.value)}
|
|
229
|
+
onChange={e => { setName(e.target.value); }}
|
|
230
230
|
/>
|
|
231
231
|
</div>
|
|
232
232
|
<div className="grid gap-1.5">
|
|
@@ -236,7 +236,7 @@ function ShadcnDemo() {
|
|
|
236
236
|
type="email"
|
|
237
237
|
placeholder="you@example.com"
|
|
238
238
|
value={email}
|
|
239
|
-
onChange={e => setEmail(e.target.value)}
|
|
239
|
+
onChange={e => { setEmail(e.target.value); }}
|
|
240
240
|
/>
|
|
241
241
|
</div>
|
|
242
242
|
</div>
|
|
@@ -290,7 +290,7 @@ function ShadcnDemo() {
|
|
|
290
290
|
2
|
|
291
291
|
)}</pre>
|
|
292
292
|
<button
|
|
293
|
-
onClick={() => setSubmitted(false)}
|
|
293
|
+
onClick={() => { setSubmitted(false); }}
|
|
294
294
|
className="text-xs text-blue-500 hover:underline cursor-pointer"
|
|
295
295
|
>Reset</button>
|
|
296
296
|
</div>
|
|
@@ -303,7 +303,7 @@ function ShadcnDemo() {
|
|
|
303
303
|
required
|
|
304
304
|
placeholder="At least 2 characters"
|
|
305
305
|
value={name}
|
|
306
|
-
onChange={e => setName(e.target.value)}
|
|
306
|
+
onChange={e => { setName(e.target.value); }}
|
|
307
307
|
/>
|
|
308
308
|
</div>
|
|
309
309
|
<div className="grid gap-1.5">
|
|
@@ -314,7 +314,7 @@ function ShadcnDemo() {
|
|
|
314
314
|
required
|
|
315
315
|
placeholder="you@example.com"
|
|
316
316
|
value={email}
|
|
317
|
-
onChange={e => setEmail(e.target.value)}
|
|
317
|
+
onChange={e => { setEmail(e.target.value); }}
|
|
318
318
|
/>
|
|
319
319
|
</div>
|
|
320
320
|
<div className="flex items-center gap-2">
|
|
@@ -417,7 +417,7 @@ export default function ReactPage({ title, serverTime, node, _urlBase, _urlStc,
|
|
|
417
417
|
{(['overview', 'routing', 'fetch', 'shadcn'] as const).map(t => (
|
|
418
418
|
<button
|
|
419
419
|
key={t}
|
|
420
|
-
onClick={() => setTab(t)}
|
|
420
|
+
onClick={() => { setTab(t); }}
|
|
421
421
|
className={[
|
|
422
422
|
'px-4 py-2 rounded-md text-sm font-medium transition-colors cursor-pointer',
|
|
423
423
|
tab === t ? 'bg-white shadow text-slate-900' : 'text-slate-500 hover:text-slate-900',
|
|
@@ -441,12 +441,12 @@ export default function ReactPage({ title, serverTime, node, _urlBase, _urlStc,
|
|
|
441
441
|
</p>
|
|
442
442
|
<div className="flex items-center gap-4">
|
|
443
443
|
<button
|
|
444
|
-
onClick={() => setCount(c => c - 1)}
|
|
444
|
+
onClick={() => { setCount(c => c - 1); }}
|
|
445
445
|
className="w-10 h-10 rounded-lg border border-slate-300 hover:bg-slate-50 font-bold text-slate-700 text-xl cursor-pointer"
|
|
446
446
|
>−</button>
|
|
447
447
|
<span className="text-3xl font-bold text-slate-900 w-10 text-center tabular-nums">{count}</span>
|
|
448
448
|
<button
|
|
449
|
-
onClick={() => setCount(c => c + 1)}
|
|
449
|
+
onClick={() => { setCount(c => c + 1); }}
|
|
450
450
|
className="w-10 h-10 rounded-lg bg-blue-500 hover:bg-blue-600 text-white font-bold text-xl cursor-pointer"
|
|
451
451
|
>+</button>
|
|
452
452
|
</div>
|
|
@@ -459,7 +459,8 @@ export default function ReactPage({ title, serverTime, node, _urlBase, _urlStc,
|
|
|
459
459
|
Passed via{' '}
|
|
460
460
|
<code className="bg-slate-100 px-1 rounded">_loadReactPage(path, props)</code>.
|
|
461
461
|
The framework auto-injects constants like _urlBase and serializes
|
|
462
|
-
all props as inline JSON, reused directly during client hydration —
|
|
462
|
+
all props as inline JSON, reused directly during client hydration —
|
|
463
|
+
no extra request needed.
|
|
463
464
|
</p>
|
|
464
465
|
<div className="space-y-2">
|
|
465
466
|
{([
|
|
@@ -491,8 +492,10 @@ export default function ReactPage({ title, serverTime, node, _urlBase, _urlStc,
|
|
|
491
492
|
react-page.tsx # Page component (import + compose)`}
|
|
492
493
|
</pre>
|
|
493
494
|
<p className="text-slate-500 text-xs mt-3">
|
|
494
|
-
Mirrors the shadcn/ui
|
|
495
|
-
|
|
495
|
+
Mirrors the shadcn/ui{' '}
|
|
496
|
+
<code className="bg-slate-100 px-1 rounded">components/ui/</code> convention.
|
|
497
|
+
In dev mode, the framework auto-scans imports —
|
|
498
|
+
no manual import map configuration needed.
|
|
496
499
|
</p>
|
|
497
500
|
</Card>
|
|
498
501
|
|
|
@@ -527,7 +530,8 @@ export default function ReactPage({ title, serverTime, node, _urlBase, _urlStc,
|
|
|
527
530
|
<h2 className="font-semibold text-slate-900 mb-2">Full-Page BrowserRouter Setup</h2>
|
|
528
531
|
<p className="text-slate-500 text-xs mb-3">
|
|
529
532
|
To let React Router manage the real URL (e.g. /app, /app/about),
|
|
530
|
-
replace MemoryRouter with BrowserRouter and route all sub-paths
|
|
533
|
+
replace MemoryRouter with BrowserRouter and route all sub-paths
|
|
534
|
+
to the same Ctr method on the server:
|
|
531
535
|
</p>
|
|
532
536
|
<pre className="bg-slate-50 border border-slate-200 rounded-lg p-4 text-xs overflow-auto leading-relaxed text-slate-700">{`// 1. route.json: 将所有子路径路由到同一 Ctr 方法
|
|
533
537
|
{
|
|
@@ -561,18 +565,19 @@ await this._loadReactPage('view/my', { ...props }, {
|
|
|
561
565
|
<Card>
|
|
562
566
|
<h2 className="font-semibold text-slate-900 mb-3">Client Fetch Demo</h2>
|
|
563
567
|
<p className="text-slate-500 text-xs mb-4">
|
|
564
|
-
After hydration, click a button to send a GET request.
|
|
568
|
+
After hydration, click a button to send a GET request.
|
|
569
|
+
setState triggers a partial re-render — no page refresh.
|
|
565
570
|
</p>
|
|
566
571
|
<div className="flex gap-3 flex-wrap">
|
|
567
572
|
<Btn
|
|
568
|
-
onClick={() => doFetch(`${_urlBase}test/json?type=4`)}
|
|
573
|
+
onClick={() => { doFetch(`${_urlBase}test/json?type=4`); }}
|
|
569
574
|
disabled={isFetching}
|
|
570
575
|
>
|
|
571
576
|
{isFetching ? 'Fetching...' : 'GET /test/json?type=4'}
|
|
572
577
|
</Btn>
|
|
573
578
|
<Btn
|
|
574
579
|
outline
|
|
575
|
-
onClick={() => doFetch(`${_urlBase}test/json?type=2`)}
|
|
580
|
+
onClick={() => { doFetch(`${_urlBase}test/json?type=2`); }}
|
|
576
581
|
disabled={isFetching}
|
|
577
582
|
>
|
|
578
583
|
GET type=2 (error resp)
|