@maiyunnet/kebab 9.14.2 → 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 +381 -559
- package/index.d.ts +1 -1
- package/index.js +1 -1
- package/lib/core.d.ts +2 -0
- package/lib/core.js +21 -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.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/sys/route.js
CHANGED
|
@@ -41,6 +41,20 @@ function respond500(res) {
|
|
|
41
41
|
res.end(content);
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* --- 安全写入外部输入键,避免 __proto__ 等特殊键改变对象原型 ---
|
|
46
|
+
* @param target 目标对象
|
|
47
|
+
* @param key 键
|
|
48
|
+
* @param value 值
|
|
49
|
+
*/
|
|
50
|
+
function setInputValue(target, key, value) {
|
|
51
|
+
Object.defineProperty(target, key, {
|
|
52
|
+
'configurable': true,
|
|
53
|
+
'enumerable': true,
|
|
54
|
+
'value': value,
|
|
55
|
+
'writable': true,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
44
58
|
/**
|
|
45
59
|
* --- 输出 404 错误响应 ---
|
|
46
60
|
* @param res 响应对象
|
|
@@ -58,7 +72,7 @@ function respond404(res, config, path) {
|
|
|
58
72
|
}
|
|
59
73
|
return;
|
|
60
74
|
}
|
|
61
|
-
const content = '[Error] Controller not found, path: ' + path + '.';
|
|
75
|
+
const content = '[Error] Controller not found, path: ' + lText.htmlescape(path) + '.';
|
|
62
76
|
if (!res.headersSent) {
|
|
63
77
|
res.setHeader('content-type', 'text/html; charset=utf-8');
|
|
64
78
|
res.setHeader('content-length', Buffer.byteLength(content));
|
|
@@ -245,17 +259,17 @@ export async function run(data) {
|
|
|
245
259
|
const key = cookie.slice(0, eqIndex).trim();
|
|
246
260
|
const rawVal = cookie.slice(eqIndex + 1);
|
|
247
261
|
try {
|
|
248
|
-
cookies
|
|
262
|
+
setInputValue(cookies, key, decodeURIComponent(rawVal));
|
|
249
263
|
}
|
|
250
264
|
catch {
|
|
251
|
-
cookies
|
|
265
|
+
setInputValue(cookies, key, rawVal);
|
|
252
266
|
}
|
|
253
267
|
}
|
|
254
268
|
}
|
|
255
269
|
// --- 处理 headers ---
|
|
256
270
|
const headers = {};
|
|
257
271
|
for (const key in data.req.headers) {
|
|
258
|
-
headers
|
|
272
|
+
setInputValue(headers, key.toLowerCase(), data.req.headers[key]);
|
|
259
273
|
}
|
|
260
274
|
headers['authorization'] ??= '';
|
|
261
275
|
/** --- 开发者返回值 --- */
|
|
@@ -922,9 +936,9 @@ export function getFormData(req, events = {}, limits = {}) {
|
|
|
922
936
|
resolve({ 'post': {}, 'files': {} });
|
|
923
937
|
return;
|
|
924
938
|
}
|
|
925
|
-
/** --- boundary
|
|
926
|
-
const
|
|
927
|
-
if (
|
|
939
|
+
/** --- 获取 boundary,兼容带引号及后续参数的标准写法 --- */
|
|
940
|
+
const boundaryMatch = /(?:^|;)\s*boundary\s*=\s*(?:"([^"]+)"|([^;\s]+))/i.exec(ct);
|
|
941
|
+
if (!boundaryMatch) {
|
|
928
942
|
resolve({ 'post': {}, 'files': {} });
|
|
929
943
|
return;
|
|
930
944
|
}
|
|
@@ -934,7 +948,11 @@ export function getFormData(req, events = {}, limits = {}) {
|
|
|
934
948
|
'files': {}
|
|
935
949
|
};
|
|
936
950
|
/** --- 获取的 boundary 文本 --- */
|
|
937
|
-
const boundary =
|
|
951
|
+
const boundary = boundaryMatch[1] ?? boundaryMatch[2];
|
|
952
|
+
if (!boundary || (boundary.length > 200) || /[\r\n]/.test(boundary)) {
|
|
953
|
+
resolve(false);
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
938
956
|
// --- 超时护盾:防止网络静默断开时 Promise 永不 resolve ---
|
|
939
957
|
/** --- 超时定时器 --- */
|
|
940
958
|
let timer;
|
|
@@ -978,6 +996,10 @@ export function getFormData(req, events = {}, limits = {}) {
|
|
|
978
996
|
let readEnd = false;
|
|
979
997
|
/** --- 是否有文件被限制拒绝(整体返回 false) --- */
|
|
980
998
|
let rejected = false;
|
|
999
|
+
/** --- 已接收的请求体字节数 --- */
|
|
1000
|
+
let totalSize = 0;
|
|
1001
|
+
/** --- 已解析的字段与文件数量 --- */
|
|
1002
|
+
let partCount = 0;
|
|
981
1003
|
/** --- 清理 rtn.files 中所有已写入的临时文件 --- */
|
|
982
1004
|
function cleanupFiles() {
|
|
983
1005
|
for (const key in rtn.files) {
|
|
@@ -990,6 +1012,16 @@ export function getFormData(req, events = {}, limits = {}) {
|
|
|
990
1012
|
}
|
|
991
1013
|
}
|
|
992
1014
|
}
|
|
1015
|
+
/** --- 清理仍在写入、尚未加入 rtn.files 的临时文件 --- */
|
|
1016
|
+
function cleanupActiveFile() {
|
|
1017
|
+
if ((state !== EState.FILE) || !ftmpName) {
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
ftmpStream.destroy();
|
|
1021
|
+
lFs.unlink(kebab.FTMP_CWD + ftmpName).catch(() => { });
|
|
1022
|
+
ftmpName = '';
|
|
1023
|
+
--writeFileLength;
|
|
1024
|
+
}
|
|
993
1025
|
// --- 启动超时定时器(在所有变量声明之后,确保回调闭包可引用) ---
|
|
994
1026
|
const timeoutMs = limits.timeout ?? 300_000;
|
|
995
1027
|
if (timeoutMs > 0) {
|
|
@@ -998,9 +1030,7 @@ export function getFormData(req, events = {}, limits = {}) {
|
|
|
998
1030
|
return;
|
|
999
1031
|
}
|
|
1000
1032
|
finished = true;
|
|
1001
|
-
|
|
1002
|
-
ftmpStream.destroy();
|
|
1003
|
-
}
|
|
1033
|
+
cleanupActiveFile();
|
|
1004
1034
|
lCore.debug('[ROUTE][GETFORMDATA] formdata request timeout');
|
|
1005
1035
|
lCore.log({}, '[ROUTE][GETFORMDATA] formdata request timeout after ' + timeoutMs + 'ms', '-error');
|
|
1006
1036
|
cleanupFiles();
|
|
@@ -1032,6 +1062,16 @@ export function getFormData(req, events = {}, limits = {}) {
|
|
|
1032
1062
|
}
|
|
1033
1063
|
// --- 开始读取 ---
|
|
1034
1064
|
req.on('data', function (chunk) {
|
|
1065
|
+
if (finished || rejected) {
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
totalSize += chunk.length;
|
|
1069
|
+
if ((limits.maxTotalSize !== undefined) && (limits.maxTotalSize > 0) && (totalSize > limits.maxTotalSize)) {
|
|
1070
|
+
rejected = true;
|
|
1071
|
+
cleanupActiveFile();
|
|
1072
|
+
buffer = Buffer.from('');
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1035
1075
|
buffer = Buffer.concat([buffer, chunk], buffer.length + chunk.length);
|
|
1036
1076
|
while (true) {
|
|
1037
1077
|
switch (state) {
|
|
@@ -1039,9 +1079,19 @@ export function getFormData(req, events = {}, limits = {}) {
|
|
|
1039
1079
|
/** --- 中断符位置 --- */
|
|
1040
1080
|
const io = buffer.indexOf('\r\n\r\n');
|
|
1041
1081
|
if (io === -1) {
|
|
1082
|
+
if (buffer.length > (limits.maxHeaderSize ?? 16 * 1024)) {
|
|
1083
|
+
rejected = true;
|
|
1084
|
+
buffer = Buffer.from('');
|
|
1085
|
+
}
|
|
1042
1086
|
return;
|
|
1043
1087
|
}
|
|
1044
1088
|
// --- 头部已经读取完毕 ---
|
|
1089
|
+
++partCount;
|
|
1090
|
+
if (partCount > (limits.maxParts ?? 1000)) {
|
|
1091
|
+
rejected = true;
|
|
1092
|
+
buffer = Buffer.from('');
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1045
1095
|
const head = buffer.subarray(0, io).toString();
|
|
1046
1096
|
// --- 除头部外剩下的 buffer ---
|
|
1047
1097
|
buffer = buffer.subarray(io + 4);
|
|
@@ -1084,8 +1134,20 @@ export function getFormData(req, events = {}, limits = {}) {
|
|
|
1084
1134
|
date.getUTCDate().toString().padStart(2, '0') +
|
|
1085
1135
|
date.getUTCHours().toString().padStart(2, '0') +
|
|
1086
1136
|
date.getUTCMinutes().toString().padStart(2, '0') + '_' + lCore.random() + '.ftmp';
|
|
1087
|
-
|
|
1088
|
-
|
|
1137
|
+
const activeName = ftmpName;
|
|
1138
|
+
const activePath = kebab.FTMP_CWD + activeName;
|
|
1139
|
+
const activeStream = lFs.createWriteStream(activePath);
|
|
1140
|
+
ftmpStream = activeStream;
|
|
1141
|
+
activeStream.on('error', (error) => {
|
|
1142
|
+
rejected = true;
|
|
1143
|
+
lFs.unlink(activePath).catch(() => { });
|
|
1144
|
+
if (ftmpStream === activeStream) {
|
|
1145
|
+
ftmpName = '';
|
|
1146
|
+
}
|
|
1147
|
+
--writeFileLength;
|
|
1148
|
+
req.resume();
|
|
1149
|
+
lCore.log({}, `[ROUTE][GETFORMDATA] temporary file error: ${error.message}`, '-error');
|
|
1150
|
+
});
|
|
1089
1151
|
ftmpSize = 0;
|
|
1090
1152
|
}
|
|
1091
1153
|
else {
|
|
@@ -1102,13 +1164,14 @@ export function getFormData(req, events = {}, limits = {}) {
|
|
|
1102
1164
|
const maxField = limits.maxFieldSize ?? 1_048_576;
|
|
1103
1165
|
if (buffer.length > maxField + boundary.length + 4) {
|
|
1104
1166
|
rejected = true;
|
|
1167
|
+
buffer = Buffer.from('');
|
|
1105
1168
|
return;
|
|
1106
1169
|
}
|
|
1107
1170
|
return;
|
|
1108
1171
|
}
|
|
1109
1172
|
// --- 找到结束标语,写入 POST ---
|
|
1110
1173
|
const val = buffer.subarray(0, io).toString();
|
|
1111
|
-
if (rtn.post
|
|
1174
|
+
if (Object.hasOwn(rtn.post, name)) {
|
|
1112
1175
|
if (Array.isArray(rtn.post[name])) {
|
|
1113
1176
|
rtn.post[name].push(val);
|
|
1114
1177
|
}
|
|
@@ -1117,7 +1180,7 @@ export function getFormData(req, events = {}, limits = {}) {
|
|
|
1117
1180
|
}
|
|
1118
1181
|
}
|
|
1119
1182
|
else {
|
|
1120
|
-
rtn.post
|
|
1183
|
+
setInputValue(rtn.post, name, val);
|
|
1121
1184
|
}
|
|
1122
1185
|
// --- 重置状态机 ---
|
|
1123
1186
|
state = EState.WAIT;
|
|
@@ -1138,7 +1201,10 @@ export function getFormData(req, events = {}, limits = {}) {
|
|
|
1138
1201
|
rejectFile();
|
|
1139
1202
|
}
|
|
1140
1203
|
else {
|
|
1141
|
-
ftmpStream.write(writeBuffer)
|
|
1204
|
+
if (!ftmpStream.write(writeBuffer)) {
|
|
1205
|
+
req.pause();
|
|
1206
|
+
ftmpStream.once('drain', () => { req.resume(); });
|
|
1207
|
+
}
|
|
1142
1208
|
ftmpSize += Buffer.byteLength(writeBuffer);
|
|
1143
1209
|
}
|
|
1144
1210
|
}
|
|
@@ -1188,7 +1254,7 @@ export function getFormData(req, events = {}, limits = {}) {
|
|
|
1188
1254
|
'size': ftmpSize,
|
|
1189
1255
|
'path': kebab.FTMP_CWD + ftmpName
|
|
1190
1256
|
};
|
|
1191
|
-
if (rtn.files
|
|
1257
|
+
if (Object.hasOwn(rtn.files, name)) {
|
|
1192
1258
|
if (Array.isArray(rtn.files[name])) {
|
|
1193
1259
|
rtn.files[name].push(val);
|
|
1194
1260
|
}
|
|
@@ -1197,7 +1263,7 @@ export function getFormData(req, events = {}, limits = {}) {
|
|
|
1197
1263
|
}
|
|
1198
1264
|
}
|
|
1199
1265
|
else {
|
|
1200
|
-
rtn.files
|
|
1266
|
+
setInputValue(rtn.files, name, val);
|
|
1201
1267
|
}
|
|
1202
1268
|
}
|
|
1203
1269
|
}
|
|
@@ -1223,9 +1289,7 @@ export function getFormData(req, events = {}, limits = {}) {
|
|
|
1223
1289
|
}
|
|
1224
1290
|
finished = true;
|
|
1225
1291
|
clearTimer();
|
|
1226
|
-
|
|
1227
|
-
ftmpStream.destroy();
|
|
1228
|
-
}
|
|
1292
|
+
cleanupActiveFile();
|
|
1229
1293
|
lCore.debug('[ROUTE][GETFORMDATA] request error before getFormData: ' + e.message);
|
|
1230
1294
|
lCore.log({}, '[ROUTE][GETFORMDATA] request error before getFormData: ' + (e.stack ?? ''), '-error');
|
|
1231
1295
|
cleanupFiles();
|
|
@@ -1237,12 +1301,10 @@ export function getFormData(req, events = {}, limits = {}) {
|
|
|
1237
1301
|
return;
|
|
1238
1302
|
}
|
|
1239
1303
|
// --- 若数据未读完且连接已关闭,视为传输中断(多数是用户主动取消,无需记录错误) ---
|
|
1240
|
-
if (!readEnd
|
|
1304
|
+
if (!readEnd) {
|
|
1241
1305
|
finished = true;
|
|
1242
1306
|
clearTimer();
|
|
1243
|
-
|
|
1244
|
-
ftmpStream.destroy();
|
|
1245
|
-
}
|
|
1307
|
+
cleanupActiveFile();
|
|
1246
1308
|
lCore.debug('[ROUTE][GETFORMDATA] connection closed before formdata complete');
|
|
1247
1309
|
cleanupFiles();
|
|
1248
1310
|
resolve(false);
|
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
|
}
|