add: home select exp
							
								
								
									
										10
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						@@ -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",
 | 
			
		||||
 
 | 
			
		||||
@@ -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",
 | 
			
		||||
 
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB  | 
| 
		 Before Width: | Height: | Size: 2.9 MiB After Width: | Height: | Size: 2.9 MiB  | 
| 
		 Before Width: | Height: | Size: 620 KiB After Width: | Height: | Size: 620 KiB  | 
| 
		 Before Width: | Height: | Size: 635 KiB After Width: | Height: | Size: 635 KiB  | 
| 
		 Before Width: | Height: | Size: 821 KiB After Width: | Height: | Size: 821 KiB  | 
| 
		 Before Width: | Height: | Size: 434 KiB After Width: | Height: | Size: 434 KiB  | 
| 
		 Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB  | 
| 
		 Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.5 MiB  | 
| 
		 Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 1.4 MiB  | 
| 
		 Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.5 MiB  | 
| 
		 Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 1.6 MiB  | 
							
								
								
									
										400
									
								
								public/doc/11/doc.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -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状态重设密码
 | 
			
		||||
 | 
			
		||||
根据上述状态转移逻辑,我们可以画出状态转移图,状态转移图如下图所示:
 | 
			
		||||
 | 
			
		||||
<div>           <!--块级封装-->
 | 
			
		||||
    <center>    <!--将图片和文字居中-->
 | 
			
		||||
    <img src="images/1.png"
 | 
			
		||||
         alt="无法显示图片时显示的文字"
 | 
			
		||||
         style="zoom:100%"/>
 | 
			
		||||
    <br>        <!--换行-->
 | 
			
		||||
    图1.状态转移图  <!--标题-->
 | 
			
		||||
    </center>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
我们已经了解了本次实验所使用的状态机,那么如何使用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. **利用按键实现简易菜单系统**拓展状态机结构,允许通过矩阵键盘导航菜单,如“输入密码”、“查看状态”、“设置新密码”等。
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								public/doc/11/images/1.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 21 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								public/doc/11/images/UDP.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 144 KiB  | 
							
								
								
									
										55
									
								
								server/src/TutorialController.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,55 @@
 | 
			
		||||
using System.IO;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using Microsoft.AspNetCore.Mvc;
 | 
			
		||||
 | 
			
		||||
namespace server.Controllers;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// 教程 API
 | 
			
		||||
/// </summary>
 | 
			
		||||
[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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 获取所有可用的教程目录
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <returns>教程目录列表</returns>
 | 
			
		||||
    [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<string>() });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // 获取所有子目录
 | 
			
		||||
            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 = "无法读取教程目录" });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										30
									
								
								server/src/TutorialService.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -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: '无法读取例程目录' });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,26 +1,67 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { computed } from 'vue';
 | 
			
		||||
import { computed, onMounted } from 'vue';
 | 
			
		||||
import { marked } from 'marked';
 | 
			
		||||
import hljs from 'highlight.js';
 | 
			
		||||
// 导入默认样式 - 选择一个适合你的主题
 | 
			
		||||
import 'highlight.js/styles/github-dark.css';
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
    content: {
 | 
			
		||||
        type: String,
 | 
			
		||||
        required: true
 | 
			
		||||
    },
 | 
			
		||||
    removeFirstH1: {
 | 
			
		||||
        type: Boolean,
 | 
			
		||||
        default: false
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const renderedContent = computed(() => {
 | 
			
		||||
    if (!props.content) return '<p>没有内容</p>';
 | 
			
		||||
    
 | 
			
		||||
    let processedContent = props.content;
 | 
			
		||||
    // 设置 marked 选项
 | 
			
		||||
    
 | 
			
		||||
    // 如果需要,移除第一个一级标题
 | 
			
		||||
    if (props.removeFirstH1) {
 | 
			
		||||
        const lines = processedContent.split('\n');
 | 
			
		||||
        const firstH1Index = lines.findIndex(line => line.startsWith('# '));
 | 
			
		||||
        
 | 
			
		||||
        if (firstH1Index !== -1) {
 | 
			
		||||
            processedContent = lines.slice(firstH1Index + 1).join('\n');
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // 创建自定义渲染器
 | 
			
		||||
    const renderer = new marked.Renderer();
 | 
			
		||||
    marked.setOptions({
 | 
			
		||||
    
 | 
			
		||||
    // 重写代码块渲染方法,添加语言信息
 | 
			
		||||
    renderer.code = (code, incomingLanguage) => {
 | 
			
		||||
        // 确保语言参数是字符串
 | 
			
		||||
        const language = incomingLanguage || 'plaintext';
 | 
			
		||||
        // 验证语言
 | 
			
		||||
        const validLanguage = hljs.getLanguage(language) ? language : 'plaintext';
 | 
			
		||||
        // 高亮代码
 | 
			
		||||
        const highlightedCode = hljs.highlight(code, { language: validLanguage }).value;
 | 
			
		||||
        
 | 
			
		||||
        // 添加语言标签到代码块
 | 
			
		||||
        return `<pre class="hljs" data-language="${validLanguage}"><code class="language-${validLanguage}">${highlightedCode}</code></pre>`;
 | 
			
		||||
    };
 | 
			
		||||
    
 | 
			
		||||
    // 设置 marked 选项
 | 
			
		||||
    marked.use({
 | 
			
		||||
        renderer: renderer,
 | 
			
		||||
        gfm: true,
 | 
			
		||||
        breaks: true
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    return marked(processedContent);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 页面挂载后,对已渲染的代码块应用高亮效果
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
    // 如果需要在客户端重新高亮(通常不需要,因为我们已经在服务端高亮)
 | 
			
		||||
    // hljs.highlightAll();
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
@@ -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);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
@@ -49,6 +49,19 @@
 | 
			
		||||
              测试功能
 | 
			
		||||
            </router-link>
 | 
			
		||||
          </li>
 | 
			
		||||
          <li class="my-1 hover:translate-x-1 transition-all duration-300">
 | 
			
		||||
            <router-link to="/markdown-test" class="text-base font-medium">
 | 
			
		||||
              <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 opacity-70" viewBox="0 0 24 24" fill="none"
 | 
			
		||||
                stroke="currentColor" stroke-width="2">
 | 
			
		||||
                <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
 | 
			
		||||
                <polyline points="14 2 14 8 20 8"></polyline>
 | 
			
		||||
                <line x1="16" y1="13" x2="8" y2="13"></line>
 | 
			
		||||
                <line x1="16" y1="17" x2="8" y2="17"></line>
 | 
			
		||||
                <polyline points="10 9 9 9 8 9"></polyline>
 | 
			
		||||
              </svg>
 | 
			
		||||
              Markdown测试
 | 
			
		||||
            </router-link>
 | 
			
		||||
          </li>
 | 
			
		||||
          <li class="my-1 hover:translate-x-1 transition-all duration-300">
 | 
			
		||||
            <a href="http://localhost:5000/swagger" target="_self" rel="noopener noreferrer"
 | 
			
		||||
              class="text-base font-medium">
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										302
									
								
								src/components/TutorialCarousel.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,302 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div 
 | 
			
		||||
    class="tutorial-carousel relative"
 | 
			
		||||
    @wheel.prevent="handleWheel"
 | 
			
		||||
    @mouseenter="pauseAutoRotation"
 | 
			
		||||
    @mouseleave="resumeAutoRotation"
 | 
			
		||||
  >    <!-- 例程卡片堆叠 -->
 | 
			
		||||
    <div class="card-stack relative mx-auto">
 | 
			
		||||
      <div 
 | 
			
		||||
        v-for="(tutorial, index) in tutorials" 
 | 
			
		||||
        :key="index" 
 | 
			
		||||
        class="tutorial-card absolute transition-all duration-500 ease-in-out rounded-2xl shadow-2xl border-4 border-base-300 overflow-hidden"
 | 
			
		||||
        :class="getCardClass(index)"
 | 
			
		||||
        :style="getCardStyle(index)"
 | 
			
		||||
        @click="handleCardClick(index, tutorial.id)"
 | 
			
		||||
      >
 | 
			
		||||
        <!-- 卡片内容 -->
 | 
			
		||||
        <div class="relative">
 | 
			
		||||
          <!-- 图片 -->          <img 
 | 
			
		||||
            :src="tutorial.thumbnail || `https://placehold.co/600x400?text=${tutorial.title}`" 
 | 
			
		||||
            class="w-full object-contain"
 | 
			
		||||
            :alt="tutorial.title"
 | 
			
		||||
            style="width: 600px; height: 400px;"
 | 
			
		||||
          />
 | 
			
		||||
          
 | 
			
		||||
          <!-- 卡片蒙层 -->
 | 
			
		||||
          <div 
 | 
			
		||||
            class="absolute inset-0 bg-primary opacity-20 transition-opacity duration-300"
 | 
			
		||||
            :class="{'opacity-10': index === currentIndex}"
 | 
			
		||||
          ></div>
 | 
			
		||||
          
 | 
			
		||||
          <!-- 标题覆盖层 -->
 | 
			
		||||
          <div class="absolute bottom-0 left-0 right-0 p-4 bg-gradient-to-t from-base-300 to-transparent">
 | 
			
		||||
            <h3 class="text-lg font-bold text-base-content">{{ tutorial.title }}</h3>
 | 
			
		||||
            <p class="text-sm opacity-80 truncate">{{ tutorial.description }}</p>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    
 | 
			
		||||
    <!-- 导航指示器 -->
 | 
			
		||||
    <div class="indicators flex justify-center gap-2 mt-4">
 | 
			
		||||
      <button 
 | 
			
		||||
        v-for="(_, index) in tutorials" 
 | 
			
		||||
        :key="index"
 | 
			
		||||
        @click="setActiveCard(index)"
 | 
			
		||||
        class="w-3 h-3 rounded-full transition-all duration-300"
 | 
			
		||||
        :class="index === currentIndex ? 'bg-primary scale-125' : 'bg-base-300'"
 | 
			
		||||
      ></button>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { ref, onMounted, onUnmounted } from 'vue';
 | 
			
		||||
import { useRouter } from 'vue-router';
 | 
			
		||||
 | 
			
		||||
// 接口定义
 | 
			
		||||
interface Tutorial {
 | 
			
		||||
  id: string;
 | 
			
		||||
  title: string;
 | 
			
		||||
  description: string;
 | 
			
		||||
  thumbnail?: string;
 | 
			
		||||
  docPath: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Props
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
  autoRotationInterval?: number;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
// 配置默认值
 | 
			
		||||
const autoRotationInterval = props.autoRotationInterval || 5000; // 默认5秒
 | 
			
		||||
 | 
			
		||||
// 状态管理
 | 
			
		||||
const tutorials = ref<Tutorial[]>([]);
 | 
			
		||||
const currentIndex = ref(0);
 | 
			
		||||
const router = useRouter();
 | 
			
		||||
let autoRotationTimer: number | null = null;
 | 
			
		||||
 | 
			
		||||
// 处理卡片点击
 | 
			
		||||
const handleCardClick = (index: number, tutorialId: string) => {
 | 
			
		||||
  if (index === currentIndex.value) {
 | 
			
		||||
    goToTutorial(tutorialId);
 | 
			
		||||
  } else {
 | 
			
		||||
    setActiveCard(index);
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 从 public/doc 目录加载例程信息
 | 
			
		||||
onMounted(async () => {
 | 
			
		||||
  try {
 | 
			
		||||
    // 尝试从API获取教程目录
 | 
			
		||||
    let tutorialIds: string[] = [];
 | 
			
		||||
    try {
 | 
			
		||||
      const response = await fetch('/api/tutorial');
 | 
			
		||||
      if (response.ok) {
 | 
			
		||||
        const data = await response.json();
 | 
			
		||||
        tutorialIds = data.tutorials || [];
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.warn('无法从API获取教程目录,使用默认值:', error);
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // 如果API调用失败或返回空列表,使用默认值
 | 
			
		||||
    if (tutorialIds.length === 0) {
 | 
			
		||||
      tutorialIds = ['01', '02', '11']; // 默认例程
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // 为每个例程创建对象并尝试获取文档标题
 | 
			
		||||
    const tutorialPromises = tutorialIds.map(async (id) => {
 | 
			
		||||
      // 尝试读取doc.md获取标题
 | 
			
		||||
      let title = `例程 ${id}`;
 | 
			
		||||
      let description = "点击加载此例程";
 | 
			
		||||
      let thumbnail = `/doc/${id}/images/1.png`; // 默认使用第一张图片作为缩略图
 | 
			
		||||
      
 | 
			
		||||
      try {
 | 
			
		||||
        // 尝试读取文档内容获取标题
 | 
			
		||||
        const response = await fetch(`/doc/${id}/doc.md`);
 | 
			
		||||
        if (response.ok) {
 | 
			
		||||
          const text = await response.text();
 | 
			
		||||
          // 从Markdown提取标题
 | 
			
		||||
          const titleMatch = text.match(/^#\s+(.+)$/m);
 | 
			
		||||
          if (titleMatch && titleMatch[1]) {
 | 
			
		||||
            title = titleMatch[1].trim();
 | 
			
		||||
          }
 | 
			
		||||
          
 | 
			
		||||
          // 提取第一段作为描述
 | 
			
		||||
          const descMatch = text.match(/\n\n([^#\n][^\n]+)/);
 | 
			
		||||
          if (descMatch && descMatch[1]) {
 | 
			
		||||
            description = descMatch[1].substring(0, 100).trim();
 | 
			
		||||
            if (description.length === 100) description += '...';
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        console.warn(`无法读取例程${id}的文档内容:`, error);
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      return {
 | 
			
		||||
        id,
 | 
			
		||||
        title,
 | 
			
		||||
        description,
 | 
			
		||||
        thumbnail,
 | 
			
		||||
        docPath: `/doc/${id}/doc.md`
 | 
			
		||||
      };
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    tutorials.value = await Promise.all(tutorialPromises);
 | 
			
		||||
    
 | 
			
		||||
    // 启动自动旋转
 | 
			
		||||
    startAutoRotation();
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error('加载例程失败:', error);
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 在组件销毁时清除计时器
 | 
			
		||||
onUnmounted(() => {
 | 
			
		||||
  if (autoRotationTimer) {
 | 
			
		||||
    clearInterval(autoRotationTimer);
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 鼠标滚轮处理
 | 
			
		||||
const handleWheel = (event: WheelEvent) => {
 | 
			
		||||
  if (event.deltaY > 0) {
 | 
			
		||||
    nextCard();
 | 
			
		||||
  } else {
 | 
			
		||||
    prevCard();
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 下一张卡片
 | 
			
		||||
const nextCard = () => {
 | 
			
		||||
  currentIndex.value = (currentIndex.value + 1) % tutorials.value.length;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 上一张卡片
 | 
			
		||||
const prevCard = () => {
 | 
			
		||||
  currentIndex.value = (currentIndex.value - 1 + tutorials.value.length) % tutorials.value.length;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 设置活动卡片
 | 
			
		||||
const setActiveCard = (index: number) => {
 | 
			
		||||
  currentIndex.value = index;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 自动旋转
 | 
			
		||||
const startAutoRotation = () => {
 | 
			
		||||
  autoRotationTimer = window.setInterval(() => {
 | 
			
		||||
    nextCard();
 | 
			
		||||
  }, autoRotationInterval);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 暂停自动旋转
 | 
			
		||||
const pauseAutoRotation = () => {
 | 
			
		||||
  if (autoRotationTimer) {
 | 
			
		||||
    clearInterval(autoRotationTimer);
 | 
			
		||||
    autoRotationTimer = null;
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 恢复自动旋转
 | 
			
		||||
const resumeAutoRotation = () => {
 | 
			
		||||
  if (!autoRotationTimer) {
 | 
			
		||||
    startAutoRotation();
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 前往例程
 | 
			
		||||
const goToTutorial = (tutorialId: string) => {
 | 
			
		||||
  // 跳转到工程页面,并通过 query 参数传递文档路径
 | 
			
		||||
  router.push({
 | 
			
		||||
    path: '/project',
 | 
			
		||||
    query: { tutorial: tutorialId }
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 计算卡片类和样式
 | 
			
		||||
const getCardClass = (index: number) => {
 | 
			
		||||
  const isActive = index === currentIndex.value;
 | 
			
		||||
  const isPrev = (index === currentIndex.value - 1) || (currentIndex.value === 0 && index === tutorials.value.length - 1);
 | 
			
		||||
  const isNext = (index === currentIndex.value + 1) || (currentIndex.value === tutorials.value.length - 1 && index === 0);
 | 
			
		||||
  
 | 
			
		||||
  return {
 | 
			
		||||
    'z-30': isActive,
 | 
			
		||||
    'z-20': isPrev || isNext,
 | 
			
		||||
    'z-10': !isActive && !isPrev && !isNext,
 | 
			
		||||
    'hover:scale-105': isActive,
 | 
			
		||||
    'cursor-pointer': true
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getCardStyle = (index: number) => {
 | 
			
		||||
  const isActive = index === currentIndex.value;
 | 
			
		||||
  const isPrev = (index === currentIndex.value - 1) || (currentIndex.value === 0 && index === tutorials.value.length - 1);
 | 
			
		||||
  const isNext = (index === currentIndex.value + 1) || (currentIndex.value === tutorials.value.length - 1 && index === 0);
 | 
			
		||||
  
 | 
			
		||||
  // 基本样式
 | 
			
		||||
  let style = {
 | 
			
		||||
    transform: 'scale(1) translateY(0) rotate(0deg)',
 | 
			
		||||
    opacity: '1',
 | 
			
		||||
    filter: 'blur(0)'
 | 
			
		||||
  };
 | 
			
		||||
  
 | 
			
		||||
  // 活动卡片
 | 
			
		||||
  if (isActive) {
 | 
			
		||||
    return style;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  // 上一张卡片
 | 
			
		||||
  if (isPrev) {
 | 
			
		||||
    style.transform = 'scale(0.85) translateY(-10%) rotate(-5deg)';
 | 
			
		||||
    style.opacity = '0.7';
 | 
			
		||||
    style.filter = 'blur(1px)';
 | 
			
		||||
    return style;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  // 下一张卡片
 | 
			
		||||
  if (isNext) {
 | 
			
		||||
    style.transform = 'scale(0.85) translateY(10%) rotate(5deg)';
 | 
			
		||||
    style.opacity = '0.7';
 | 
			
		||||
    style.filter = 'blur(1px)';
 | 
			
		||||
    return style;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  // 其他卡片
 | 
			
		||||
  style.transform = 'scale(0.7) translateY(0) rotate(0deg)';
 | 
			
		||||
  style.opacity = '0.4';
 | 
			
		||||
  style.filter = 'blur(2px)';
 | 
			
		||||
  return style;
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
.tutorial-carousel {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 500px;
 | 
			
		||||
  perspective: 1000px;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.card-stack {
 | 
			
		||||
  width: 600px;
 | 
			
		||||
  height: 440px;
 | 
			
		||||
  position: relative;
 | 
			
		||||
  transform-style: preserve-3d;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.tutorial-card {
 | 
			
		||||
  width: 600px;
 | 
			
		||||
  height: 400px;
 | 
			
		||||
  background-color: hsl(var(--b2));
 | 
			
		||||
  will-change: transform, opacity;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.tutorial-card:hover {
 | 
			
		||||
  box-shadow: 0 0 15px rgba(var(--p), 0.5);
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +1,9 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="bg-base-200 min-h-screen">
 | 
			
		||||
    <main class="hero min-h-screen bg-base-200">
 | 
			
		||||
      <div class="hero-content flex-col lg:flex-row-reverse gap-8 lg:gap-12 py-10 px-4">
 | 
			
		||||
        <!-- 图片容器 -->
 | 
			
		||||
        <div
 | 
			
		||||
          class="image-container relative w-full max-w-sm hover:scale-105 hover:-rotate-1 transition-transform duration-500 ease-in-out">
 | 
			
		||||
          <img src="https://placehold.co/600x400"
 | 
			
		||||
            class="w-full rounded-2xl shadow-2xl border-4 border-base-300 transition-shadow duration-300 hover:shadow-primary" />
 | 
			
		||||
          <!-- 这里使用relative定位,限制覆盖层只在图片容器内 -->
 | 
			
		||||
          <div class="absolute inset-0 bg-primary opacity-10 rounded-2xl pointer-events-none"></div>
 | 
			
		||||
      <div class="hero-content flex-col xl:flex-row-reverse gap-8 xl:gap-12 py-10 px-4">        <!-- 例程轮播容器 -->
 | 
			
		||||
        <div class="w-full flex justify-center" style="min-width: 650px;">
 | 
			
		||||
          <TutorialCarousel :autoRotationInterval="3000" />
 | 
			
		||||
        </div>
 | 
			
		||||
        <!-- 内容容器 -->
 | 
			
		||||
        <div class="content-container max-w-md lg:max-w-2xl transform transition-all duration-500 ease-in-out">
 | 
			
		||||
@@ -93,6 +88,7 @@
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import "@/router";
 | 
			
		||||
import TutorialCarousel from "@/components/TutorialCarousel.vue";
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="postcss">
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										445
									
								
								src/views/MarkdownTestView.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,445 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="p-8 max-w-5xl mx-auto">
 | 
			
		||||
    <h1 class="text-2xl font-bold mb-6">Markdown 渲染器语法高亮测试</h1>
 | 
			
		||||
    
 | 
			
		||||
    <div class="grid grid-cols-1 gap-8">
 | 
			
		||||
      <!-- 测试控制面板 -->
 | 
			
		||||
      <div class="bg-base-200 p-4 rounded-lg">
 | 
			
		||||
        <div class="flex gap-4 mb-4">
 | 
			
		||||
          <button @click="currentTheme = 'light'" class="btn btn-primary" :class="{'btn-outline': currentTheme !== 'light'}">
 | 
			
		||||
            亮色主题
 | 
			
		||||
          </button>
 | 
			
		||||
          <button @click="currentTheme = 'dark'" class="btn btn-primary" :class="{'btn-outline': currentTheme !== 'dark'}">
 | 
			
		||||
            暗色主题
 | 
			
		||||
          </button>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      
 | 
			
		||||
      <!-- 示例展示 -->
 | 
			
		||||
      <div class="bg-base-100 rounded-lg shadow-lg overflow-hidden" :data-theme="currentTheme">
 | 
			
		||||
        <div class="p-4 bg-base-200 font-semibold">Markdown 渲染结果</div>
 | 
			
		||||
        <div class="px-1">
 | 
			
		||||
          <MarkdownRenderer :content="sampleContent" :remove-first-h1="false" />
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { ref } from 'vue';
 | 
			
		||||
import MarkdownRenderer from '@/components/MarkdownRenderer.vue';
 | 
			
		||||
 | 
			
		||||
const currentTheme = ref('light');
 | 
			
		||||
 | 
			
		||||
// 包含各种代码示例的 Markdown 示例内容
 | 
			
		||||
const sampleContent = ref(`
 | 
			
		||||
# Markdown 语法高亮测试
 | 
			
		||||
 | 
			
		||||
这是一个用于测试 Markdown 渲染器语法高亮功能的页面。下面是一些代码示例。
 | 
			
		||||
 | 
			
		||||
## JavaScript 代码示例
 | 
			
		||||
 | 
			
		||||
\`\`\`javascript
 | 
			
		||||
// 一个简单的 JavaScript 函数
 | 
			
		||||
function calculateSum(a, b) {
 | 
			
		||||
  // 这是一个注释
 | 
			
		||||
  const sum = a + b;
 | 
			
		||||
  console.log(\`计算结果: \${sum}\`);
 | 
			
		||||
  return sum;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 类示例
 | 
			
		||||
class Person {
 | 
			
		||||
  constructor(name, age) {
 | 
			
		||||
    this.name = name;
 | 
			
		||||
    this.age = age;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  greet() {
 | 
			
		||||
    return \`你好,我是 \${this.name},我 \${this.age} 岁了。\`;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 使用 async/await
 | 
			
		||||
async function fetchData() {
 | 
			
		||||
  try {
 | 
			
		||||
    const response = await fetch('https://api.example.com/data');
 | 
			
		||||
    const data = await response.json();
 | 
			
		||||
    return data;
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error('获取数据出错:', error);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
\`\`\`
 | 
			
		||||
 | 
			
		||||
## TypeScript 代码示例
 | 
			
		||||
 | 
			
		||||
\`\`\`typescript
 | 
			
		||||
// TypeScript 接口
 | 
			
		||||
interface User {
 | 
			
		||||
  id: number;
 | 
			
		||||
  name: string;
 | 
			
		||||
  email: string;
 | 
			
		||||
  age?: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 泛型函数
 | 
			
		||||
function getFirst<T>(array: T[]): T | undefined {
 | 
			
		||||
  return array.length > 0 ? array[0] : undefined;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 类型别名和联合类型
 | 
			
		||||
type Result<T> = 
 | 
			
		||||
  | { success: true; value: T }
 | 
			
		||||
  | { success: false; error: Error };
 | 
			
		||||
 | 
			
		||||
// 装饰器
 | 
			
		||||
function log(target: any, key: string) {
 | 
			
		||||
  const originalMethod = target[key];
 | 
			
		||||
  
 | 
			
		||||
  target[key] = function(...args: any[]) {
 | 
			
		||||
    console.log(\`调用方法 \${key} 参数:, args);
 | 
			
		||||
    return originalMethod.apply(this, args);
 | 
			
		||||
  };
 | 
			
		||||
  
 | 
			
		||||
  return target;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class Calculator {
 | 
			
		||||
  @log
 | 
			
		||||
  add(a: number, b: number): number {
 | 
			
		||||
    return a + b;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
\`\`\`
 | 
			
		||||
 | 
			
		||||
## HTML & CSS 示例
 | 
			
		||||
 | 
			
		||||
\`\`\`html
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html lang="zh-CN">
 | 
			
		||||
<head>
 | 
			
		||||
  <meta charset="UTF-8">
 | 
			
		||||
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
			
		||||
  <title>示例页面</title>
 | 
			
		||||
  <style>
 | 
			
		||||
    .container {
 | 
			
		||||
      max-width: 1200px;
 | 
			
		||||
      margin: 0 auto;
 | 
			
		||||
      padding: 20px;
 | 
			
		||||
      font-family: Arial, sans-serif;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .card {
 | 
			
		||||
      border-radius: 8px;
 | 
			
		||||
      box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
 | 
			
		||||
      overflow: hidden;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .card-header {
 | 
			
		||||
      background-color: #4a6cf7;
 | 
			
		||||
      color: white;
 | 
			
		||||
      padding: 15px;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .card-body {
 | 
			
		||||
      padding: 20px;
 | 
			
		||||
    }
 | 
			
		||||
  </style>
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
  <div class="container">
 | 
			
		||||
    <div class="card">
 | 
			
		||||
      <div class="card-header">
 | 
			
		||||
        <h2>欢迎使用我们的应用</h2>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="card-body">
 | 
			
		||||
        <p>这是卡片内容区域。</p>
 | 
			
		||||
        <button onclick="alert('你点击了按钮!')">点击我</button>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
\`\`\`
 | 
			
		||||
 | 
			
		||||
## C# 代码示例
 | 
			
		||||
 | 
			
		||||
\`\`\`csharp
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
namespace Demo
 | 
			
		||||
{
 | 
			
		||||
    // 简单的用户类
 | 
			
		||||
    public class User
 | 
			
		||||
    {
 | 
			
		||||
        public int Id { get; set; }
 | 
			
		||||
        public string Name { get; set; }
 | 
			
		||||
        public string Email { get; set; }
 | 
			
		||||
        
 | 
			
		||||
        public override string ToString()
 | 
			
		||||
        {
 | 
			
		||||
            return $"User(Id={Id}, Name={Name}, Email={Email})";
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    public class Program
 | 
			
		||||
    {
 | 
			
		||||
        public static async Task<List<User>> GetUsersAsync()
 | 
			
		||||
        {
 | 
			
		||||
            // 模拟异步操作
 | 
			
		||||
            await Task.Delay(1000);
 | 
			
		||||
            
 | 
			
		||||
            return new List<User>
 | 
			
		||||
            {
 | 
			
		||||
                new User { Id = 1, Name = "张三", Email = "zhangsan@example.com" },
 | 
			
		||||
                new User { Id = 2, Name = "李四", Email = "lisi@example.com" },
 | 
			
		||||
                new User { Id = 3, Name = "王五", Email = "wangwu@example.com" }
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        public static void Main(string[] args)
 | 
			
		||||
        {
 | 
			
		||||
            Console.WriteLine("获取用户列表中...");
 | 
			
		||||
            
 | 
			
		||||
            var users = GetUsersAsync().GetAwaiter().GetResult();
 | 
			
		||||
            
 | 
			
		||||
            foreach (var user in users)
 | 
			
		||||
            {
 | 
			
		||||
                Console.WriteLine(user);
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            Console.WriteLine("完成!");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
\`\`\`
 | 
			
		||||
 | 
			
		||||
## Python 代码示例
 | 
			
		||||
 | 
			
		||||
\`\`\`python
 | 
			
		||||
import os
 | 
			
		||||
import json
 | 
			
		||||
from typing import List, Dict, Any, Optional
 | 
			
		||||
from dataclasses import dataclass
 | 
			
		||||
 | 
			
		||||
@dataclass
 | 
			
		||||
class Person:
 | 
			
		||||
    name: str
 | 
			
		||||
    age: int
 | 
			
		||||
    email: Optional[str] = None
 | 
			
		||||
    
 | 
			
		||||
    def greet(self) -> str:
 | 
			
		||||
        return f"你好,我是 {self.name},我 {self.age} 岁了。"
 | 
			
		||||
    
 | 
			
		||||
    def to_dict(self) -> Dict[str, Any]:
 | 
			
		||||
        return {
 | 
			
		||||
            "name": self.name,
 | 
			
		||||
            "age": self.age,
 | 
			
		||||
            "email": self.email
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
# 一些常用的 Python 函数
 | 
			
		||||
def read_json_file(file_path: str) -> Dict[str, Any]:
 | 
			
		||||
    """从JSON文件读取数据
 | 
			
		||||
    
 | 
			
		||||
    Args:
 | 
			
		||||
        file_path: JSON文件路径
 | 
			
		||||
        
 | 
			
		||||
    Returns:
 | 
			
		||||
        解析后的JSON数据
 | 
			
		||||
        
 | 
			
		||||
    Raises:
 | 
			
		||||
        FileNotFoundError: 如果文件不存在
 | 
			
		||||
        json.JSONDecodeError: 如果JSON格式不正确
 | 
			
		||||
    """
 | 
			
		||||
    if not os.path.exists(file_path):
 | 
			
		||||
        raise FileNotFoundError(f"文件不存在: {file_path}")
 | 
			
		||||
    
 | 
			
		||||
    with open(file_path, 'r', encoding='utf-8') as f:
 | 
			
		||||
        return json.load(f)
 | 
			
		||||
 | 
			
		||||
# 列表推导式与生成器
 | 
			
		||||
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
 | 
			
		||||
even_numbers = [n for n in numbers if n % 2 == 0]
 | 
			
		||||
squared = (n**2 for n in even_numbers)  # 生成器表达式
 | 
			
		||||
 | 
			
		||||
# 使用装饰器
 | 
			
		||||
def log_function_call(func):
 | 
			
		||||
    def wrapper(*args, **kwargs):
 | 
			
		||||
        print(f"调用函数: {func.__name__}")
 | 
			
		||||
        result = func(*args, **kwargs)
 | 
			
		||||
        print(f"函数返回: {result}")
 | 
			
		||||
        return result
 | 
			
		||||
    return wrapper
 | 
			
		||||
 | 
			
		||||
@log_function_call
 | 
			
		||||
def add(a: int, b: int) -> int:
 | 
			
		||||
    return a + b
 | 
			
		||||
 | 
			
		||||
if __name__ == "__main__":
 | 
			
		||||
    person = Person("张三", 30, "zhangsan@example.com")
 | 
			
		||||
    print(person.greet())
 | 
			
		||||
    print(add(5, 3))
 | 
			
		||||
\`\`\`
 | 
			
		||||
 | 
			
		||||
## VHDL 代码示例
 | 
			
		||||
 | 
			
		||||
\`\`\`vhdl
 | 
			
		||||
library IEEE;
 | 
			
		||||
use IEEE.STD_LOGIC_1164.ALL;
 | 
			
		||||
use IEEE.NUMERIC_STD.ALL;
 | 
			
		||||
 | 
			
		||||
entity Counter is
 | 
			
		||||
    Port ( 
 | 
			
		||||
        clk     : in  STD_LOGIC;
 | 
			
		||||
        reset   : in  STD_LOGIC;
 | 
			
		||||
        enable  : in  STD_LOGIC;
 | 
			
		||||
        count   : out STD_LOGIC_VECTOR(7 downto 0)
 | 
			
		||||
    );
 | 
			
		||||
end Counter;
 | 
			
		||||
 | 
			
		||||
architecture Behavioral of Counter is
 | 
			
		||||
    signal count_reg : unsigned(7 downto 0) := (others => '0');
 | 
			
		||||
begin
 | 
			
		||||
    process(clk, reset)
 | 
			
		||||
    begin
 | 
			
		||||
        if reset = '1' then
 | 
			
		||||
            count_reg <= (others => '0');
 | 
			
		||||
        elsif rising_edge(clk) then
 | 
			
		||||
            if enable = '1' then
 | 
			
		||||
                count_reg <= count_reg + 1;
 | 
			
		||||
            end if;
 | 
			
		||||
        end if;
 | 
			
		||||
    end process;
 | 
			
		||||
    
 | 
			
		||||
    count <= STD_LOGIC_VECTOR(count_reg);
 | 
			
		||||
end Behavioral;
 | 
			
		||||
\`\`\`
 | 
			
		||||
 | 
			
		||||
## Verilog 代码示例
 | 
			
		||||
 | 
			
		||||
\`\`\`verilog
 | 
			
		||||
module counter(
 | 
			
		||||
    input wire clk,
 | 
			
		||||
    input wire reset,
 | 
			
		||||
    input wire enable,
 | 
			
		||||
    output reg [7:0] count
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
    // 初始化计数器
 | 
			
		||||
    initial begin
 | 
			
		||||
        count = 8'b0;
 | 
			
		||||
    end
 | 
			
		||||
    
 | 
			
		||||
    // 在时钟上升沿处理
 | 
			
		||||
    always @(posedge clk or posedge reset) begin
 | 
			
		||||
        if (reset) begin
 | 
			
		||||
            count <= 8'b0;
 | 
			
		||||
        end else if (enable) begin
 | 
			
		||||
            count <= count + 1'b1;
 | 
			
		||||
        end
 | 
			
		||||
    end
 | 
			
		||||
    
 | 
			
		||||
    // 显示当前计数值
 | 
			
		||||
    always @(count) begin
 | 
			
		||||
        $display("当前计数值: %d", count);
 | 
			
		||||
    end
 | 
			
		||||
endmodule
 | 
			
		||||
\`\`\`
 | 
			
		||||
 | 
			
		||||
## JSON 示例
 | 
			
		||||
 | 
			
		||||
\`\`\`json
 | 
			
		||||
{
 | 
			
		||||
  "name": "fpga-weblab",
 | 
			
		||||
  "version": "1.0.0",
 | 
			
		||||
  "description": "FPGA WebLab 项目配置",
 | 
			
		||||
  "settings": {
 | 
			
		||||
    "theme": {
 | 
			
		||||
      "light": {
 | 
			
		||||
        "primary": "#4a6cf7",
 | 
			
		||||
        "secondary": "#f79e1b",
 | 
			
		||||
        "background": "#ffffff"
 | 
			
		||||
      },
 | 
			
		||||
      "dark": {
 | 
			
		||||
        "primary": "#6d8aff",
 | 
			
		||||
        "secondary": "#ffb74d",
 | 
			
		||||
        "background": "#121212"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "features": [
 | 
			
		||||
      "代码高亮",
 | 
			
		||||
      "实时预览",
 | 
			
		||||
      "项目管理",
 | 
			
		||||
      "远程硬件访问"
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "vue": "^3.5.0",
 | 
			
		||||
    "marked": "^12.0.0",
 | 
			
		||||
    "highlight.js": "^11.9.0"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
\`\`\`
 | 
			
		||||
 | 
			
		||||
## Shell 脚本示例
 | 
			
		||||
 | 
			
		||||
\`\`\`bash
 | 
			
		||||
#!/bin/bash
 | 
			
		||||
 | 
			
		||||
# 定义变量
 | 
			
		||||
PROJECT_DIR=$(pwd)
 | 
			
		||||
OUTPUT_DIR="$PROJECT_DIR/build"
 | 
			
		||||
LOG_FILE="$PROJECT_DIR/build.log"
 | 
			
		||||
 | 
			
		||||
# 创建输出目录
 | 
			
		||||
mkdir -p "$OUTPUT_DIR"
 | 
			
		||||
 | 
			
		||||
# 定义函数
 | 
			
		||||
function log_message() {
 | 
			
		||||
  local timestamp=$(date +"%Y-%m-%d %H:%M:%S")
 | 
			
		||||
  echo "[$timestamp] $1" | tee -a "$LOG_FILE"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# 清理旧构建
 | 
			
		||||
log_message "清理旧构建文件..."
 | 
			
		||||
rm -rf "$OUTPUT_DIR/*"
 | 
			
		||||
 | 
			
		||||
# 执行构建
 | 
			
		||||
log_message "开始构建项目..."
 | 
			
		||||
npm run build
 | 
			
		||||
 | 
			
		||||
# 检查构建结果
 | 
			
		||||
if [ $? -eq 0 ]; then
 | 
			
		||||
  log_message "构建成功!输出文件位于: $OUTPUT_DIR"
 | 
			
		||||
else
 | 
			
		||||
  log_message "构建失败,请检查错误信息"
 | 
			
		||||
  exit 1
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
# 统计文件数量
 | 
			
		||||
file_count=$(find "$OUTPUT_DIR" -type f | wc -l)
 | 
			
		||||
log_message "共构建了 $file_count 个文件"
 | 
			
		||||
 | 
			
		||||
# 显示环境信息
 | 
			
		||||
echo "系统信息:"
 | 
			
		||||
echo "----------------------"
 | 
			
		||||
echo "操作系统: $(uname -s)"
 | 
			
		||||
echo "Node 版本: $(node -v)"
 | 
			
		||||
echo "NPM 版本: $(npm -v)"
 | 
			
		||||
echo "磁盘空间: $(df -h | grep -E '^/dev')"
 | 
			
		||||
\`\`\`
 | 
			
		||||
 | 
			
		||||
## 其他示例
 | 
			
		||||
 | 
			
		||||
这里是一个内联代码示例:\`const value = calculate(x, y);\`
 | 
			
		||||
 | 
			
		||||
`);
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
</style>
 | 
			
		||||
@@ -26,9 +26,7 @@
 | 
			
		||||
      <div
 | 
			
		||||
        class="resizer bg-base-100 hover:bg-primary hover:opacity-70 active:bg-primary active:opacity-90 transition-colors"
 | 
			
		||||
        @mousedown="startResize"
 | 
			
		||||
      ></div>
 | 
			
		||||
 | 
			
		||||
      <!-- 右侧编辑区域 -->
 | 
			
		||||
      ></div>      <!-- 右侧编辑区域 -->
 | 
			
		||||
      <div
 | 
			
		||||
        class="bg-base-200 h-full overflow-hidden flex flex-col"
 | 
			
		||||
        :style="{ width: 100 - leftPanelWidth + '%' }"
 | 
			
		||||
@@ -41,10 +39,9 @@
 | 
			
		||||
            :componentConfig="selectedComponentConfig"
 | 
			
		||||
            @updateProp="updateComponentProp"
 | 
			
		||||
            @updateDirectProp="updateComponentDirectProp"
 | 
			
		||||
          />
 | 
			
		||||
          <div
 | 
			
		||||
          />          <div
 | 
			
		||||
            v-else
 | 
			
		||||
            class="doc-panel overflow-y-auto bg-base-100 rounded-md h-full"
 | 
			
		||||
            class="doc-panel overflow-y-auto h-full"
 | 
			
		||||
          >
 | 
			
		||||
            <MarkdownRenderer :content="documentContent" />
 | 
			
		||||
          </div>
 | 
			
		||||
@@ -82,20 +79,57 @@ import {
 | 
			
		||||
const showDocPanel = ref(false);
 | 
			
		||||
const documentContent = ref("");
 | 
			
		||||
 | 
			
		||||
// 获取路由参数
 | 
			
		||||
import { useRoute } from 'vue-router';
 | 
			
		||||
const route = useRoute();
 | 
			
		||||
 | 
			
		||||
// 切换文档面板和属性面板
 | 
			
		||||
async function toggleDocPanel() {
 | 
			
		||||
  showDocPanel.value = !showDocPanel.value;
 | 
			
		||||
 | 
			
		||||
  // 如果切换到文档面板,则获取文档内容
 | 
			
		||||
  if (showDocPanel.value) {
 | 
			
		||||
    const response = await fetch("/doc/01_water_led/water_led.md");
 | 
			
		||||
    documentContent.value = (await response.text()).replace(
 | 
			
		||||
      /.\/images/gi,
 | 
			
		||||
      "/doc/01_water_led/images",
 | 
			
		||||
    );
 | 
			
		||||
    await loadDocumentContent();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 加载文档内容
 | 
			
		||||
async function loadDocumentContent() {
 | 
			
		||||
  try {
 | 
			
		||||
    // 从路由参数中获取教程ID
 | 
			
		||||
    const tutorialId = route.query.tutorial as string || '02'; // 默认加载02例程
 | 
			
		||||
    
 | 
			
		||||
    // 构建文档路径
 | 
			
		||||
    let docPath = `/doc/${tutorialId}/doc.md`;
 | 
			
		||||
    
 | 
			
		||||
    // 检查当前路径是否包含下划线(例如 02_key 格式)
 | 
			
		||||
    // 如果不包含,那么使用更新的命名格式
 | 
			
		||||
    if (!tutorialId.includes('_')) {
 | 
			
		||||
      docPath = `/doc/${tutorialId}/doc.md`;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // 获取文档内容
 | 
			
		||||
    const response = await fetch(docPath);
 | 
			
		||||
    if (!response.ok) {
 | 
			
		||||
      throw new Error(`Failed to load document: ${response.status}`);
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // 更新文档内容,并替换图片路径
 | 
			
		||||
    documentContent.value = (await response.text())
 | 
			
		||||
      .replace(/.\/images/gi, `/doc/${tutorialId}/images`);
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error('加载文档失败:', error);
 | 
			
		||||
    documentContent.value = '# 文档加载失败\n\n无法加载请求的文档。';  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 检查是否有例程参数,如果有则自动打开文档面板
 | 
			
		||||
onMounted(async () => {
 | 
			
		||||
  if (route.query.tutorial) {
 | 
			
		||||
    showDocPanel.value = true;
 | 
			
		||||
    await loadDocumentContent();
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// --- 元器件管理 ---
 | 
			
		||||
const showComponentsMenu = ref(false);
 | 
			
		||||
const diagramData = ref<DiagramData>({
 | 
			
		||||
@@ -807,8 +841,10 @@ body {
 | 
			
		||||
/* 文档面板样式 */
 | 
			
		||||
.doc-panel {
 | 
			
		||||
  padding: 1.5rem;
 | 
			
		||||
  max-width: 800px;
 | 
			
		||||
  margin: 0 auto;
 | 
			
		||||
  max-width: 100%;
 | 
			
		||||
  margin: 0;
 | 
			
		||||
  background-color: transparent; /* 使用透明背景 */
 | 
			
		||||
  border: none; /* 确保没有边框 */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 文档切换按钮样式 */
 | 
			
		||||
 
 | 
			
		||||