📝 docs(vscode): 代码阅读 Rust 原型

This commit is contained in:
cruldra committed 2026-10-04 15:10:31 +08:00
1 parent ff071f132f
commit e322f01edb
5 files changed
+15361

No files matched your search

+660
View File
@@ -0,0 +1,660 @@
// 示例是手写的短 Rust,不是仓库里的文件。名字照桌面端和接口服务:AgentRow、start_server。
// 库只覆盖 serde、thiserror、axum、sqlx、tauri。tokio 不单独成卡,异步写在函数上。
// 颜色取自代码阅读原型里的 VS Code Dark Modern。
const C = {
editor: "#1f1f1f",
side: "#181818",
border: "#2b2b2b",
borderStrong: "#3c3c3c",
fg: "#cccccc",
fgStrong: "#e8e8e8",
desc: "#9d9d9d",
dim: "#6e7681",
activityIdle: "#868686",
activityOn: "#d7d7d7",
focus: "#0078d4",
selection: "#04395e",
input: "#313131",
field: "#111111",
toggleOn: "#37373d",
comment: "#6a9955",
control: "#c586c0",
storage: "#569cd6",
variable: "#9cdcfe",
fn: "#dcdcaa",
type: "#4ec9b0",
string: "#ce9178",
folder: "#8b9ba8",
rust: "#dea584",
toml: "#c0754a",
added: "#2ea043",
get: "#61affe",
};
const KIND = {
use: { label: "导入", color: "#c586c0", icon: "import" },
struct: { label: "结构体", color: "#4ec9b0", icon: "braces" },
enum: { label: "枚举", color: "#b180d7", icon: "list-ordered" },
impl: { label: "实现", color: "#dcdcaa", icon: "square-function" },
fn: { label: "函数", color: "#dcdcaa", icon: "square-function" },
trait: { label: "特质", color: "#569cd6", icon: "boxes" },
error: { label: "错误", color: "#f14c4c", icon: "triangle-alert" },
route: { label: "接口", color: "#61afef", icon: "globe" },
query: { label: "查询", color: "#ce9178", icon: "database" },
command: { label: "命令", color: "#c0754a", icon: "app-window" },
};
const TREE = [
{ kind: "folder", name: "agents", open: true, depth: 0 },
{ kind: "folder", name: "src", open: true, depth: 1 },
{ kind: "file", name: "agent.rs", depth: 2 },
{ kind: "file", name: "error.rs", depth: 2 },
{ kind: "file", name: "routes.rs", depth: 2 },
{ kind: "file", name: "store.rs", depth: 2 },
{ kind: "file", name: "Cargo.toml", depth: 1, ext: "TM" },
];
function solid(color) {
return [{ type: "solid", color }];
}
function text(parent, content, o) {
const s = String(content);
const opt = o || {};
const mono = Boolean(opt.mono) && !/[^\x00-\x7F]/.test(s);
const node = {
type: "text",
name: opt.name || s.slice(0, 32),
content: s,
fontFamily: mono ? "Menlo" : "PingFang SC",
fontSize: opt.size || 13,
fontWeight: opt.weight || 400,
lineHeight: opt.lh || 1.45,
fill: solid(opt.color || C.fg),
height: "fit_content",
};
if (opt.wrap) {
node.width = opt.width || "fill_container";
node.textGrowth = "fixed-width";
} else {
node.width = "fit_content";
node.textGrowth = "auto";
}
return I(parent, node);
}
function icon(parent, name, color, size) {
return I(parent, {
type: "icon_font",
name: name,
iconFontName: name,
width: size,
height: size,
fill: solid(color),
});
}
function row(parent, name, o) {
const opt = o || {};
return I(parent, {
type: "frame",
name,
layout: "horizontal",
width: opt.width || "fill_container",
height: opt.height || "fit_content",
gap: opt.gap == null ? 8 : opt.gap,
padding: opt.padding,
alignItems: opt.align || "center",
justifyContent: opt.justify,
fill: opt.fill ? solid(opt.fill) : undefined,
cornerRadius: opt.radius,
});
}
function col(parent, name, o) {
const opt = o || {};
return I(parent, {
type: "frame",
name,
layout: "vertical",
width: opt.width || "fill_container",
height: "fit_content",
gap: opt.gap == null ? 4 : opt.gap,
padding: opt.padding,
alignItems: opt.align,
fill: opt.fill ? solid(opt.fill) : undefined,
cornerRadius: opt.radius,
stroke: opt.stroke ? { thickness: 1, fill: solid(opt.stroke) } : undefined,
clipContent: false,
});
}
function prop(parent, label, value, color, mono) {
const line = row(parent, "Prop / " + label, { gap: 12, align: "start" });
const key = I(line, { type: "frame", name: "Key", width: 72, height: "fit_content", layout: "vertical" });
text(key, label, { size: 12, color: C.desc, wrap: true });
const val = I(line, { type: "frame", name: "Value", width: "fill_container", height: "fit_content", layout: "vertical" });
if (typeof value === "function") value(val);
else text(val, value, { size: 13, color: color || C.fg, mono: mono !== false, wrap: true });
return line;
}
function tag(parent, label, color) {
const pill = I(parent, {
type: "frame",
name: "Tag / " + label,
layout: "horizontal",
width: "fit_content",
height: "fit_content",
padding: [1, 6],
cornerRadius: 4,
alignItems: "center",
fill: solid("#2a2a2a"),
stroke: { thickness: 1, fill: solid(color) },
});
text(pill, label, { size: 11, color });
return pill;
}
function nameLine(parent, name, color, extra) {
const line = row(parent, "Prop / 名称", { gap: 12 });
const key = I(line, { type: "frame", name: "Key", width: 72, height: "fit_content" });
text(key, "名称", { size: 12, color: C.desc });
text(line, name, { size: 13, color: color || C.fn, mono: true, weight: 600 });
if (extra) extra(line);
}
function table(parent, columns, rows) {
const wrap = col(parent, "Table wrap", { gap: 0, padding: [2, 0, 0, 4] });
const grid = I(wrap, {
type: "frame",
name: "Table",
layout: "vertical",
width: "fill_container",
height: "fit_content",
cornerRadius: 4,
stroke: { thickness: 1, fill: solid(C.borderStrong) },
clipContent: true,
gap: 0,
});
function draw(cells, header) {
const line = I(grid, {
type: "frame",
name: header ? "Header" : "Row",
layout: "horizontal",
width: "fill_container",
height: "fit_content",
alignItems: "stretch",
fill: header ? solid(C.side) : undefined,
});
cells.forEach((cell, index) => {
const box = I(line, {
type: "frame",
name: columns[index].name,
layout: "vertical",
width: columns[index].width,
height: "fit_content",
padding: [5, 8],
justifyContent: "center",
});
if (typeof cell === "function") cell(box);
else text(box, cell, {
size: 12,
color: header ? C.desc : (columns[index].color || C.fg),
mono: !header && columns[index].mono !== false,
wrap: true,
});
});
}
draw(columns.map((column) => column.name), true);
for (const cells of rows) draw(cells, false);
}
function tools(parent) {
const bar = row(parent, "Tools", { width: "fit_content", gap: 2 });
for (const name of ["sparkles", "pencil"]) {
const button = I(bar, {
type: "frame",
name: name,
layout: "horizontal",
width: 22,
height: 22,
alignItems: "center",
justifyContent: "center",
cornerRadius: 4,
});
icon(button, name, C.desc, 14);
}
}
function card(parent, kind, draw) {
const node = I(parent, {
type: "frame",
name: kind.label,
layout: "horizontal",
width: "fill_container",
height: "fit_content",
gap: 10,
padding: [8, 10, 8, 8],
cornerRadius: 8,
alignItems: "start",
fill: solid("#242424"),
stroke: { thickness: 1, fill: solid(kind.color) },
clipContent: false,
});
const fold = I(node, {
type: "frame",
name: "Fold",
layout: "horizontal",
width: 14,
height: 20,
alignItems: "center",
justifyContent: "center",
});
icon(fold, "chevron-down", C.desc, 14);
icon(node, kind.icon, kind.color, 18);
const content = col(node, "Content", { gap: 4 });
draw(content);
tools(node);
return node;
}
function activity(parent) {
const bar = I(parent, {
type: "frame",
name: "Activity bar",
layout: "vertical",
width: 48,
height: "fit_content",
justifyContent: "space_between",
fill: solid(C.side),
});
const top = col(bar, "Top", { width: 48, gap: 0 });
const icons = ["files", "search", "git-branch", "play", "monitor", "blocks", "rocket", "book-open-text"];
for (const name of icons) {
const on = name === "book-open-text";
const cell = I(top, {
type: "frame",
name: "Item / " + name,
layout: "horizontal",
width: 48,
height: 42,
alignItems: "center",
justifyContent: on ? "start" : "center",
gap: on ? 10 : 0,
});
if (on) I(cell, { type: "rectangle", name: "Active bar", width: 2, height: 42, fill: solid(C.focus) });
icon(cell, name, on ? C.activityOn : C.activityIdle, 22);
}
return bar;
}
function sidebar(parent, selected) {
const side = col(parent, "Sidebar", { width: 280, gap: 0, fill: C.side });
const header = row(side, "Header", { height: 34, padding: [0, 12, 0, 16], justify: "space_between" });
text(header, "代码阅读", { size: 11, color: C.fg });
const actions = row(header, "Actions", { width: "fit_content", gap: 10 });
for (const name of ["git-pull-request", "file-plus", "refresh-cw"]) icon(actions, name, C.fg, 14);
const list = col(side, "Tree", { gap: 0, padding: [4, 0, 8, 0] });
for (const item of TREE) {
const line = I(list, {
type: "frame",
name: item.name,
layout: "horizontal",
width: "fill_container",
height: 26,
alignItems: "center",
gap: 6,
padding: [0, 8, 0, 10 + item.depth * 14],
fill: item.name === selected ? solid(C.selection) : undefined,
});
if (item.kind === "folder") icon(line, item.open ? "chevron-down" : "chevron-right", C.fg, 14);
else I(line, { type: "frame", name: "Twisty", width: 14, height: 14 });
if (item.kind === "folder") icon(line, "folder", C.folder, 15);
else text(line, item.ext || "RS", { size: 9, weight: 700, color: item.ext === "TM" ? C.toml : C.rust });
text(line, item.name, { size: 13, color: item.name === selected ? C.fgStrong : C.fg });
}
return side;
}
function editorChrome(parent, file) {
const bar = row(parent, "Editor title", { height: 36, justify: "space_between", fill: C.side, gap: 0 });
const tab = I(bar, {
type: "frame",
name: "Tab",
layout: "vertical",
width: 148,
height: "fill_container",
fill: solid(C.editor),
gap: 0,
});
I(tab, { type: "rectangle", name: "Active", width: "fill_container", height: 2, fill: solid(C.focus) });
const label = row(tab, "Tab label", { height: "fill_container", padding: [0, 12], gap: 6 });
text(label, "RS", { size: 9, weight: 700, color: C.rust });
text(label, file, { size: 13, color: C.fg });
const mode = I(bar, {
type: "frame",
name: "Mode",
layout: "horizontal",
width: "fit_content",
height: "fit_content",
padding: [2, 2],
gap: 2,
cornerRadius: 5,
alignItems: "center",
fill: solid(C.field),
stroke: { thickness: 1, fill: solid(C.borderStrong) },
});
const edit = row(mode, "Seg / 编辑", { width: "fit_content", padding: [2, 8], gap: 4, radius: 3 });
icon(edit, "pencil", C.desc, 13);
text(edit, "编辑", { size: 12, color: C.desc });
const preview = row(mode, "Seg / 预览", { width: "fit_content", padding: [2, 8], gap: 4, radius: 3, fill: C.toggleOn });
icon(preview, "eye", C.fgStrong, 13);
text(preview, "预览", { size: 12, color: C.fgStrong });
I(bar, { type: "frame", name: "Pad", width: 8, height: 8 });
}
function crumb(parent, file) {
const line = row(parent, "Breadcrumb", { padding: [6, 16], gap: 6 });
const parts = ["agents", "src", file];
parts.forEach((part, index) => {
if (index) text(line, "/", { size: 12, color: C.dim });
text(line, part, { size: 12, color: index === parts.length - 1 ? C.fg : C.desc, mono: part.endsWith(".rs") });
});
}
function filters(parent, chips, count) {
const bar = row(parent, "Filter", { height: 36, padding: [0, 16], gap: 8 });
text(bar, "筛选", { size: 12, color: C.desc });
for (const chip of chips) {
const item = row(bar, "Chip / " + chip, { width: "fit_content", height: 24, padding: [0, 8], gap: 4, radius: 4, fill: C.input });
text(item, chip, { size: 12, color: C.fg });
icon(item, "chevron-down", C.desc, 12);
}
const spacer = I(bar, { type: "frame", name: "Spacer", width: "fill_container", height: 8 });
text(bar, count + " 块", { size: 12, color: C.desc });
return spacer;
}
function status(parent) {
const bar = row(parent, "Status", {
height: 26,
padding: [0, 12],
justify: "space_between",
fill: C.side,
});
const left = row(bar, "Left", { width: "fit_content", gap: 12 });
const branch = row(left, "Branch", { width: "fit_content", gap: 4 });
icon(branch, "git-branch", C.fg, 13);
text(branch, "master", { size: 12 });
const right = row(bar, "Right", { width: "fit_content", gap: 16 });
text(right, "代码阅读", { size: 12 });
text(right, "Rust", { size: 12 });
}
function windowOf(name, x, file, chips, count, drawCards) {
const win = I(null, {
type: "frame",
name,
x,
y: 0,
width: 1280,
height: "fit_content",
layout: "vertical",
gap: 0,
fill: solid(C.side),
clipContent: false,
});
const title = row(win, "Title bar", { height: 36, padding: [0, 12], justify: "space_between" });
const lights = row(title, "Lights", { width: "fit_content", gap: 8 });
for (const color of ["#ff5f57", "#febc2e", "#28c840"]) {
I(lights, { type: "ellipse", width: 12, height: 12, fill: solid(color) });
}
text(title, "agents — " + file, { size: 12, color: C.desc });
I(title, { type: "frame", name: "Balance", width: 52, height: 12 });
const body = I(win, {
type: "frame",
name: "Body",
layout: "horizontal",
width: "fill_container",
height: "fit_content",
gap: 0,
alignItems: "start",
fill: solid(C.side),
clipContent: false,
});
activity(body);
sidebar(body, file);
const editor = col(body, "Editor", { gap: 0, fill: C.editor });
editorChrome(editor, file);
crumb(editor, file);
filters(editor, chips, count);
const preview = col(editor, "Preview", { gap: 10, padding: [8, 20, 24, 20] });
drawCards(preview);
status(win);
return win;
}
function useCard(parent, source, names) {
card(parent, KIND.use, (content) => {
prop(content, "来源", source, C.string, true);
prop(content, "导入项", names.length + " 个", C.desc, false);
table(content,
[{ name: "名字", width: 220, color: C.variable }, { name: "别名", width: 220, color: C.dim }],
names.map((item) => [item.name, item.alias || "—"]));
});
}
function languageCards(preview) {
useCard(preview, "serde", [{ name: "Serialize" }]);
useCard(preview, "std::sync", [{ name: "Arc" }]);
card(preview, KIND.struct, (content) => {
nameLine(content, "AgentRow", C.type);
prop(content, "说明", "桌面端列表里的一行代理。", C.comment, false);
prop(content, "可见性", "公开", C.control, false);
prop(content, "字段", "2 个", C.desc, false);
table(content,
[{ name: "字段", width: 180, color: C.variable }, { name: "类型", width: 220, color: C.type }, { name: "可见性", width: 160 }],
[
["id", "Uuid", { text: "公开", color: C.control }],
["name", "String", { text: "本模块", color: C.desc }],
].map(([field, type, vis]) => [field, type, (box) => text(box, vis.text, { size: 12, color: vis.color })]));
});
card(preview, KIND.enum, (content) => {
nameLine(content, "AgentState", C.type);
prop(content, "说明", "这个代理现在处在哪一步。", C.comment, false);
prop(content, "变体", "3 个", C.desc, false);
table(content,
[{ name: "变体", width: 220, color: C.type }, { name: "携带的数据", width: 360 }],
[
["Idle", "—"],
["Running", "{ pid: u32 }"],
["Failed", "String"],
]);
});
card(preview, KIND.impl, (content) => {
nameLine(content, "AgentRow 的方法", C.fn);
prop(content, "方法", "1 个", C.desc, false);
table(content,
[{ name: "名称", width: 180, color: C.fn }, { name: "参数", width: 320 }, { name: "返回", width: 140, color: C.dim }],
[["rename", "&mut self, name: String", "()"]]);
const nested = col(content, "展开 / rename", { gap: 0, padding: [4, 0, 0, 8] });
card(nested, KIND.fn, (body) => {
nameLine(body, "rename", C.fn);
prop(body, "参数", "2 个", C.desc, false);
table(body,
[{ name: "名称", width: 180, color: C.variable }, { name: "类型", width: 280 }],
[["self", "&mut self"], ["name", "String"]]);
});
});
card(preview, KIND.fn, (content) => {
nameLine(content, "start_server", C.fn);
prop(content, "说明", "在本机拉起接口服务。", C.comment, false);
prop(content, "异步", "是", C.storage, false);
prop(content, "可见性", "公开", C.control, false);
prop(content, "输入参数", "1 个", C.desc, false);
table(content,
[{ name: "名称", width: 180, color: C.variable }, { name: "类型", width: 280, color: C.type }],
[["addr", "SocketAddr"]]);
prop(content, "返回", "Result<(), StartError>", C.type, true);
prop(content, "分情况", "2 支", C.desc, false);
table(content,
[{ name: "分支", width: 220, color: C.type }, { name: "做什么", width: 420, mono: false }],
[
["Ok(listener)", "记下监听地址"],
["Err(err)", "返回 StartError"],
]);
});
card(preview, KIND.trait, (content) => {
nameLine(content, "Store", C.storage);
prop(content, "说明", "按 id 读写代理。", C.comment, false);
prop(content, "方法", "2 个", C.desc, false);
table(content,
[{ name: "名称", width: 140, color: C.fn }, { name: "参数", width: 280 }, { name: "返回", width: 280, color: C.type }],
[
["get", "&self, id: Uuid", "Result<AgentRow, StoreError>"],
["save", "&mut self, row: AgentRow", "Result<(), StoreError>"],
]);
});
}
function libraryCards(preview) {
card(preview, KIND.struct, (content) => {
nameLine(content, "AgentRow", C.type, (line) => tag(line, "可序列化", C.string));
prop(content, "说明", "桌面端列表里的一行代理。", C.comment, false);
prop(content, "可见性", "公开", C.control, false);
prop(content, "选项", "字段名改驼峰", C.string, false);
prop(content, "字段", "3 个", C.desc, false);
table(content,
[
{ name: "字段", width: 140, color: C.variable },
{ name: "类型", width: 160, color: C.type },
{ name: "可见性", width: 120 },
{ name: "序列化", width: 220 },
],
[
["id", "Uuid", (box) => text(box, "公开", { size: 12, color: C.control }), (box) => text(box, "改名 agent_id", { size: 12, color: C.string, wrap: true })],
["name", "String", (box) => text(box, "公开", { size: 12, color: C.control }), "—"],
["secret", "String", (box) => text(box, "本模块", { size: 12, color: C.desc }), (box) => text(box, "不输出", { size: 12, color: C.desc })],
]);
});
card(preview, KIND.error, (content) => {
nameLine(content, "AgentError", "#f14c4c");
prop(content, "说明", "读写代理时出的错。", C.comment, false);
prop(content, "变体", "2 个", C.desc, false);
table(content,
[
{ name: "变体", width: 150, color: C.type },
{ name: "何时", width: 200, mono: false },
{ name: "展示", width: 280 },
],
[
["NotFound", "这个 id 没有代理", (box) => text(box, "agent {id} 不存在", { size: 12, color: C.string, wrap: true })],
["Upstream", "调用上游失败", (box) => {
const line = row(box, "展示", { gap: 6 });
text(line, "上游错误", { size: 12, color: C.string });
tag(line, "透传来源", C.control);
}],
]);
});
card(preview, KIND.route, (content) => {
nameLine(content, "get_agent", C.fn);
prop(content, "说明", "按 id 取一个代理。", C.comment, false);
prop(content, "异步", "是", C.storage, false);
prop(content, "请求", "GET /agents/{id}", C.desc, true);
table(content,
[{ name: "项", width: 140 }, { name: "值", width: 420 }],
[
[(box) => text(box, "方法", { size: 12, color: C.desc }), (box) => text(box, "GET", { size: 12, color: C.get, weight: 600, mono: true })],
[(box) => text(box, "路径", { size: 12, color: C.desc }), (box) => text(box, "/agents/{id}", { size: 12, color: C.string, mono: true })],
[(box) => text(box, "路径参数", { size: 12, color: C.desc }), (box) => text(box, "id: Uuid", { size: 12, color: C.variable, mono: true })],
]);
prop(content, "返回", "Result<Json<AgentRow>, StatusCode>", C.type, true);
});
card(preview, KIND.query, (content) => {
prop(content, "说明", "按 id 取一行。", C.comment, false);
prop(content, "SQL", "SELECT id, name FROM agents WHERE id = $1", C.fg, true);
prop(content, "参数", "$1 = id", C.variable, true);
prop(content, "返回类型", "AgentRow", C.type, true);
});
card(preview, KIND.command, (content) => {
nameLine(content, "rename_agent", C.fn, (line) => tag(line, "桌面命令", C.toml));
prop(content, "说明", "让窗口把这一行改名。", C.comment, false);
prop(content, "异步", "是", C.storage, false);
prop(content, "参数", "2 个", C.desc, false);
table(content,
[
{ name: "名称", width: 140, color: C.variable },
{ name: "类型", width: 200, color: C.type },
{ name: "来源", width: 200, mono: false },
],
[
["state", "State<App>", "窗口状态"],
["name", "String", "调用方传入"],
]);
prop(content, "返回", "Result<String, String>", C.type, true);
});
}
function note(parent, label, color, sentence) {
const line = row(parent, "Note / " + label, { gap: 10, align: "start" });
if (label) tag(line, label, color || C.desc);
const body = I(line, { type: "frame", name: "Sentence", width: "fill_container", height: "fit_content", layout: "vertical" });
text(body, sentence, { size: 13, color: C.fg, wrap: true });
}
function legend(x) {
const board = I(null, {
type: "frame",
name: "Prototype / Rust · 标记说明",
x,
y: 0,
width: 880,
height: "fit_content",
layout: "vertical",
gap: 14,
padding: 28,
fill: solid(C.editor),
clipContent: false,
});
text(board, "标记说明", { size: 18, weight: 600, color: C.fgStrong });
text(board, "可见性", { size: 14, weight: 600, color: C.fgStrong });
note(board, "公开", C.control, "pub,别的模块也能用。");
note(board, "本模块", C.desc, "没写 pub,只有这个文件里能用。");
note(board, "父模块", C.string, "pub(super),上一层模块能用。");
text(board, "语言卡片", { size: 14, weight: 600, color: C.fgStrong });
note(board, "导入", KIND.use.color, "一条 use。来源是路径,下面表是名字和别名。");
note(board, "结构体", KIND.struct.color, "字段表:字段、类型、可见性。没有的属性不占一行。");
note(board, "枚举", KIND.enum.color, "变体表。没带数据的格子是破折号,不硬凑类型。");
note(board, "实现", KIND.impl.color, "标题写成「某某的方法」。点方法名展开一张函数子卡片。");
note(board, "函数", KIND.fn.color, "异步写「是」。函数体里的 match 收成「分情况」,不展开源码。");
note(board, "特质", KIND.trait.color, "方法表只列签名,不展开函数体。");
text(board, "库", { size: 14, weight: 600, color: C.fgStrong });
note(board, "改名", C.string, "serde rename:序列化那一列写成「改名 agent_id」。");
note(board, "不输出", C.desc, "serde skip:这一列写成「不输出」。没标的是破折号。");
note(board, "驼峰", C.string, "rename_all = camelCase 收成选项行「字段名改驼峰」。");
note(board, "展示", KIND.error.color, "thiserror 的文案写在「展示」列。透传的上游错误另标「透传来源」。");
note(board, "接口", KIND.route.color, "axum 处理函数和 FastAPI 接口是同一张卡:请求表加返回。");
note(board, "查询", KIND.query.color, "sqlx 一张小卡:一句说明、等宽 SQL、参数、返回类型。");
note(board, "桌面命令", KIND.command.color, "tauri 命令在名称后标「桌面命令」。参数写明来自窗口状态还是调用方。");
note(board, "异步", C.storage, "tokio 不单独做卡。spawn 和 select 仍是普通调用。");
return board;
}
windowOf("Prototype / Rust · 语言", 0, "agent.rs", ["种类", "可见性"], 7, languageCards);
windowOf("Prototype / Rust · 库", 1360, "routes.rs", ["种类", "库"], 5, libraryCards);
legend(2720);
Binary file not shown.

After

Width:  |  Height:  |  Size: 384 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 KiB

File diff suppressed because it is too large. Load diff