📝 docs(code-reader): React 其它常用钩子按状态卡思路的原型
This commit is contained in:
1 parent
966d893f18
commit
67e8a77f5d
4 files changed
+23541
-56
No files matched your search
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
+4
-4
@@ -174,7 +174,7 @@ function link(text, color) {
|
|||||||
|
|
||||||
// ─── 源码着色 ────────────────────────────────────────────────────────────────
|
// ─── 源码着色 ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const TSX_TOKEN = /'[^']*'|"[^"]*"|=>|\.\.\.|\?\.|&&|<\/|\/>|[A-Za-z_$][\w$]*|\d+(?:\.\d+)?|\s+|[\s\S]/g;
|
const TSX_TOKEN = /'[^']*'|"[^"]*"|`[^`]*`|=>|\.\.\.|\?\.|&&|<\/|\/>|[A-Za-z_$][\w$]*|\d+(?:\.\d+)?|\s+|[\s\S]/g;
|
||||||
const TSX_STORAGE = new Set(["const", "let", "var", "function", "new", "typeof", "true", "false", "null", "undefined", "this"]);
|
const TSX_STORAGE = new Set(["const", "let", "var", "function", "new", "typeof", "true", "false", "null", "undefined", "this"]);
|
||||||
const TSX_CONTROL = new Set(["import", "export", "from", "return", "if", "else", "switch", "case", "default", "await"]);
|
const TSX_CONTROL = new Set(["import", "export", "from", "return", "if", "else", "switch", "case", "default", "await"]);
|
||||||
const TSX_PRIMITIVE = new Set(["string", "number", "boolean", "void", "any", "unknown"]);
|
const TSX_PRIMITIVE = new Set(["string", "number", "boolean", "void", "any", "unknown"]);
|
||||||
@@ -211,7 +211,7 @@ function highlight(lines) {
|
|||||||
prev = tok;
|
prev = tok;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (/^['"]/.test(tok))
|
if (/^['"`]/.test(tok))
|
||||||
color = C.string;
|
color = C.string;
|
||||||
else if (/^\d/.test(tok))
|
else if (/^\d/.test(tok))
|
||||||
color = C.number;
|
color = C.number;
|
||||||
@@ -583,7 +583,7 @@ function conceptIcon(parent, item) {
|
|||||||
/**
|
/**
|
||||||
* 一张卡片:折叠箭头(有子块才有:`open` 展开、`closed` 收起)、概念图标、内容、右上角两个按钮。
|
* 一张卡片:折叠箭头(有子块才有:`open` 展开、`closed` 收起)、概念图标、内容、右上角两个按钮。
|
||||||
* 概念图标默认是种类图标一个,给了 `icons` 就并排画这几个(`{name, color, tip, bubble?}`)。
|
* 概念图标默认是种类图标一个,给了 `icons` 就并排画这几个(`{name, color, tip, bubble?}`)。
|
||||||
* 淡底默认用种类色,给了 `tint` 用它(钩子卡片按来源分底色)。给了 `calls` 右上角多一个调用树按钮。
|
* 淡底、描边默认用种类色,给了 `tint`、`stroke` 用它们(钩子卡片按来源分底色)。给了 `calls` 右上角多一个调用树按钮。
|
||||||
* 放进自由摆放的框里时给 `x`、`y`,宽度定成 `w`。
|
* 放进自由摆放的框里时给 `x`、`y`,宽度定成 `w`。
|
||||||
* `w` 是卡片外宽;返回内容区和内容区的宽度,属性行、表格按它断行。
|
* `w` 是卡片外宽;返回内容区和内容区的宽度,属性行、表格按它断行。
|
||||||
*/
|
*/
|
||||||
@@ -600,7 +600,7 @@ function card(parent, kind, w, o) {
|
|||||||
radius: 8,
|
radius: 8,
|
||||||
align: "start",
|
align: "start",
|
||||||
fill: bg,
|
fill: bg,
|
||||||
stroke: mix(kind.color, C.editor, 0.35),
|
stroke: mix(opt.stroke || kind.color, C.editor, 0.35),
|
||||||
});
|
});
|
||||||
const fold = row(node, "Fold", { width: 14, height: 20, justify: "center" });
|
const fold = row(node, "Fold", { width: 14, height: 20, justify: "center" });
|
||||||
if (opt.fold)
|
if (opt.fold)
|
||||||
|
|||||||
+420
-52
@@ -3,7 +3,8 @@
|
|||||||
// 它来自 React 内置的 useState(悬停说明);状态类型;默认值。解构表的「操作」一列放每一项能做的操作,
|
// 它来自 React 内置的 useState(悬停说明);状态类型;默认值。解构表的「操作」一列放每一项能做的操作,
|
||||||
// 现在是调用树:当前值查谁用了它,修改函数查谁改了它。
|
// 现在是调用树:当前值查谁用了它,修改函数查谁改了它。
|
||||||
// 画板一照抄 luffy-agent-platform 的 frontend/client-nextjs/src/app/desktop/login/page.tsx 第 1–37 行,
|
// 画板一照抄 luffy-agent-platform 的 frontend/client-nextjs/src/app/desktop/login/page.tsx 第 1–37 行,
|
||||||
// 画板二是手写的三行,讲没标类型、函数形式的默认值、只取当前值三种写法。
|
// 画板二是手写的三行,讲没标类型、函数形式的默认值、只取当前值三种写法;
|
||||||
|
// 画板三把其它常用钩子照同一个思路画:卡片讲这个变量(或这条语句)是什么,钩子退到「来自」一行,样例是手写的搜索面板。
|
||||||
// 窗口、对齐、别的卡片的画法在同目录 react-shared.js,接在它后面跑:`cat react-shared.js react-state.js | op design - --script`。
|
// 窗口、对齐、别的卡片的画法在同目录 react-shared.js,接在它后面跑:`cat react-shared.js react-state.js | op design - --script`。
|
||||||
|
|
||||||
// ─── 样例源码 ────────────────────────────────────────────────────────────────
|
// ─── 样例源码 ────────────────────────────────────────────────────────────────
|
||||||
@@ -81,12 +82,26 @@ const VARIANTS = sampleFile({
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
// ─── 状态卡片 ────────────────────────────────────────────────────────────────
|
// ─── 变量卡片:讲「这个变量是什么」,钩子退到「来自」一行 ────────────────────
|
||||||
|
|
||||||
// 种类「状态」:描边、淡底、两个图标都用变量色;左边先是变量卡片的图标,再是状态图标,悬停才出名字
|
// 左边第一个图标是语言的概念:变量、语句、函数,颜色同各自的卡片
|
||||||
const STATE = { label: "状态", color: "#9cdcfe", icon: "variable" };
|
const VAR = { icon: "variable", color: "#9cdcfe", tip: "变量" };
|
||||||
|
const STMT = { icon: "code", color: "#9d9d9d", tip: "语句" };
|
||||||
|
const FUNC = { icon: "square-function", color: "#dcdcaa", tip: "函数" };
|
||||||
|
|
||||||
// 解构出的每一项:说明、它那一行调用树按钮查什么(悬停文字)
|
// 第二个图标是 React 的概念,也是种类:描边、淡底默认用它的颜色,悬停写 `tip`
|
||||||
|
const STATE = { label: "状态", color: "#9cdcfe", icon: "toggle-right", tip: "React 状态" };
|
||||||
|
const HOOK_VALUE = { label: "钩子返回值", color: "#b180d7", icon: "fishing-hook", tip: "钩子返回值" };
|
||||||
|
const CONTEXT_VALUE = { label: "上下文值", color: "#4ec9b0", icon: "share-2", tip: "上下文值" };
|
||||||
|
const TRANSITION = { label: "过渡", color: "#ce9178", icon: "hourglass", tip: "过渡" };
|
||||||
|
const REF = { label: "引用", color: "#4fc1ff", icon: "anchor", tip: "引用" };
|
||||||
|
const MEMO_VALUE = { label: "记忆值", color: "#b5cea8", icon: "sigma", tip: "记忆值" };
|
||||||
|
const MEMO_FN = { label: "记忆函数", color: "#b5cea8", icon: "sigma", tip: "记忆函数" };
|
||||||
|
const EFFECT = { label: "副作用", color: "#e5c07b", icon: "zap", tip: "副作用" };
|
||||||
|
|
||||||
|
const BUILTIN_TIP = "React 内置钩子";
|
||||||
|
|
||||||
|
// useState 解构出的两项:说明、它那一行调用树按钮查什么(悬停文字)
|
||||||
const ITEMS = [
|
const ITEMS = [
|
||||||
{ what: "当前值", tip: "谁用了这个状态" },
|
{ what: "当前值", tip: "谁用了这个状态" },
|
||||||
{ what: "修改函数", tip: "谁改了这个状态" },
|
{ what: "修改函数", tip: "谁改了这个状态" },
|
||||||
@@ -97,20 +112,25 @@ function callTreeWidth(calls) {
|
|||||||
return calls === 0 ? 22 : 4 + 14 + 1 + 14 + 4;
|
return calls === 0 ? 22 : 4 + 14 + 1 + 14 + 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 「操作」一列的宽:按这张卡里按钮最宽的那一行,不窄于表头。 */
|
||||||
|
function opsWidth(calls) {
|
||||||
|
return Math.max(Math.ceil(widthOf("操作", 12, false) + 18), 8 + Math.max(...calls.map(callTreeWidth)) + 8);
|
||||||
|
}
|
||||||
|
|
||||||
/** 「操作」一格:这一项能做的操作从左往右排,现在只有调用树。 */
|
/** 「操作」一格:这一项能做的操作从左往右排,现在只有调用树。 */
|
||||||
function opsCell(item, calls) {
|
function opsCell(tip, calls) {
|
||||||
return {
|
return {
|
||||||
est: "",
|
est: "",
|
||||||
draw: (frame) => {
|
draw: (frame) => {
|
||||||
const line = row(frame, "Ops", { gap: 2, height: 20, width: "fit_content" });
|
const line = row(frame, "Ops", { gap: 2, height: 20, width: "fit_content" });
|
||||||
callTreeButton(line, calls, item.tip);
|
callTreeButton(line, calls, tip);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 第 `n` 行调用树按钮的悬停气泡在卡片里的位置:在按钮正下方居中、隔 6px。
|
* 第 `n` 行调用树按钮的悬停气泡在卡片里的位置:在按钮正下方居中、隔 6px。
|
||||||
* 按钮上方是上一行的按钮,右边伸出卡片,只有下面空着(「来自」那一行的右半边)。
|
* 按钮上方是上一行的按钮,右边伸出卡片,只有下面空着(表格下面那一行的右半边)。
|
||||||
* 卡片里的位置照 [card]、[table] 的排法算:内容区左边在 84px(内边距 8、折叠箭头 14、两个图标 42、两处间距 10),
|
* 卡片里的位置照 [card]、[table] 的排法算:内容区左边在 84px(内边距 8、折叠箭头 14、两个图标 42、两处间距 10),
|
||||||
* 宽是卡片宽减 150;表从内容区上方 8 + 名称一行 20 + 间距 3 处开始,表格框留白 2、表头 24、每行前面 1px 分隔线、行高 24,
|
* 宽是卡片宽减 150;表从内容区上方 8 + 名称一行 20 + 间距 3 处开始,表格框留白 2、表头 24、每行前面 1px 分隔线、行高 24,
|
||||||
* 一行的各格从表格框左边 5px 排到内容区右边往回 2px,「操作」一格在最右,按钮在格子左边 8px、上边 2px。
|
* 一行的各格从表格框左边 5px 排到内容区右边往回 2px,「操作」一格在最右,按钮在格子左边 8px、上边 2px。
|
||||||
@@ -124,55 +144,136 @@ function opsBubble(w, n, opsW, calls, tip) {
|
|||||||
return { x: buttonX + callTreeWidth(calls) / 2 - tooltipWidth(tip) / 2, y: buttonBottom + 6 };
|
return { x: buttonX + callTreeWidth(calls) / 2 - tooltipWidth(tip) / 2, y: buttonBottom + 6 };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 一处 useState:名称照源码写出解构(后面灰字「数组解构」,展开是「第几项 | 名称 | 说明 | 操作」)/
|
|
||||||
* 来自(悬停说是 React 内置钩子)/ 状态类型 / 默认值。
|
|
||||||
* `names` 是解构出的名字,第二个是修改函数;`calls` 是各项在样例里出现几次,「操作」一格里调用树按钮上的数字。
|
|
||||||
* 「操作」一列按这张卡里按钮最宽的那一行定宽(不窄于表头),「说明」一列分剩下的。
|
|
||||||
* `type` 是状态类型(没写时「未标注」),`initial` 是默认值的着色段。
|
|
||||||
* `bubble` 画出修改函数那一行调用树按钮的悬停气泡:卡片和气泡放进一个自由摆放的框,气泡在前,才画在卡片上层。
|
|
||||||
*/
|
|
||||||
function stateCard(s) {
|
|
||||||
return (parent, w) => {
|
|
||||||
const icons = [
|
|
||||||
{ name: "variable", color: STATE.color, tip: "变量" },
|
|
||||||
{ name: "toggle-right", color: STATE.color, tip: "React 状态" },
|
|
||||||
];
|
|
||||||
const opsW = Math.max(Math.ceil(widthOf("操作", 12, false) + 18), 8 + Math.max(...s.calls.map(callTreeWidth)) + 8);
|
|
||||||
let holder = parent;
|
|
||||||
let place = {};
|
|
||||||
if (s.bubble) {
|
|
||||||
holder = box(parent, "Hover / 谁改了这个状态", { layout: "none", width: w, height: "fit_content" });
|
|
||||||
const spot = opsBubble(w, 2, opsW, s.calls[1], ITEMS[1].tip);
|
|
||||||
tooltip(holder, ITEMS[1].tip, spot.x, spot.y);
|
|
||||||
place = { x: 0, y: 0 };
|
|
||||||
}
|
|
||||||
const ctx = card(holder, STATE, w, Object.assign({ name: s.names[0], icons }, place));
|
|
||||||
prop(ctx, "名称", (frame) => {
|
|
||||||
const line = row(frame, "Destructure", { gap: 8, height: 20, width: "fit_content" });
|
|
||||||
const parts = [seg("[", C.fg)];
|
|
||||||
s.names.forEach((name, i) => {
|
|
||||||
if (i > 0)
|
|
||||||
parts.push(seg(", ", C.fg));
|
|
||||||
parts.push(seg(name, i === 0 ? C.variable : C.fn, { w: 600 }));
|
|
||||||
});
|
|
||||||
parts.push(seg("]", C.fg));
|
|
||||||
text(line, parts, { mono: true });
|
|
||||||
text(line, "数组解构", { size: 12.5, color: C.desc });
|
|
||||||
}, { fold: "open" });
|
|
||||||
table(ctx, [{ name: "第几项", mono: false }, { name: "名称", color: C.variable }, { name: "说明", mono: false, fill: true }, { name: "操作", width: opsW }],
|
|
||||||
s.names.map((name, i) => [indexCell(i + 1), px(name, i === 0 ? C.variable : C.fn), ITEMS[i].what, opsCell(ITEMS[i], s.calls[i])]));
|
|
||||||
prop(ctx, "来自", (frame) => text(frame, px("useState", C.fn), { mono: true, name: "useState(悬停:React 内置钩子)" }));
|
|
||||||
prop(ctx, "状态类型", s.type || px("未标注", C.desc), { mono: Boolean(s.type) });
|
|
||||||
prop(ctx, "默认值", s.initial);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 「第几项」一格:只有一个数字,宽度定死,按内容长会被导出时挤小。 */
|
/** 「第几项」一格:只有一个数字,宽度定死,按内容长会被导出时挤小。 */
|
||||||
function indexCell(n) {
|
function indexCell(n) {
|
||||||
return { est: String(n), draw: (frame) => text(frame, String(n), { size: 12.5, color: C.desc, lh: 20, fixedW: 12 }) };
|
return { est: String(n), draw: (frame) => text(frame, String(n), { size: 12.5, color: C.desc, lh: 20, fixedW: 12 }) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 「名称」一行。只接一个名字时是名字加最右边的调用树按钮,按钮和表格「操作」一列的按钮对齐在同一个 x;
|
||||||
|
* 解构时照源码写出 `[a, b]` / `{ a, b }`,后面灰字「数组解构」「对象解构」,展开是一张表:
|
||||||
|
* 数组「第几项 | 名称 | 说明 | 操作」,对象「名称 | 说明 | 操作」(没有说明的项时不出说明一列,改了名时前面多一列「键」)。
|
||||||
|
*/
|
||||||
|
function nameRow(ctx, s, opsW) {
|
||||||
|
if (typeof s.name === "string") {
|
||||||
|
prop(ctx, "名称", (frame) => {
|
||||||
|
const line = row(frame, "Name", { height: 20 });
|
||||||
|
text(line, [seg(s.name, s.nameColor || C.variable, { w: 600 })], { mono: true });
|
||||||
|
spacer(line, "Spacer", "fill_container", 8);
|
||||||
|
const ops = row(line, "Ops", { width: opsW - 6, height: 20, gap: 2 });
|
||||||
|
callTreeButton(ops, s.calls, s.tip);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const d = s.name;
|
||||||
|
const array = d.kind === "array";
|
||||||
|
prop(ctx, "名称", (frame) => {
|
||||||
|
const line = row(frame, "Destructure", { gap: 8, height: 20, width: "fit_content" });
|
||||||
|
const parts = [seg(array ? "[" : "{ ", C.fg)];
|
||||||
|
d.items.forEach((item, i) => {
|
||||||
|
if (i > 0)
|
||||||
|
parts.push(seg(", ", C.fg));
|
||||||
|
parts.push(seg(item.name, item.color || C.variable, { w: 600 }));
|
||||||
|
});
|
||||||
|
parts.push(seg(array ? "]" : " }", C.fg));
|
||||||
|
text(line, parts, { mono: true });
|
||||||
|
text(line, array ? "数组解构" : "对象解构", { size: 12.5, color: C.desc });
|
||||||
|
}, { fold: "open" });
|
||||||
|
const described = d.items.some((item) => item.what);
|
||||||
|
const keyed = !array && d.items.some((item) => item.key);
|
||||||
|
const cols = [];
|
||||||
|
if (array)
|
||||||
|
cols.push({ name: "第几项", mono: false });
|
||||||
|
if (keyed)
|
||||||
|
cols.push({ name: "键", color: C.variable });
|
||||||
|
cols.push(Object.assign({ name: "名称", color: C.variable }, described ? {} : { fill: true }));
|
||||||
|
if (described)
|
||||||
|
cols.push({ name: "说明", mono: false, fill: true });
|
||||||
|
cols.push({ name: "操作", width: opsW });
|
||||||
|
table(ctx, cols, d.items.map((item, i) => {
|
||||||
|
const cells = [];
|
||||||
|
if (array)
|
||||||
|
cells.push(indexCell(i + 1));
|
||||||
|
if (keyed)
|
||||||
|
cells.push(px(item.key || "", C.variable));
|
||||||
|
cells.push(px(item.name, item.color || C.variable));
|
||||||
|
if (described)
|
||||||
|
cells.push(item.what || "");
|
||||||
|
cells.push(opsCell(item.tip, item.calls));
|
||||||
|
return cells;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 一张讲变量(或一条语句)的卡片:左边两个图标,先是语言的概念 `lang`,再是 React 的概念 `kind`,悬停才出名字;
|
||||||
|
* 描边、淡底默认用 `kind` 的颜色,`stroke`、`tint` 另给。`title` 写进图层名。
|
||||||
|
* `name` 是只接的一个名字(按钮的次数 `calls`、悬停 `tip`),或解构 `{kind: "array" | "object", items}`,
|
||||||
|
* 每项 `{name, color?, what?, key?, calls, tip}`;没有就不出名称一行。`rows` 是后面各行,见 [line]、[fromRow]。
|
||||||
|
* `bubble` 画出解构表第 2 行调用树按钮的悬停气泡:卡片和气泡放进一个自由摆放的框,气泡在前,才画在卡片上层。
|
||||||
|
* 右上角只有问 AI、编辑;`fold` 表示有函数体子块,收起。
|
||||||
|
*/
|
||||||
|
function valueCard(s) {
|
||||||
|
return (parent, w) => {
|
||||||
|
const icons = [
|
||||||
|
{ name: s.lang.icon, color: s.lang.color, tip: s.lang.tip },
|
||||||
|
{ name: s.kind.icon, color: s.kind.color, tip: s.kind.tip },
|
||||||
|
];
|
||||||
|
const calls = typeof s.name === "string" ? [s.calls] : s.name ? s.name.items.map((item) => item.calls) : [];
|
||||||
|
const opsW = calls.length > 0 ? opsWidth(calls) : 0;
|
||||||
|
let holder = parent;
|
||||||
|
let place = {};
|
||||||
|
if (s.bubble) {
|
||||||
|
const item = s.name.items[1];
|
||||||
|
holder = box(parent, "Hover / " + item.tip, { layout: "none", width: w, height: "fit_content" });
|
||||||
|
const spot = opsBubble(w, 2, opsW, item.calls, item.tip);
|
||||||
|
tooltip(holder, item.tip, spot.x, spot.y);
|
||||||
|
place = { x: 0, y: 0 };
|
||||||
|
}
|
||||||
|
const ctx = card(holder, s.kind, w, Object.assign({ name: s.title, icons, fold: s.fold, tint: s.tint, stroke: s.stroke }, place));
|
||||||
|
if (s.name)
|
||||||
|
nameRow(ctx, s, opsW);
|
||||||
|
for (const draw of s.rows) draw(ctx);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 一行属性,同 [prop]。 */
|
||||||
|
const line = (label, value, o) => (ctx) => prop(ctx, label, value, o);
|
||||||
|
|
||||||
|
/** 「来自」一行:钩子名,悬停说它是谁提供的(记在图层名里);自己写的钩子 `linked`,能点到定义。 */
|
||||||
|
const fromRow = (hook, tip, linked) => (ctx) => prop(ctx, "来自", (frame) =>
|
||||||
|
text(frame, [seg(hook, C.fn, linked ? { u: true } : {})], { mono: true, name: `${hook}(悬停:${tip})` }));
|
||||||
|
|
||||||
|
/** 收起的函数体一行:小框「函数 (参数) · N 句」,同 [fnBox]。 */
|
||||||
|
const fnRow = (label, params, count) => (ctx) => prop(ctx, label, (frame) => fnBox(params, count).draw(frame));
|
||||||
|
|
||||||
|
/** 带「N 个」和展开表的一行(参数)。 */
|
||||||
|
const tableRow = (label, cols, rows) => (ctx) => {
|
||||||
|
prop(ctx, label, px(`${rows.length} 个`, C.desc), { mono: false, fold: "open" });
|
||||||
|
table(ctx, cols, rows);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 依赖的几个值变化时:名字用顿号连起来。 */
|
||||||
|
const changedAll = (names) => names.flatMap((name, i) => (i > 0 ? [seg("、", C.fg)] : []).concat([seg(name, C.variable)])).concat([seg(" 变化时", C.keyword)]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 一处 useState:名称照源码写出解构(展开是「第几项 | 名称 | 说明 | 操作」,当前值查谁用了它,修改函数查谁改了它)/
|
||||||
|
* 来自 / 状态类型(没写时「未标注」)/ 默认值。`names` 是解构出的名字,`calls` 是各项在样例里出现几次。
|
||||||
|
*/
|
||||||
|
function stateCard(s) {
|
||||||
|
return valueCard({
|
||||||
|
kind: STATE,
|
||||||
|
lang: VAR,
|
||||||
|
title: s.names[0],
|
||||||
|
name: { kind: "array", items: s.names.map((name, i) => ({ name, color: i === 0 ? C.variable : C.fn, what: ITEMS[i].what, tip: ITEMS[i].tip, calls: s.calls[i] })) },
|
||||||
|
bubble: s.bubble,
|
||||||
|
rows: [
|
||||||
|
fromRow("useState", BUILTIN_TIP),
|
||||||
|
line("状态类型", s.type || px("未标注", C.desc), { mono: Boolean(s.type) }),
|
||||||
|
line("默认值", s.initial),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ─── 画板一:登录页 ──────────────────────────────────────────────────────────
|
// ─── 画板一:登录页 ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const LOGIN_PAGE = componentCard({
|
const LOGIN_PAGE = componentCard({
|
||||||
@@ -276,3 +377,270 @@ windowOf("Prototype / React 状态 · 其他写法", WIN_W + BOARD_GAP, VARIANTS
|
|||||||
draw: stateCard({ names: ["count"], calls: [0], initial: cx("0") }),
|
draw: stateCard({ names: ["count"], calls: [0], initial: cx("0") }),
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// ─── 画板三:其它常用钩子 ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// 搜索面板:return 里的 JSX 是补写的,每个名字在样例里出现几次就是它调用树按钮上的数(依赖数组里的不算)
|
||||||
|
const SEARCH = sampleFile({
|
||||||
|
project: "web",
|
||||||
|
file: "SearchPanel.tsx",
|
||||||
|
crumbs: ["src", "search", "SearchPanel.tsx"],
|
||||||
|
tree: [
|
||||||
|
{ kind: "folder", name: "src", open: true, depth: 0 },
|
||||||
|
{ kind: "folder", name: "hooks", open: false, depth: 1 },
|
||||||
|
{ kind: "folder", name: "lib", open: false, depth: 1 },
|
||||||
|
{ kind: "folder", name: "search", open: true, depth: 1 },
|
||||||
|
{ kind: "file", name: "SearchPanel.tsx", depth: 2 },
|
||||||
|
],
|
||||||
|
count: 6,
|
||||||
|
source: [
|
||||||
|
"\"use client\";",
|
||||||
|
"",
|
||||||
|
"import { useCallback, useContext, useEffect, useMemo, useReducer, useRef, useState, useTransition } from \"react\";",
|
||||||
|
"import { useRouter } from \"next/navigation\";",
|
||||||
|
"",
|
||||||
|
"import { AuthContext } from \"@/lib/auth\";",
|
||||||
|
"import { useSearch } from \"@/hooks/use-search\";",
|
||||||
|
"import { EMPTY_FILTER, filterReducer } from \"./filter\";",
|
||||||
|
"",
|
||||||
|
"export const SearchPanel = ({ initialQuery }: { initialQuery: string }) => {",
|
||||||
|
" const router = useRouter();",
|
||||||
|
" const { user, logout } = useContext(AuthContext);",
|
||||||
|
" const [query, setQuery] = useState(initialQuery);",
|
||||||
|
" const [filter, dispatch] = useReducer(filterReducer, EMPTY_FILTER);",
|
||||||
|
" const { results, loading } = useSearch(query, filter);",
|
||||||
|
" const [isPending, startTransition] = useTransition();",
|
||||||
|
" const inputRef = useRef<HTMLInputElement>(null);",
|
||||||
|
" const visible = useMemo(() => results.filter((r) => !r.hidden), [results]);",
|
||||||
|
"",
|
||||||
|
" const submit = useCallback((e: FormEvent) => {",
|
||||||
|
" e.preventDefault();",
|
||||||
|
" startTransition(() => router.push(`/search?q=${query}`));",
|
||||||
|
" }, [router, query]);",
|
||||||
|
"",
|
||||||
|
" useEffect(() => {",
|
||||||
|
" inputRef.current?.focus();",
|
||||||
|
" }, []);",
|
||||||
|
"",
|
||||||
|
" useEffect(() => {",
|
||||||
|
" if (!user) logout();",
|
||||||
|
" }, [user, logout]);",
|
||||||
|
"",
|
||||||
|
" return (",
|
||||||
|
" <form onSubmit={submit}>",
|
||||||
|
" <input ref={inputRef} value={query} onChange={(e) => setQuery(e.target.value)} />",
|
||||||
|
" <button type=\"button\" onClick={() => dispatch({ type: \"toggleHidden\" })}>",
|
||||||
|
" 显示隐藏项",
|
||||||
|
" </button>",
|
||||||
|
" {isPending && <span>跳转中…</span>}",
|
||||||
|
" {loading && <p>搜索中…</p>}",
|
||||||
|
" <ul>",
|
||||||
|
" {visible.map((r) => <li key={r.id}>{r.title}</li>)}",
|
||||||
|
" </ul>",
|
||||||
|
" </form>",
|
||||||
|
" );",
|
||||||
|
"};",
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const SEARCH_PANEL = componentCard({
|
||||||
|
name: "SearchPanel",
|
||||||
|
fold: "open",
|
||||||
|
tags: [EXPORTED, CLIENT],
|
||||||
|
props: [["initialQuery", "字符串", "—", "是"]],
|
||||||
|
});
|
||||||
|
|
||||||
|
const ROUTER = valueCard({
|
||||||
|
kind: HOOK_VALUE,
|
||||||
|
lang: VAR,
|
||||||
|
tint: ORIGIN.library,
|
||||||
|
title: "router",
|
||||||
|
name: "router",
|
||||||
|
calls: 1,
|
||||||
|
tip: "谁用了",
|
||||||
|
rows: [
|
||||||
|
fromRow("useRouter", "next/navigation 提供的钩子"),
|
||||||
|
line("说明", px("路由器,用于页面跳转", C.fg), { mono: false }),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const AUTH = valueCard({
|
||||||
|
kind: CONTEXT_VALUE,
|
||||||
|
lang: VAR,
|
||||||
|
title: "user, logout",
|
||||||
|
name: {
|
||||||
|
kind: "object",
|
||||||
|
items: [
|
||||||
|
{ name: "user", calls: 1, tip: "谁用了" },
|
||||||
|
{ name: "logout", color: C.fn, calls: 1, tip: "谁调用了" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
rows: [
|
||||||
|
fromRow("useContext", BUILTIN_TIP),
|
||||||
|
line("上下文", [link("AuthContext", CONTEXT_VALUE.color)]),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const QUERY = stateCard({
|
||||||
|
names: ["query", "setQuery"],
|
||||||
|
calls: [3, 1],
|
||||||
|
initial: cx("initialQuery"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const FILTER = valueCard({
|
||||||
|
kind: STATE,
|
||||||
|
lang: VAR,
|
||||||
|
title: "filter",
|
||||||
|
name: {
|
||||||
|
kind: "array",
|
||||||
|
items: [
|
||||||
|
{ name: "filter", what: "当前值", calls: 1, tip: "谁用了" },
|
||||||
|
{ name: "dispatch", color: C.fn, what: "派发函数", calls: 1, tip: "谁派发了动作" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
rows: [
|
||||||
|
fromRow("useReducer", BUILTIN_TIP),
|
||||||
|
line("处理函数", [link("filterReducer", C.fn)]),
|
||||||
|
line("状态类型", px("未标注", C.desc), { mono: false }),
|
||||||
|
line("初始值", [link("EMPTY_FILTER", C.constant)]),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const RESULTS = valueCard({
|
||||||
|
kind: HOOK_VALUE,
|
||||||
|
lang: VAR,
|
||||||
|
tint: ORIGIN.own,
|
||||||
|
title: "results, loading",
|
||||||
|
name: {
|
||||||
|
kind: "object",
|
||||||
|
items: [
|
||||||
|
{ name: "results", calls: 1, tip: "谁用了" },
|
||||||
|
{ name: "loading", calls: 1, tip: "谁用了" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
rows: [
|
||||||
|
fromRow("useSearch", "自定义钩子", true),
|
||||||
|
tableRow("参数", [{ name: "参数", color: C.variable }, { name: "值" }], [["query", cx("query")], ["filter", cx("filter")]]),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const PENDING = valueCard({
|
||||||
|
kind: TRANSITION,
|
||||||
|
lang: VAR,
|
||||||
|
title: "isPending, startTransition",
|
||||||
|
name: {
|
||||||
|
kind: "array",
|
||||||
|
items: [
|
||||||
|
{ name: "isPending", what: "是否正在过渡", calls: 1, tip: "谁用了" },
|
||||||
|
{ name: "startTransition", color: C.fn, what: "启动过渡的函数", calls: 1, tip: "谁调用了" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
rows: [fromRow("useTransition", BUILTIN_TIP)],
|
||||||
|
});
|
||||||
|
|
||||||
|
const INPUT_REF = valueCard({
|
||||||
|
kind: REF,
|
||||||
|
lang: VAR,
|
||||||
|
title: "inputRef",
|
||||||
|
name: "inputRef",
|
||||||
|
calls: 2,
|
||||||
|
tip: "谁用了",
|
||||||
|
rows: [
|
||||||
|
fromRow("useRef", BUILTIN_TIP),
|
||||||
|
line("指向类型", cx("HTMLInputElement")),
|
||||||
|
line("初始值", cx("空")),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const VISIBLE = valueCard({
|
||||||
|
kind: MEMO_VALUE,
|
||||||
|
lang: VAR,
|
||||||
|
title: "visible",
|
||||||
|
name: "visible",
|
||||||
|
calls: 1,
|
||||||
|
tip: "谁用了",
|
||||||
|
rows: [
|
||||||
|
fromRow("useMemo", BUILTIN_TIP),
|
||||||
|
line("计算", cx("results.filter((r) => r.hidden 不成立)")),
|
||||||
|
line("重算时机", changedAll(["results"])),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const SUBMIT = valueCard({
|
||||||
|
kind: MEMO_FN,
|
||||||
|
lang: FUNC,
|
||||||
|
stroke: C.fn,
|
||||||
|
tint: C.fn,
|
||||||
|
fold: "closed",
|
||||||
|
title: "submit",
|
||||||
|
name: "submit",
|
||||||
|
nameColor: C.fn,
|
||||||
|
calls: 1,
|
||||||
|
tip: "谁调用了",
|
||||||
|
rows: [
|
||||||
|
fromRow("useCallback", BUILTIN_TIP),
|
||||||
|
tableRow("参数", [{ name: "名称", color: C.variable }, { name: "类型", color: C.type }], [["e", cx("FormEvent")]]),
|
||||||
|
fnRow("函数体", null, 2),
|
||||||
|
line("重建时机", changedAll(["router", "query"])),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 一处 useEffect:没有名称一行,触发时机打头,执行的函数收成小框。 */
|
||||||
|
function effectCard(title, trigger, count) {
|
||||||
|
return valueCard({
|
||||||
|
kind: EFFECT,
|
||||||
|
lang: STMT,
|
||||||
|
fold: "closed",
|
||||||
|
title,
|
||||||
|
rows: [
|
||||||
|
line("触发时机", trigger, { mono: trigger !== ON_MOUNT }),
|
||||||
|
fromRow("useEffect", BUILTIN_TIP),
|
||||||
|
fnRow("执行", null, count),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// SearchPanel 返回的 JSX:条件写「当 … 时」,列表写「对 … 的每一项」并标 key,回调照译
|
||||||
|
const SEARCH_OUTLINE = [
|
||||||
|
{
|
||||||
|
line: [el("form"), at("onSubmit", [link("submit", C.fn)])],
|
||||||
|
kids: [
|
||||||
|
{ line: [el("input"), at("ref", [link("inputRef", C.variable)]), at("value", [link("query", C.variable)]), at("onChange", cx("(e) => setQuery(e.target.value)"))] },
|
||||||
|
{ line: [el("button"), at("type", px("\"button\"", C.string)), at("onClick", cx("() => dispatch({ type: \"toggleHidden\" })")), words(px("\"显示隐藏项\"", C.string))] },
|
||||||
|
{ line: [cond(cx("当 isPending 时")), el("span"), words(px("\"跳转中…\"", C.string))] },
|
||||||
|
{ line: [cond(cx("当 loading 时")), el("p"), words(px("\"搜索中…\"", C.string))] },
|
||||||
|
{
|
||||||
|
line: [el("ul")],
|
||||||
|
kids: [
|
||||||
|
{
|
||||||
|
line: [cond(cx("对 visible 的每一项 r"), C.loop), words([seg("key ", C.desc)].concat(cx("r.id")))],
|
||||||
|
kids: [{ line: [el("li"), words([seg("{", C.desc)].concat(cx("r.title"), [seg("}", C.desc)]))] }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
windowOf("Prototype / React 内置钩子 · 搜索面板", (WIN_W + BOARD_GAP) * 2, SEARCH, [
|
||||||
|
{ from: 1, to: 2 },
|
||||||
|
{ from: 3, to: 3, draw: importCard("\"react\"", 8) },
|
||||||
|
{ from: 4, to: 5, draw: importCard("\"next/navigation\"", 1) },
|
||||||
|
{ from: 6, to: 6, draw: importCard("\"@/lib/auth\"", 1) },
|
||||||
|
{ from: 7, to: 7, draw: importCard("\"@/hooks/use-search\"", 1) },
|
||||||
|
{ from: 8, to: 9, draw: importCard("\"./filter\"", 2) },
|
||||||
|
{ from: 10, to: 10, draw: SEARCH_PANEL },
|
||||||
|
{ from: 11, to: 11, depth: 1, draw: ROUTER },
|
||||||
|
{ from: 12, to: 12, depth: 1, draw: AUTH },
|
||||||
|
{ from: 13, to: 13, depth: 1, draw: QUERY },
|
||||||
|
{ from: 14, to: 14, depth: 1, draw: FILTER },
|
||||||
|
{ from: 15, to: 15, depth: 1, draw: RESULTS },
|
||||||
|
{ from: 16, to: 16, depth: 1, draw: PENDING },
|
||||||
|
{ from: 17, to: 17, depth: 1, draw: INPUT_REF },
|
||||||
|
{ from: 18, to: 19, depth: 1, draw: VISIBLE },
|
||||||
|
{ from: 20, to: 24, depth: 1, draw: SUBMIT },
|
||||||
|
{ from: 25, to: 28, depth: 1, draw: effectCard("仅挂载时", ON_MOUNT, 1) },
|
||||||
|
{ from: 29, to: 32, depth: 1, draw: effectCard("user、logout 变化时", changedAll(["user", "logout"]), 1) },
|
||||||
|
{ from: 33, to: 45, depth: 1, last: true, draw: renderCard(SEARCH_OUTLINE, "SearchPanel") },
|
||||||
|
{ from: 46, to: 46 },
|
||||||
|
]);
|
||||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user