add: home select exp

This commit is contained in:
alivender 2025-05-20 09:35:29 +08:00
parent 8eefed92a8
commit 6d640e8049
27 changed files with 1595 additions and 77 deletions

10
package-lock.json generated
View File

@ -11,6 +11,7 @@
"@svgdotjs/svg.js": "^3.2.4", "@svgdotjs/svg.js": "^3.2.4",
"@types/lodash": "^4.17.16", "@types/lodash": "^4.17.16",
"async-mutex": "^0.5.0", "async-mutex": "^0.5.0",
"highlight.js": "^11.11.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"log-symbols": "^7.0.0", "log-symbols": "^7.0.0",
"marked": "^12.0.0", "marked": "^12.0.0",
@ -2586,6 +2587,15 @@
"he": "bin/he" "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": { "node_modules/hookable": {
"version": "5.5.3", "version": "5.5.3",
"resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",

View File

@ -17,6 +17,7 @@
"@svgdotjs/svg.js": "^3.2.4", "@svgdotjs/svg.js": "^3.2.4",
"@types/lodash": "^4.17.16", "@types/lodash": "^4.17.16",
"async-mutex": "^0.5.0", "async-mutex": "^0.5.0",
"highlight.js": "^11.11.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"log-symbols": "^7.0.0", "log-symbols": "^7.0.0",
"marked": "^12.0.0", "marked": "^12.0.0",

View File

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

Before

Width:  |  Height:  |  Size: 2.9 MiB

After

Width:  |  Height:  |  Size: 2.9 MiB

View File

Before

Width:  |  Height:  |  Size: 620 KiB

After

Width:  |  Height:  |  Size: 620 KiB

View File

Before

Width:  |  Height:  |  Size: 635 KiB

After

Width:  |  Height:  |  Size: 635 KiB

View File

Before

Width:  |  Height:  |  Size: 821 KiB

After

Width:  |  Height:  |  Size: 821 KiB

View File

Before

Width:  |  Height:  |  Size: 434 KiB

After

Width:  |  Height:  |  Size: 434 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

Before

Width:  |  Height:  |  Size: 1.6 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

400
public/doc/11/doc.md Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

View 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 = "无法读取教程目录" });
}
}
}

View 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: '无法读取例程目录' });
}
}

View File

@ -1,26 +1,67 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'; import { computed, onMounted } from 'vue';
import { marked } from 'marked'; import { marked } from 'marked';
import hljs from 'highlight.js';
// -
import 'highlight.js/styles/github-dark.css';
const props = defineProps({ const props = defineProps({
content: { content: {
type: String, type: String,
required: true required: true
},
removeFirstH1: {
type: Boolean,
default: false
} }
}); });
const renderedContent = computed(() => { const renderedContent = computed(() => {
if (!props.content) return '<p>没有内容</p>'; if (!props.content) return '<p>没有内容</p>';
let processedContent = props.content; 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(); 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, renderer: renderer,
gfm: true, gfm: true,
breaks: true breaks: true
}); });
return marked(processedContent); return marked(processedContent);
}); });
//
onMounted(() => {
//
// hljs.highlightAll();
});
</script> </script>
<template> <template>
@ -33,6 +74,8 @@ const renderedContent = computed(() => {
line-height: 1.6; line-height: 1.6;
padding: 1rem 1.5rem; padding: 1rem 1.5rem;
max-width: 100%; max-width: 100%;
background-color: inherit; /* 继承父元素的背景色 */
height: 100%;
} }
.markdown-content :deep(img) { .markdown-content :deep(img) {
@ -45,47 +88,64 @@ const renderedContent = computed(() => {
} }
.markdown-content :deep(h1) { .markdown-content :deep(h1) {
margin-top: 2rem; margin-top: 2.5rem;
margin-bottom: 1rem; margin-bottom: 1.5rem;
color: hsl(var(--bc)); color: hsl(var(--bc));
font-weight: 700; font-weight: 700;
font-size: 2rem; font-size: 2.2rem;
line-height: 1.3; line-height: 1.3;
padding-bottom: 0.5rem; padding-bottom: 0.7rem;
border-bottom: 1px solid hsl(var(--b2)); border-bottom: 2px solid hsl(var(--p) / 0.7);
text-shadow: 1px 1px 2px rgba(0,0,0,0.05);
} }
.markdown-content :deep(h2) { .markdown-content :deep(h2) {
margin-top: 1.8rem; margin-top: 2rem;
margin-bottom: 0.8rem; margin-bottom: 1rem;
color: hsl(var(--bc)); color: hsl(var(--bc));
font-weight: 600; font-weight: 600;
font-size: 1.5rem; font-size: 1.7rem;
line-height: 1.4; line-height: 1.4;
padding-left: 0.5rem; padding: 0.5rem 1rem;
border-left: 4px solid hsl(var(--p)); 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) { .markdown-content :deep(h3) {
margin-top: 1.5rem; margin-top: 1.8rem;
margin-bottom: 0.75rem; margin-bottom: 0.9rem;
color: hsl(var(--bc)); color: hsl(var(--bc));
font-weight: 600; font-weight: 600;
font-size: 1.25rem; font-size: 1.4rem;
line-height: 1.4; line-height: 1.4;
padding-left: 1rem; 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(h4),
.markdown-content :deep(h5), .markdown-content :deep(h5),
.markdown-content :deep(h6) { .markdown-content :deep(h6) {
margin-top: 1.2rem; margin-top: 1.5rem;
margin-bottom: 0.6rem; margin-bottom: 0.7rem;
color: hsl(var(--bc)); color: hsl(var(--bc));
font-weight: 600; font-weight: 600;
font-size: 1.1rem; font-size: 1.2rem;
line-height: 1.5; line-height: 1.5;
padding-left: 1.5rem; 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) { .markdown-content :deep(p) {
@ -97,21 +157,31 @@ const renderedContent = computed(() => {
.markdown-content :deep(ul), .markdown-content :deep(ul),
.markdown-content :deep(ol) { .markdown-content :deep(ol) {
padding-left: 2em; padding-left: 2.5em;
margin: 0.75rem 0; margin: 1.25rem 0;
color: hsl(var(--bc) / 0.8); 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) { .markdown-content :deep(li) {
margin: 0.4rem 0; margin: 0.5rem 0;
position: relative; position: relative;
padding-left: 0.5rem;
} }
.markdown-content :deep(ul ul), .markdown-content :deep(ul ul),
.markdown-content :deep(ul ol), .markdown-content :deep(ul ol),
.markdown-content :deep(ol ul), .markdown-content :deep(ol ul),
.markdown-content :deep(ol ol) { .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) { .markdown-content :deep(ul) {
@ -134,45 +204,142 @@ const renderedContent = computed(() => {
color: hsl(var(--p)); color: hsl(var(--p));
} }
.markdown-content :deep(ol li::marker) {
color: hsl(var(--s));
}
/* 代码块样式增强 */
.markdown-content :deep(pre) { .markdown-content :deep(pre) {
background-color: hsl(var(--b3)); background-color: hsl(var(--b3));
padding: 1rem; padding: 1rem;
border-radius: 0.5rem; border-radius: 0.5rem;
overflow-x: auto; overflow-x: auto;
border: 1px solid hsl(var(--b2)); 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) { .markdown-content :deep(code) {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
background-color: hsl(var(--b3)); background-color: hsl(var(--b3) / 0.7);
padding: 2px 0.5rem; padding: 0.2rem 0.5rem;
border-radius: 0.25rem; border-radius: 0.25rem;
font-size: 0.9em; font-size: 0.9em;
color: hsl(var(--p)); 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) { .markdown-content :deep(table) {
border-collapse: collapse; border-collapse: separate;
border-spacing: 0;
width: 100%; width: 100%;
margin: 1rem 0; margin: 1.5rem 0;
background-color: hsl(var(--b1)); background-color: hsl(var(--b1));
border-radius: 0.5rem; border-radius: 0.5rem;
overflow: hidden; 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(th),
.markdown-content :deep(td) { .markdown-content :deep(td) {
border: 1px solid hsl(var(--b2)); border: 1px solid hsl(var(--b2));
padding: 0.75rem; padding: 0.75rem 1rem;
text-align: left; text-align: left;
} }
.markdown-content :deep(th) { .markdown-content :deep(th) {
background-color: hsl(var(--b2)); background-color: hsl(var(--p) / 0.15);
font-weight: 500; font-weight: 600;
color: hsl(var(--bc)); 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) { .markdown-content :deep(td) {
@ -180,12 +347,29 @@ const renderedContent = computed(() => {
} }
.markdown-content :deep(blockquote) { .markdown-content :deep(blockquote) {
margin: 1rem 0; margin: 1.5rem 0;
padding: 0.5rem 1rem; padding: 1rem 1.5rem;
border-left: 4px solid hsl(var(--p)); border-left: 4px solid hsl(var(--p));
background-color: hsl(var(--b2)); background-color: hsl(var(--b2) / 0.3);
color: hsl(var(--bc) / 0.8); color: hsl(var(--bc) / 0.9);
font-style: italic; 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) { .markdown-content :deep(hr) {
@ -204,4 +388,16 @@ const renderedContent = computed(() => {
color: hsl(var(--pf)); color: hsl(var(--pf));
text-decoration: underline; 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> </style>

View File

@ -49,6 +49,19 @@
测试功能 测试功能
</router-link> </router-link>
</li> </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"> <li class="my-1 hover:translate-x-1 transition-all duration-300">
<a href="http://localhost:5000/swagger" target="_self" rel="noopener noreferrer" <a href="http://localhost:5000/swagger" target="_self" rel="noopener noreferrer"
class="text-base font-medium"> class="text-base font-medium">

View 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>

View File

@ -1,23 +1,57 @@
import { createWebHistory, createRouter } from "vue-router"; import { createRouter, createWebHistory } from 'vue-router'
import LoginView from "../views/LoginView.vue"; import HomeView from '../views/HomeView.vue'
import UserView from "../views/UserView.vue"; import LoginView from '../views/LoginView.vue'
import TestView from "../views/TestView.vue"; import LabView from '../views/LabView.vue'
import ProjectView from "../views/ProjectView.vue"; import ProjectView from '../views/ProjectView.vue'
import HomeView from "@/views/HomeView.vue"; import TestView from '../views/TestView.vue'
import AdminView from "@/views/AdminView.vue"; import UserView from '../views/UserView.vue'
import AdminView from '../views/AdminView.vue'
const routes = [ import MarkdownTestView from '../views/MarkdownTestView.vue'
{ 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 },
];
const router = createRouter({ const router = createRouter({
history: createWebHistory(), history: createWebHistory(import.meta.env.BASE_URL),
routes, 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

View File

@ -1,14 +1,9 @@
<template> <template>
<div class="bg-base-200 min-h-screen"> <div class="bg-base-200 min-h-screen">
<main class="hero min-h-screen bg-base-200"> <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="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;">
<div <TutorialCarousel :autoRotationInterval="3000" />
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> </div>
<!-- 内容容器 --> <!-- 内容容器 -->
<div class="content-container max-w-md lg:max-w-2xl transform transition-all duration-500 ease-in-out"> <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> <script lang="ts" setup>
import "@/router"; import "@/router";
import TutorialCarousel from "@/components/TutorialCarousel.vue";
</script> </script>
<style scoped lang="postcss"> <style scoped lang="postcss">

View 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>

View File

@ -26,9 +26,7 @@
<div <div
class="resizer bg-base-100 hover:bg-primary hover:opacity-70 active:bg-primary active:opacity-90 transition-colors" class="resizer bg-base-100 hover:bg-primary hover:opacity-70 active:bg-primary active:opacity-90 transition-colors"
@mousedown="startResize" @mousedown="startResize"
></div> ></div> <!-- 右侧编辑区域 -->
<!-- 右侧编辑区域 -->
<div <div
class="bg-base-200 h-full overflow-hidden flex flex-col" class="bg-base-200 h-full overflow-hidden flex flex-col"
:style="{ width: 100 - leftPanelWidth + '%' }" :style="{ width: 100 - leftPanelWidth + '%' }"
@ -41,10 +39,9 @@
:componentConfig="selectedComponentConfig" :componentConfig="selectedComponentConfig"
@updateProp="updateComponentProp" @updateProp="updateComponentProp"
@updateDirectProp="updateComponentDirectProp" @updateDirectProp="updateComponentDirectProp"
/> /> <div
<div
v-else 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" /> <MarkdownRenderer :content="documentContent" />
</div> </div>
@ -82,20 +79,57 @@ import {
const showDocPanel = ref(false); const showDocPanel = ref(false);
const documentContent = ref(""); const documentContent = ref("");
//
import { useRoute } from 'vue-router';
const route = useRoute();
// //
async function toggleDocPanel() { async function toggleDocPanel() {
showDocPanel.value = !showDocPanel.value; showDocPanel.value = !showDocPanel.value;
// //
if (showDocPanel.value) { if (showDocPanel.value) {
const response = await fetch("/doc/01_water_led/water_led.md"); await loadDocumentContent();
documentContent.value = (await response.text()).replace(
/.\/images/gi,
"/doc/01_water_led/images",
);
} }
} }
//
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 showComponentsMenu = ref(false);
const diagramData = ref<DiagramData>({ const diagramData = ref<DiagramData>({
@ -807,8 +841,10 @@ body {
/* 文档面板样式 */ /* 文档面板样式 */
.doc-panel { .doc-panel {
padding: 1.5rem; padding: 1.5rem;
max-width: 800px; max-width: 100%;
margin: 0 auto; margin: 0;
background-color: transparent; /* 使用透明背景 */
border: none; /* 确保没有边框 */
} }
/* 文档切换按钮样式 */ /* 文档切换按钮样式 */