diff --git a/package-lock.json b/package-lock.json
index 0640e94..9dcaef7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,6 +11,7 @@
"@svgdotjs/svg.js": "^3.2.4",
"@types/lodash": "^4.17.16",
"async-mutex": "^0.5.0",
+ "highlight.js": "^11.11.1",
"lodash": "^4.17.21",
"log-symbols": "^7.0.0",
"marked": "^12.0.0",
@@ -2586,6 +2587,15 @@
"he": "bin/he"
}
},
+ "node_modules/highlight.js": {
+ "version": "11.11.1",
+ "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
+ "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
"node_modules/hookable": {
"version": "5.5.3",
"resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
diff --git a/package.json b/package.json
index 16d2349..ac7b8d0 100644
--- a/package.json
+++ b/package.json
@@ -17,6 +17,7 @@
"@svgdotjs/svg.js": "^3.2.4",
"@types/lodash": "^4.17.16",
"async-mutex": "^0.5.0",
+ "highlight.js": "^11.11.1",
"lodash": "^4.17.21",
"log-symbols": "^7.0.0",
"marked": "^12.0.0",
diff --git a/public/doc/01_water_led/water_led.md b/public/doc/01/doc.md
similarity index 100%
rename from public/doc/01_water_led/water_led.md
rename to public/doc/01/doc.md
diff --git a/public/doc/01_water_led/images/1.png b/public/doc/01/images/1.png
similarity index 100%
rename from public/doc/01_water_led/images/1.png
rename to public/doc/01/images/1.png
diff --git a/public/doc/01_water_led/images/2.png b/public/doc/01/images/2.png
similarity index 100%
rename from public/doc/01_water_led/images/2.png
rename to public/doc/01/images/2.png
diff --git a/public/doc/01_water_led/images/3.png b/public/doc/01/images/3.png
similarity index 100%
rename from public/doc/01_water_led/images/3.png
rename to public/doc/01/images/3.png
diff --git a/public/doc/01_water_led/images/4.png b/public/doc/01/images/4.png
similarity index 100%
rename from public/doc/01_water_led/images/4.png
rename to public/doc/01/images/4.png
diff --git a/public/doc/02_key/key.md b/public/doc/02/doc.md
similarity index 100%
rename from public/doc/02_key/key.md
rename to public/doc/02/doc.md
diff --git a/public/doc/02_key/images/1.png b/public/doc/02/images/1.png
similarity index 100%
rename from public/doc/02_key/images/1.png
rename to public/doc/02/images/1.png
diff --git a/public/doc/02_key/images/2.png b/public/doc/02/images/2.png
similarity index 100%
rename from public/doc/02_key/images/2.png
rename to public/doc/02/images/2.png
diff --git a/public/doc/02_key/images/3.png b/public/doc/02/images/3.png
similarity index 100%
rename from public/doc/02_key/images/3.png
rename to public/doc/02/images/3.png
diff --git a/public/doc/02_key/images/4.png b/public/doc/02/images/4.png
similarity index 100%
rename from public/doc/02_key/images/4.png
rename to public/doc/02/images/4.png
diff --git a/public/doc/02_key/images/5.png b/public/doc/02/images/5.png
similarity index 100%
rename from public/doc/02_key/images/5.png
rename to public/doc/02/images/5.png
diff --git a/public/doc/02_key/images/6.png b/public/doc/02/images/6.png
similarity index 100%
rename from public/doc/02_key/images/6.png
rename to public/doc/02/images/6.png
diff --git a/public/doc/02_key/images/7.png b/public/doc/02/images/7.png
similarity index 100%
rename from public/doc/02_key/images/7.png
rename to public/doc/02/images/7.png
diff --git a/public/doc/11/doc.md b/public/doc/11/doc.md
new file mode 100644
index 0000000..fefa7c7
--- /dev/null
+++ b/public/doc/11/doc.md
@@ -0,0 +1,400 @@
+# 进阶-1-密码锁实验
+
+## 1.1 章节导读
+本章作为进阶的第一个实验,主要学习状态机的写法和使用,同时联系前面所学的数码管和矩阵键盘,完成一个密码锁的设计。
+
+
+## 1.2 理论学习
+### 1.2.1 FSM状态机
+
+在数字逻辑设计中,**有限状态机(FSM, Finite State Machine)**是一种根据输入和当前状态决定下一个状态和输出的模型,广泛用于顺序逻辑电路的控制部分。
+
+在本实验中,我们将使用 FSM 构建密码锁的控制逻辑,用于管理**按键输入过程、密码比对、开锁显示、错误处理等多个步骤**。
+
+FSM 通常包含以下几个组成部分:
+
+- **状态定义(State)**:用来描述系统当前所处的逻辑阶段。例如:待输入、输入中、校验中、成功、失败等。
+
+- **状态转移条件(Transition)**:根据输入信号(如按键、定时器、复位)从一个状态跳转到另一个状态。
+
+- **输出控制(Output)**:每个状态下系统应有的行为,比如更新数码管、检测密码、拉高开锁信号等。
+
+常见的 FSM 类型包括:
+
+- **Moore 状态机:**输出只与当前状态有关,结构更稳定;
+- **Mealy 状态机:**输出与当前状态和输入有关,反应更灵敏。
+
+在本例中我们要设计一个状态机去对密码锁进行控制。首先我们应该先给密码锁分一下他会处于什么状态,每个状态有什么输出(本例中将密码锁设计成下述4个状态):
+
+1. SETUP状态:该状态下可以设置4位密码,输入4位数字后按#键设置密码有效,*清空设置,数码管输出4位数字输入
+2. LOCK状态:锁定状态,可以输入密码解锁,按#确定,*键清空输入,数码管输出4位数字输入
+3. ERROR状态:如果输入密码错误,或者操作错误,进入此状态,数码管输出ERROR
+4. UNLOCK状态:解锁状态,可以按*重设密码,也可以按#重新锁定,数码管输出UNLOCK
+
+然后确定状态之间如何进行转移:
+
+1. SETUP状态:输入4位数字后按#键设置密码有效,有效后进入LOCK状态
+2. LOCK状态:输入密码,按#确定后如果密码正确进入UNLOCK状态,如果错误进入ERROR状态
+3. ERROR状态:按下任意按键后进入LOCK状态
+4. UNLOCK状态:按下#键进入LOCK状态,按*键进入SETUP状态重设密码
+
+根据上述状态转移逻辑,我们可以画出状态转移图,状态转移图如下图所示:
+
+
+
+
+
+ 图1.状态转移图
+
+
+
+我们已经了解了本次实验所使用的状态机,那么如何使用verilog编写状态机呢?主要有三种方法,分别是:三段式状态机,二段式状态机,一段式状态机。
+
+三段式状态机写法如下:
+
+- 状态机第一段,时序逻辑,非阻塞赋值,传递寄存器的状态。
+- 状态机第二段,组合逻辑,阻塞赋值,根据当前状态和当前输入,确定下一个状态机的状态。
+- 状态机第三代,时序逻辑,非阻塞赋值,因为是 Mealy 型状态机,根据当前状态和当前输入,确定输出信号。
+
+二段式状态机将三段式状态机二三段糅合在一起,一段式状态机则将三段式状态机三段融合。推荐使用三段式状态机,只有在状态转移逻辑非常简单,状态很少时会采用一段式状态机。
+
+### 1.2.2 数码管
+
+见基础实验3
+
+### 1.2.3 矩阵键盘
+
+见基础实验4
+
+## 1.3 实战演练
+### 1.3.1 系统架构
+``` verilog
+系统框图:
+```
+
+### 1.3.2 模块设计
+首先是密码锁状态机逻辑,本例采用三段式状态机写法。代码如下:
+
+#### password_lock
+
+```verilog
+module password_lock(
+ input wire clk,
+ input wire rstn,
+ input wire [15:0] key_trigger,
+ output reg [8*8-1:0] assic_seg,
+ output wire [7:0] seg_point
+);
+
+/*
+ K00 K01 K02 K03 | 1 2 3 A
+ |
+ K04 K05 K06 K07 | 4 5 6 B
+ |
+ K08 K09 K10 K11 | 7 8 9 C
+ |
+ K12 K13 K14 K15 | * 0 # D
+*/
+
+/*
+密码锁状态机设定:
+1. SETUP状态 :设置密码,按*清空输入,按#确认输入进入LOCK状态,不足4位#键无效
+2. LOCK状态 :锁定状态,按*清空输入,按#确认输入,不足4位#键无效,密码正确解锁,错误则进入ERROR状态
+3. ERROR状态 :密码错误状态,按任意键返回LCOK状态
+4. UNLOCK状态:解锁状态,按*重设密码,按#重新锁定,其余键无效
+
+1-D键为输入
+*为清空之前的输入
+#为确认输入
+
+*/
+
+wire flag_setup_password;
+wire flag_input_pass;
+wire flag_input_confirm;
+wire flag_error_return;
+wire flag_relock;
+wire flag_reset;
+
+localparam [2:0] ST_SETUP = 3'b001;
+localparam [2:0] ST_LOCK = 3'b010;
+localparam [2:0] ST_ERROR = 3'b100;
+localparam [2:0] ST_UNLOCK = 3'b101;
+
+reg [2:0] cu_st, nt_st;
+reg [4*4-1:0] password, input_password;
+reg [2:0] input_num;
+
+assign flag_setup_password = (cu_st == ST_SETUP) && (key_trigger[14]) && (input_num == 3'b100);
+assign flag_input_confirm = (cu_st == ST_LOCK) && (key_trigger[14]) && (input_num == 3'b100);
+assign flag_input_pass = (cu_st == ST_LOCK) && (password == input_password) && (input_num == 3'b100);
+assign flag_error_return = (cu_st == ST_ERROR) && (|key_trigger);
+assign flag_relock = (cu_st == ST_UNLOCK) && (key_trigger[14]);
+assign flag_reset = (cu_st == ST_UNLOCK) && (key_trigger[12]);
+//状态机第一段,传递寄存器状态
+always @(posedge clk or negedge rstn) begin
+ if(~rstn) cu_st <= ST_SETUP;
+ else cu_st <= nt_st;
+end
+//状态机第二段,确定下一个状态机状态
+always @(*) begin
+ case(cu_st)
+ ST_SETUP : nt_st <= (flag_setup_password)?(ST_LOCK):(ST_SETUP);
+ ST_LOCK : nt_st <= (flag_input_confirm)?((flag_input_pass)?(ST_UNLOCK):(ST_ERROR)):(ST_LOCK);
+ ST_ERROR : nt_st <= (flag_error_return)?(ST_LOCK):(ST_ERROR);
+ ST_UNLOCK: nt_st <= (flag_relock)?(ST_LOCK):((flag_reset)?(ST_SETUP):(ST_UNLOCK));
+ default : nt_st <= ST_SETUP;
+ endcase
+end
+//状态机第三段,根据状态和输入确定输出,这里由于信号较多,分了多个always块,也可以用case语句写在同一个always块中
+always @(posedge clk or negedge rstn) begin
+ if(~rstn) password <= 0;
+ else if((cu_st == ST_SETUP) && (input_num != 3'b100)) begin
+ if(key_trigger[00]) password <= {password[0+:3*4], 4'h1};
+ else if(key_trigger[01]) password <= {password[0+:3*4], 4'h2};
+ else if(key_trigger[02]) password <= {password[0+:3*4], 4'h3};
+ else if(key_trigger[03]) password <= {password[0+:3*4], 4'hA};
+ else if(key_trigger[04]) password <= {password[0+:3*4], 4'h4};
+ else if(key_trigger[05]) password <= {password[0+:3*4], 4'h5};
+ else if(key_trigger[06]) password <= {password[0+:3*4], 4'h6};
+ else if(key_trigger[07]) password <= {password[0+:3*4], 4'hB};
+ else if(key_trigger[08]) password <= {password[0+:3*4], 4'h7};
+ else if(key_trigger[09]) password <= {password[0+:3*4], 4'h8};
+ else if(key_trigger[10]) password <= {password[0+:3*4], 4'h9};
+ else if(key_trigger[11]) password <= {password[0+:3*4], 4'hC};
+ else if(key_trigger[12]) password <= 0;
+ else if(key_trigger[13]) password <= {password[0+:3*4], 4'h0};
+ else if(key_trigger[14]) password <= password;
+ else if(key_trigger[15]) password <= {password[0+:3*4], 4'hD};
+ else password <= password;
+ end else password <= password;
+end
+
+always @(posedge clk or negedge rstn) begin
+ if(~rstn) input_password <= 0;
+ else if(cu_st == ST_LOCK) begin
+ if(input_num == 3'b100) input_password <= input_password;
+ else if(key_trigger[00]) input_password <= {input_password[0+:3*4], 4'h1};
+ else if(key_trigger[01]) input_password <= {input_password[0+:3*4], 4'h2};
+ else if(key_trigger[02]) input_password <= {input_password[0+:3*4], 4'h3};
+ else if(key_trigger[03]) input_password <= {input_password[0+:3*4], 4'hA};
+ else if(key_trigger[04]) input_password <= {input_password[0+:3*4], 4'h4};
+ else if(key_trigger[05]) input_password <= {input_password[0+:3*4], 4'h5};
+ else if(key_trigger[06]) input_password <= {input_password[0+:3*4], 4'h6};
+ else if(key_trigger[07]) input_password <= {input_password[0+:3*4], 4'hB};
+ else if(key_trigger[08]) input_password <= {input_password[0+:3*4], 4'h7};
+ else if(key_trigger[09]) input_password <= {input_password[0+:3*4], 4'h8};
+ else if(key_trigger[10]) input_password <= {input_password[0+:3*4], 4'h9};
+ else if(key_trigger[11]) input_password <= {input_password[0+:3*4], 4'hC};
+ else if(key_trigger[12]) input_password <= 0;
+ else if(key_trigger[13]) input_password <= {input_password[0+:3*4], 4'h0};
+ else if(key_trigger[14]) input_password <= input_password;
+ else if(key_trigger[15]) input_password <= {input_password[0+:3*4], 4'hD};
+ else input_password <= input_password;
+ end else input_password <= 0;
+end
+
+always @(posedge clk or negedge rstn) begin
+ if(~rstn) input_num <= 0;
+ else if(cu_st == ST_SETUP || cu_st == ST_LOCK) begin
+ if(flag_setup_password || flag_input_confirm) input_num <= 0;
+ else if(key_trigger[00] || key_trigger[01] || key_trigger[02] || key_trigger[03] ||
+ key_trigger[04] || key_trigger[05] || key_trigger[06] || key_trigger[07] ||
+ key_trigger[08] || key_trigger[09] || key_trigger[10] || key_trigger[11] ||
+ key_trigger[13] || key_trigger[15])
+ input_num <= (input_num < 3'b100)?(input_num + 1):(input_num);
+ else if(key_trigger[12]) input_num <= 0;
+ else input_num <= input_num;
+ end else input_num <= 0;
+end
+
+assign seg_point = 8'b0;
+always @(posedge clk or negedge rstn) begin
+ if(~rstn) assic_seg <= "12345678";
+ else case(cu_st)
+ ST_SETUP :begin
+ assic_seg[0+:8] <= "-";
+ assic_seg[8+:8] <= "-";
+ assic_seg[16+:8] <= (input_num > 0)?(hex2assic(password[0+:4])):("_");
+ assic_seg[24+:8] <= (input_num > 1)?(hex2assic(password[4+:4])):("_");
+ assic_seg[32+:8] <= (input_num > 2)?(hex2assic(password[8+:4])):("_");
+ assic_seg[40+:8] <= (input_num > 3)?(hex2assic(password[12+:4])):("_");
+ assic_seg[48+:8] <= "-";
+ assic_seg[56+:8] <= "-";
+ end
+ ST_LOCK :begin
+ assic_seg[0+:8] <= "=";
+ assic_seg[8+:8] <= "=";
+ assic_seg[16+:8] <= (input_num > 0)?(hex2assic(input_password[0+:4])):("-");
+ assic_seg[24+:8] <= (input_num > 1)?(hex2assic(input_password[4+:4])):("-");
+ assic_seg[32+:8] <= (input_num > 2)?(hex2assic(input_password[8+:4])):("-");
+ assic_seg[40+:8] <= (input_num > 3)?(hex2assic(input_password[12+:4])):("-");
+ assic_seg[48+:8] <= "=";
+ assic_seg[56+:8] <= "=";
+ end
+ ST_ERROR : assic_seg <= " ERROR ";
+ ST_UNLOCK: assic_seg <= " unlock ";
+ default : assic_seg <= "12345678";
+ endcase
+end
+
+
+function [7:0] hex2assic;
+ input [3:0] hex;
+ case(hex)
+ 4'h0: hex2assic = "0"; // 0
+ 4'h1: hex2assic = "1"; // 1
+ 4'h2: hex2assic = "2"; // 2
+ 4'h3: hex2assic = "3"; // 3
+ 4'h4: hex2assic = "4"; // 4
+ 4'h5: hex2assic = "5"; // 5
+ 4'h6: hex2assic = "6"; // 6
+ 4'h7: hex2assic = "7"; // 7
+ 4'h8: hex2assic = "8"; // 8
+ 4'h9: hex2assic = "9"; // 9
+ 4'hA: hex2assic = "A"; // A
+ 4'hB: hex2assic = "B"; // B
+ 4'hC: hex2assic = "C"; // C
+ 4'hD: hex2assic = "D"; // D
+ 4'hE: hex2assic = "E"; // E
+ 4'hF: hex2assic = "F"; // F
+ default: hex2assic = " ";
+ endcase
+endfunction
+
+endmodule //password_lock
+```
+
+矩阵键盘行扫描模块在前面基础实验已经介绍过,但这次实验还需要为矩阵键盘添加按键上升沿检测模块,代码如下:
+
+#### matrix_key_trigger
+
+```verilog
+module matrix_key_trigger(
+ input wire clk,
+ input wire rstn,
+ input wire [15:0] key,
+ output wire [15:0] key_trigger
+);
+
+// 按键上升沿捕获模块
+
+reg [15:0] key_d; // 上一时钟周期的按键状态
+reg [15:0] key_d2; // 上两时钟周期的按键状态
+
+assign key_trigger = (key_d) & (~key_d2);
+
+always @(posedge clk or negedge rstn) begin
+ if (!rstn) begin
+ key_d <= 0;
+ key_d2 <= 0;
+ end else begin
+ key_d <= key;
+ key_d2 <= key_d;
+ end
+end
+
+endmodule //matrix_key_decode
+```
+
+至于数码管模块,为了方便,在led_display_driver模块添加了参数定义,并未进行其他修改。
+
+最后将几个模块例化在顶层,将端口相连接,代码如下所示:
+
+#### password_lock_top
+
+```verilog
+
+module password_lock_top #(
+ parameter VALID_SIGNAL = 1'b0,
+ parameter CLK_CYCLE = 5000
+)(
+//system io
+input wire external_clk ,
+input wire external_rstn,
+
+output wire [7:0] led_display_seg,
+output wire [7:0] led_display_sel,
+
+input wire [3:0] col,
+output wire [3:0] row
+);
+
+wire [15:0] key_out;
+wire [15:0] key_trigger;
+wire [8*8-1:0] assic_seg;
+wire [7:0] seg_point;
+
+led_display_driver #(
+ .VALID_SIGNAL (VALID_SIGNAL),
+ .CLK_CYCLE (CLK_CYCLE)
+)u_led_display_driver(
+ .clk ( external_clk ),
+ .rstn ( external_rstn ),
+ .assic_seg ( assic_seg ),
+ .seg_point ( seg_point ),
+ .led_display_seg ( led_display_seg ),
+ .led_display_sel ( led_display_sel )
+);
+
+matrix_key #(
+ .ROW_NUM ( 4 ),
+ .COL_NUM ( 4 ),
+ .DEBOUNCE_TIME ( 10000 ),
+ .DELAY_TIME ( 2000 ))
+u_matrix_key(
+ .clk ( external_clk ),
+ .rstn ( external_rstn ),
+ .row ( row ),
+ .col ( col ),
+ .key_out ( key_out )
+);
+
+matrix_key_trigger u_matrix_key_trigger(
+ .clk ( external_clk ),
+ .rstn ( external_rstn),
+ .key ( key_out ),
+ .key_trigger ( key_trigger )
+);
+
+password_lock u_password_lock(
+ .clk ( external_clk ),
+ .rstn ( external_rstn),
+ .key_trigger ( key_trigger ),
+ .assic_seg ( assic_seg ),
+ .seg_point ( seg_point )
+);
+
+endmodule //led_diaplay_top
+
+```
+
+### 1.3.3 上板验证步骤
+
+---
+
+可以直接将矩阵键盘,数码管的管脚约束文件中的约束复制到本次实验的管脚约束文件中。
+
+将生成的sbit文件烧录好后,即可使用网页界面的虚拟按键进行使用。
+
+## 1.4 章末总结
+
+本章通过设计一个简易密码锁系统,综合运用了前面基础实验中学习的**矩阵键盘扫描**、**数码管显示**等知识,并引入了**有限状态机(FSM)**的设计方法,完成了一个具有较强工程实用性的综合实验。
+
+通过本实验,你应该掌握了以下几点核心能力:
+
+- 理解并运用 状态机进行系统流程控制;
+- 将多个功能模块(键盘、数码管、比较器)整合为一个完整系统;
+- 设计基于状态的控制逻辑,实现密码输入、校验、反馈显示等功能;
+- 理解数字电路系统中控制与数据路径的分离思想。
+
+密码锁系统虽然逻辑简单,但已经具备了完整嵌入式控制系统的基本结构,是后续更复杂项目设计的重要基础。
+
+## 1.5 拓展训练
+
+为了进一步加深对本实验内容的理解,并锻炼系统设计与工程实现能力,你可以尝试完成以下拓展任务:
+
+1. **增加防爆破机制**:限定密码错误尝试次数,例如连续三次错误后锁定一段时间,并在数码管上提示“Err”。
+2. **利用按键实现简易菜单系统**拓展状态机结构,允许通过矩阵键盘导航菜单,如“输入密码”、“查看状态”、“设置新密码”等。
diff --git a/public/doc/11/images/1.png b/public/doc/11/images/1.png
new file mode 100644
index 0000000..c226fe5
Binary files /dev/null and b/public/doc/11/images/1.png differ
diff --git a/public/doc/11/images/UDP.png b/public/doc/11/images/UDP.png
new file mode 100644
index 0000000..02293d5
Binary files /dev/null and b/public/doc/11/images/UDP.png differ
diff --git a/server/src/TutorialController.cs b/server/src/TutorialController.cs
new file mode 100644
index 0000000..f5ffb01
--- /dev/null
+++ b/server/src/TutorialController.cs
@@ -0,0 +1,55 @@
+using System.IO;
+using System.Collections.Generic;
+using Microsoft.AspNetCore.Mvc;
+
+namespace server.Controllers;
+
+///
+/// 教程 API
+///
+[ApiController]
+[Route("api/[controller]")]
+public class TutorialController : ControllerBase
+{
+ private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
+ private readonly IWebHostEnvironment _environment;
+
+ public TutorialController(IWebHostEnvironment environment)
+ {
+ _environment = environment;
+ }
+
+ ///
+ /// 获取所有可用的教程目录
+ ///
+ /// 教程目录列表
+ [HttpGet]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status500InternalServerError)]
+ public IActionResult GetTutorials()
+ {
+ try
+ {
+ // 获取文档目录
+ string docPath = Path.Combine(_environment.WebRootPath, "doc");
+
+ if (!Directory.Exists(docPath))
+ {
+ return Ok(new { tutorials = new List() });
+ }
+
+ // 获取所有子目录
+ var directories = Directory.GetDirectories(docPath)
+ .Select(Path.GetFileName)
+ .Where(dir => !string.IsNullOrEmpty(dir))
+ .ToList();
+
+ return Ok(new { tutorials = directories });
+ }
+ catch (Exception ex)
+ {
+ logger.Error(ex, "获取教程目录失败");
+ return StatusCode(500, new { error = "无法读取教程目录" });
+ }
+ }
+}
diff --git a/server/src/TutorialService.ts b/server/src/TutorialService.ts
new file mode 100644
index 0000000..6dd953d
--- /dev/null
+++ b/server/src/TutorialService.ts
@@ -0,0 +1,30 @@
+// 此接口提供获取例程目录服务
+// GET /api/tutorials 返回所有可用的例程目录
+
+import fs from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+import { Request, Response } from 'express';
+
+// 获取当前文件的目录
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+const publicDir = path.resolve(__dirname, '../public');
+
+export function getTutorials(req: Request, res: Response) {
+ try {
+ const docDir = path.join(publicDir, 'doc');
+
+ // 读取doc目录下的所有文件夹
+ const entries = fs.readdirSync(docDir, { withFileTypes: true });
+ const dirs = entries
+ .filter(dirent => dirent.isDirectory())
+ .map(dirent => dirent.name);
+
+ // 返回文件夹列表
+ res.json({ tutorials: dirs });
+ } catch (error) {
+ console.error('获取例程目录失败:', error);
+ res.status(500).json({ error: '无法读取例程目录' });
+ }
+}
diff --git a/src/components/MarkdownRenderer.vue b/src/components/MarkdownRenderer.vue
index 8e631a2..4015a80 100644
--- a/src/components/MarkdownRenderer.vue
+++ b/src/components/MarkdownRenderer.vue
@@ -1,26 +1,67 @@
@@ -33,6 +74,8 @@ const renderedContent = computed(() => {
line-height: 1.6;
padding: 1rem 1.5rem;
max-width: 100%;
+ background-color: inherit; /* 继承父元素的背景色 */
+ height: 100%;
}
.markdown-content :deep(img) {
@@ -45,47 +88,64 @@ const renderedContent = computed(() => {
}
.markdown-content :deep(h1) {
- margin-top: 2rem;
- margin-bottom: 1rem;
+ margin-top: 2.5rem;
+ margin-bottom: 1.5rem;
color: hsl(var(--bc));
font-weight: 700;
- font-size: 2rem;
+ font-size: 2.2rem;
line-height: 1.3;
- padding-bottom: 0.5rem;
- border-bottom: 1px solid hsl(var(--b2));
+ padding-bottom: 0.7rem;
+ border-bottom: 2px solid hsl(var(--p) / 0.7);
+ text-shadow: 1px 1px 2px rgba(0,0,0,0.05);
}
.markdown-content :deep(h2) {
- margin-top: 1.8rem;
- margin-bottom: 0.8rem;
+ margin-top: 2rem;
+ margin-bottom: 1rem;
color: hsl(var(--bc));
font-weight: 600;
- font-size: 1.5rem;
+ font-size: 1.7rem;
line-height: 1.4;
- padding-left: 0.5rem;
- border-left: 4px solid hsl(var(--p));
+ padding: 0.5rem 1rem;
+ border-left: 5px solid hsl(var(--p));
+ background: linear-gradient(to right, hsl(var(--b2) / 0.5), transparent);
+ border-radius: 0.3rem;
}
.markdown-content :deep(h3) {
- margin-top: 1.5rem;
- margin-bottom: 0.75rem;
+ margin-top: 1.8rem;
+ margin-bottom: 0.9rem;
color: hsl(var(--bc));
font-weight: 600;
- font-size: 1.25rem;
+ font-size: 1.4rem;
line-height: 1.4;
padding-left: 1rem;
+ border-left: 3px solid hsl(var(--s));
+ padding-top: 0.3rem;
+ padding-bottom: 0.3rem;
}
.markdown-content :deep(h4),
.markdown-content :deep(h5),
.markdown-content :deep(h6) {
- margin-top: 1.2rem;
- margin-bottom: 0.6rem;
+ margin-top: 1.5rem;
+ margin-bottom: 0.7rem;
color: hsl(var(--bc));
font-weight: 600;
- font-size: 1.1rem;
+ font-size: 1.2rem;
line-height: 1.5;
padding-left: 1.5rem;
+ position: relative;
+}
+
+.markdown-content :deep(h4::before),
+.markdown-content :deep(h5::before),
+.markdown-content :deep(h6::before) {
+ content: '▶';
+ color: hsl(var(--p) / 0.7);
+ position: absolute;
+ left: 0.2rem;
+ font-size: 0.9em;
}
.markdown-content :deep(p) {
@@ -97,21 +157,31 @@ const renderedContent = computed(() => {
.markdown-content :deep(ul),
.markdown-content :deep(ol) {
- padding-left: 2em;
- margin: 0.75rem 0;
+ padding-left: 2.5em;
+ margin: 1.25rem 0;
color: hsl(var(--bc) / 0.8);
+ background-color: hsl(var(--b1) / 0.3);
+ border-radius: 0.5rem;
+ padding-top: 0.75rem;
+ padding-bottom: 0.75rem;
+ padding-right: 1rem;
+ border-left: 3px solid hsl(var(--p) / 0.7);
}
.markdown-content :deep(li) {
- margin: 0.4rem 0;
+ margin: 0.5rem 0;
position: relative;
+ padding-left: 0.5rem;
}
.markdown-content :deep(ul ul),
.markdown-content :deep(ul ol),
.markdown-content :deep(ol ul),
.markdown-content :deep(ol ol) {
- margin: 0.4rem 0 0.4rem 1rem;
+ margin: 0.5rem 0 0.5rem 0.5rem;
+ padding-left: 1.5rem;
+ border-left: 2px solid hsl(var(--s) / 0.5);
+ background-color: transparent;
}
.markdown-content :deep(ul) {
@@ -134,45 +204,142 @@ const renderedContent = computed(() => {
color: hsl(var(--p));
}
+.markdown-content :deep(ol li::marker) {
+ color: hsl(var(--s));
+}
+
+/* 代码块样式增强 */
.markdown-content :deep(pre) {
background-color: hsl(var(--b3));
padding: 1rem;
border-radius: 0.5rem;
overflow-x: auto;
border: 1px solid hsl(var(--b2));
- margin: 1rem 0;
+ margin: 1.5rem 0;
+ box-shadow: 0 4px 8px rgba(0,0,0,0.1);
+ position: relative;
}
+.markdown-content :deep(pre::before) {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 4px;
+ background: linear-gradient(90deg, hsl(var(--p)), hsl(var(--s)));
+ border-radius: 0.5rem 0.5rem 0 0;
+}
+
+/* 代码语言标签 */
+.markdown-content :deep(pre.hljs::after) {
+ content: attr(class);
+ content: attr(data-language);
+ position: absolute;
+ top: 0;
+ right: 0;
+ color: hsl(var(--bc) / 0.7);
+ font-size: 0.75rem;
+ background-color: hsl(var(--b2));
+ padding: 0.2rem 0.5rem;
+ border-radius: 0 0.3rem 0 0.3rem;
+ opacity: 0.8;
+}
+
+/* 内联代码样式 */
.markdown-content :deep(code) {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
- background-color: hsl(var(--b3));
- padding: 2px 0.5rem;
+ background-color: hsl(var(--b3) / 0.7);
+ padding: 0.2rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.9em;
color: hsl(var(--p));
+ border: 1px solid hsl(var(--b2) / 0.5);
+}
+
+/* 确保代码块内的代码不受内联代码样式影响 */
+.markdown-content :deep(pre code) {
+ background-color: transparent;
+ padding: 0;
+ border: none;
+ color: inherit;
+ font-size: 0.95em;
+ line-height: 1.5;
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
+}
+
+/* 为常见语言添加一些特殊的高亮效果 */
+.markdown-content :deep(.hljs-keyword),
+.markdown-content :deep(.hljs-tag),
+.markdown-content :deep(.hljs-selector-tag) {
+ color: #cc99cd; /* 紫色,用于关键字 */
+}
+
+.markdown-content :deep(.hljs-string),
+.markdown-content :deep(.hljs-regexp),
+.markdown-content :deep(.hljs-template-tag) {
+ color: #7ec699; /* 绿色,用于字符串 */
+}
+
+.markdown-content :deep(.hljs-number),
+.markdown-content :deep(.hljs-literal) {
+ color: #f08d49; /* 橙色,用于数字 */
+}
+
+.markdown-content :deep(.hljs-comment) {
+ color: #999999; /* 灰色,用于注释 */
+ font-style: italic;
+}
+
+.markdown-content :deep(.hljs-name),
+.markdown-content :deep(.hljs-attribute),
+.markdown-content :deep(.hljs-selector-id),
+.markdown-content :deep(.hljs-selector-class) {
+ color: #e2777a; /* 红色,用于HTML标签名和属性 */
+}
+
+.markdown-content :deep(.hljs-built_in),
+.markdown-content :deep(.hljs-builtin-name) {
+ color: #6196cc; /* 蓝色,用于内置函数 */
+}
+
+.markdown-content :deep(.hljs-title),
+.markdown-content :deep(.hljs-function) {
+ color: #f8c555; /* 金色,用于函数名和类名 */
}
.markdown-content :deep(table) {
- border-collapse: collapse;
+ border-collapse: separate;
+ border-spacing: 0;
width: 100%;
- margin: 1rem 0;
+ margin: 1.5rem 0;
background-color: hsl(var(--b1));
border-radius: 0.5rem;
overflow: hidden;
- box-shadow: 0 1px 3px rgba(0,0,0,0.12);
+ box-shadow: 0 2px 8px rgba(0,0,0,0.08);
+ border: 1px solid hsl(var(--b2));
}
.markdown-content :deep(th),
.markdown-content :deep(td) {
border: 1px solid hsl(var(--b2));
- padding: 0.75rem;
+ padding: 0.75rem 1rem;
text-align: left;
}
.markdown-content :deep(th) {
- background-color: hsl(var(--b2));
- font-weight: 500;
+ background-color: hsl(var(--p) / 0.15);
+ font-weight: 600;
color: hsl(var(--bc));
+ border-bottom: 2px solid hsl(var(--p) / 0.5);
+}
+
+.markdown-content :deep(tr:nth-child(even)) {
+ background-color: hsl(var(--b2) / 0.3);
+}
+
+.markdown-content :deep(tr:hover) {
+ background-color: hsl(var(--b2) / 0.5);
}
.markdown-content :deep(td) {
@@ -180,12 +347,29 @@ const renderedContent = computed(() => {
}
.markdown-content :deep(blockquote) {
- margin: 1rem 0;
- padding: 0.5rem 1rem;
+ margin: 1.5rem 0;
+ padding: 1rem 1.5rem;
border-left: 4px solid hsl(var(--p));
- background-color: hsl(var(--b2));
- color: hsl(var(--bc) / 0.8);
+ background-color: hsl(var(--b2) / 0.3);
+ color: hsl(var(--bc) / 0.9);
font-style: italic;
+ border-radius: 0 0.5rem 0.5rem 0;
+ box-shadow: 0 2px 5px rgba(0,0,0,0.03);
+ position: relative;
+}
+
+.markdown-content :deep(blockquote::before) {
+ content: '"';
+ font-size: 2rem;
+ color: hsl(var(--p) / 0.3);
+ position: absolute;
+ top: 0.5rem;
+ left: 0.5rem;
+ font-family: serif;
+}
+
+.markdown-content :deep(blockquote p) {
+ margin-top: 0.5rem;
}
.markdown-content :deep(hr) {
@@ -204,4 +388,16 @@ const renderedContent = computed(() => {
color: hsl(var(--pf));
text-decoration: underline;
}
+
+/* 暗黑模式下的代码高亮调整 */
+@media (prefers-color-scheme: dark) {
+ .markdown-content :deep(pre) {
+ background-color: hsl(var(--b3));
+ border-color: hsl(var(--b1) / 0.7);
+ }
+
+ .markdown-content :deep(code) {
+ background-color: hsl(var(--b2) / 0.7);
+ }
+}
diff --git a/src/components/Navbar.vue b/src/components/Navbar.vue
index 4bc11ae..4ceaa05 100644
--- a/src/components/Navbar.vue
+++ b/src/components/Navbar.vue
@@ -49,6 +49,19 @@
测试功能
+
+
+
+ Markdown测试
+
+
diff --git a/src/components/TutorialCarousel.vue b/src/components/TutorialCarousel.vue
new file mode 100644
index 0000000..590e33c
--- /dev/null
+++ b/src/components/TutorialCarousel.vue
@@ -0,0 +1,302 @@
+
+
+
+
+
+
+
![]()
+
+
+
+
+
+
+
{{ tutorial.title }}
+
{{ tutorial.description }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/router/index.ts b/src/router/index.ts
index 362942f..5403c59 100644
--- a/src/router/index.ts
+++ b/src/router/index.ts
@@ -1,23 +1,57 @@
-import { createWebHistory, createRouter } from "vue-router";
-import LoginView from "../views/LoginView.vue";
-import UserView from "../views/UserView.vue";
-import TestView from "../views/TestView.vue";
-import ProjectView from "../views/ProjectView.vue";
-import HomeView from "@/views/HomeView.vue";
-import AdminView from "@/views/AdminView.vue";
-
-const routes = [
- { path: "/", name: "Home", component: HomeView },
- { path: "/login", name: "Login", component: LoginView },
- { path: "/user", name: "User", component: UserView },
- { path: "/test", name: "Test", component: TestView },
- { path: "/project", name: "Project", component: ProjectView },
- { path: "/admin", name: "Admin", component: AdminView },
-];
+import { createRouter, createWebHistory } from 'vue-router'
+import HomeView from '../views/HomeView.vue'
+import LoginView from '../views/LoginView.vue'
+import LabView from '../views/LabView.vue'
+import ProjectView from '../views/ProjectView.vue'
+import TestView from '../views/TestView.vue'
+import UserView from '../views/UserView.vue'
+import AdminView from '../views/AdminView.vue'
+import MarkdownTestView from '../views/MarkdownTestView.vue'
const router = createRouter({
- history: createWebHistory(),
- routes,
-});
+ history: createWebHistory(import.meta.env.BASE_URL),
+ routes: [
+ {
+ path: '/',
+ name: 'home',
+ component: HomeView
+ },
+ {
+ path: '/login',
+ name: 'login',
+ component: LoginView
+ },
+ {
+ path: '/lab/:id',
+ name: 'lab',
+ component: LabView
+ },
+ {
+ path: '/project',
+ name: 'project',
+ component: ProjectView
+ },
+ {
+ path: '/test',
+ name: 'test',
+ component: TestView
+ },
+ {
+ path: '/markdown-test',
+ name: 'markdown-test',
+ component: MarkdownTestView
+ },
+ {
+ path: '/user',
+ name: 'user',
+ component: UserView
+ },
+ {
+ path: '/admin',
+ name: 'admin',
+ component: AdminView
+ }
+ ]
+})
-export default router;
+export default router
diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue
index c817949..2c61551 100644
--- a/src/views/HomeView.vue
+++ b/src/views/HomeView.vue
@@ -1,14 +1,9 @@