feat: 添加用户手册只读预览功能
- 新增 UserManualPanel 组件实现只读预览 - 支持 Markdown 完整渲染(表格、代码块、图片、分隔线) - 优化排版和字体大小 - 用户无法编辑手册内容
This commit is contained in:
@ -104,7 +104,7 @@
|
|||||||
|
|
||||||
**症状**:安装 VSIX 文件时报错
|
**症状**:安装 VSIX 文件时报错
|
||||||
|
|
||||||
#### **解决方案**:
|
#### 解决方案:
|
||||||
|
|
||||||
- 确认 VS Code 版本 >= 1.60.0
|
- 确认 VS Code 版本 >= 1.60.0
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import * as vscode from "vscode";
|
|||||||
import { ICViewProvider } from "./views/ICViewProvider";
|
import { ICViewProvider } from "./views/ICViewProvider";
|
||||||
import { showICHelperPanel } from "./panels/ICHelperPanel";
|
import { showICHelperPanel } from "./panels/ICHelperPanel";
|
||||||
import { VCDViewerPanel, VCDViewerEditorProvider } from "./panels/VCDViewerPanel";
|
import { VCDViewerPanel, VCDViewerEditorProvider } from "./panels/VCDViewerPanel";
|
||||||
|
import { UserManualPanel } from "./panels/UserManualPanel";
|
||||||
import { ChatHistoryManager } from "./utils/chatHistoryManager";
|
import { ChatHistoryManager } from "./utils/chatHistoryManager";
|
||||||
import { ICCoderAuthenticationProvider } from "./services/icCoderAuthProvider";
|
import { ICCoderAuthenticationProvider } from "./services/icCoderAuthProvider";
|
||||||
import { VCDFileServer } from "./services/vcdFileServer";
|
import { VCDFileServer } from "./services/vcdFileServer";
|
||||||
@ -184,9 +185,8 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||||||
// 注册命令:打开用户手册
|
// 注册命令:打开用户手册
|
||||||
const openUserManualCommand = vscode.commands.registerCommand(
|
const openUserManualCommand = vscode.commands.registerCommand(
|
||||||
"ic-coder.openUserManual",
|
"ic-coder.openUserManual",
|
||||||
async () => {
|
() => {
|
||||||
const manualPath = vscode.Uri.joinPath(context.extensionUri, "media", "USER_MANUAL.md");
|
UserManualPanel.render(context.extensionUri);
|
||||||
await vscode.commands.executeCommand("markdown.showPreview", manualPath);
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
181
src/panels/UserManualPanel.ts
Normal file
181
src/panels/UserManualPanel.ts
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
/**
|
||||||
|
* 用户手册只读预览面板
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as vscode from "vscode";
|
||||||
|
|
||||||
|
export class UserManualPanel {
|
||||||
|
public static currentPanel: UserManualPanel | undefined;
|
||||||
|
private readonly _panel: vscode.WebviewPanel;
|
||||||
|
private _disposables: vscode.Disposable[] = [];
|
||||||
|
|
||||||
|
private constructor(panel: vscode.WebviewPanel, extensionUri: vscode.Uri) {
|
||||||
|
this._panel = panel;
|
||||||
|
this._panel.onDidDispose(() => this.dispose(), null, this._disposables);
|
||||||
|
this._update(extensionUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static render(extensionUri: vscode.Uri) {
|
||||||
|
if (UserManualPanel.currentPanel) {
|
||||||
|
UserManualPanel.currentPanel._panel.reveal(vscode.ViewColumn.One);
|
||||||
|
} else {
|
||||||
|
const panel = vscode.window.createWebviewPanel(
|
||||||
|
"userManual",
|
||||||
|
"IC Coder 用户手册",
|
||||||
|
vscode.ViewColumn.One,
|
||||||
|
{
|
||||||
|
enableScripts: true,
|
||||||
|
localResourceRoots: [vscode.Uri.joinPath(extensionUri, "media")],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
UserManualPanel.currentPanel = new UserManualPanel(panel, extensionUri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _update(extensionUri: vscode.Uri) {
|
||||||
|
const manualPath = vscode.Uri.joinPath(
|
||||||
|
extensionUri,
|
||||||
|
"media",
|
||||||
|
"USER_MANUAL.md",
|
||||||
|
);
|
||||||
|
const markdown = await vscode.workspace.fs.readFile(manualPath);
|
||||||
|
const content = Buffer.from(markdown).toString("utf-8");
|
||||||
|
this._panel.webview.html = await this._getHtmlContent(
|
||||||
|
content,
|
||||||
|
extensionUri,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _getHtmlContent(
|
||||||
|
markdown: string,
|
||||||
|
extensionUri: vscode.Uri,
|
||||||
|
): Promise<string> {
|
||||||
|
let inCodeBlock = false;
|
||||||
|
let inTable = false;
|
||||||
|
let tableRows: string[] = [];
|
||||||
|
const lines: string[] = [];
|
||||||
|
|
||||||
|
// 先处理图片
|
||||||
|
markdown = markdown.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (_, alt, src) => {
|
||||||
|
const imgUri = this._panel.webview.asWebviewUri(
|
||||||
|
vscode.Uri.joinPath(extensionUri, "media", src),
|
||||||
|
);
|
||||||
|
return `<img src="${imgUri}" alt="${alt}">`;
|
||||||
|
});
|
||||||
|
|
||||||
|
markdown.split("\n").forEach((line) => {
|
||||||
|
// 代码块
|
||||||
|
if (line.startsWith("```")) {
|
||||||
|
if (inCodeBlock) {
|
||||||
|
lines.push("</code></pre>");
|
||||||
|
inCodeBlock = false;
|
||||||
|
} else {
|
||||||
|
lines.push("<pre><code>");
|
||||||
|
inCodeBlock = true;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (inCodeBlock) {
|
||||||
|
lines.push(line);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 表格
|
||||||
|
if (line.startsWith("|")) {
|
||||||
|
if (!inTable) inTable = true;
|
||||||
|
tableRows.push(line);
|
||||||
|
return;
|
||||||
|
} else if (inTable) {
|
||||||
|
// 表格结束
|
||||||
|
const headers = tableRows[0]
|
||||||
|
.split("|")
|
||||||
|
.filter((c) => c.trim())
|
||||||
|
.map((h) => `<th>${h.trim()}</th>`)
|
||||||
|
.join("");
|
||||||
|
const body = tableRows
|
||||||
|
.slice(2)
|
||||||
|
.map(
|
||||||
|
(r) =>
|
||||||
|
"<tr>" +
|
||||||
|
r
|
||||||
|
.split("|")
|
||||||
|
.filter((c) => c.trim())
|
||||||
|
.map((c) => `<td>${c.trim()}</td>`)
|
||||||
|
.join("") +
|
||||||
|
"</tr>",
|
||||||
|
)
|
||||||
|
.join("");
|
||||||
|
lines.push(
|
||||||
|
`<table><thead><tr>${headers}</tr></thead><tbody>${body}</tbody></table>`,
|
||||||
|
);
|
||||||
|
tableRows = [];
|
||||||
|
inTable = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 其他行
|
||||||
|
if (line === "---") lines.push("<hr>");
|
||||||
|
else if (line.startsWith("#### "))
|
||||||
|
lines.push(`<h4>${line.slice(5)}</h4>`);
|
||||||
|
else if (line.startsWith("### ")) lines.push(`<h3>${line.slice(4)}</h3>`);
|
||||||
|
else if (line.startsWith("## ")) lines.push(`<h2>${line.slice(3)}</h2>`);
|
||||||
|
else if (line.startsWith("# ")) lines.push(`<h1>${line.slice(2)}</h1>`);
|
||||||
|
else if (line.startsWith("- "))
|
||||||
|
lines.push(
|
||||||
|
`<li>${line.slice(2).replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>")}</li>`,
|
||||||
|
);
|
||||||
|
else if (line.trim() === "") lines.push("<p></p>");
|
||||||
|
else
|
||||||
|
lines.push(
|
||||||
|
`<p>${line.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>").replace(/\[(.+?)\]\((.+?)\)/g, '<a href="$2">$1</a>')}</p>`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const html = lines
|
||||||
|
.join("\n")
|
||||||
|
.replace(/((?:<li>.*<\/li>\n?)+)/g, "<ul>$1</ul>");
|
||||||
|
|
||||||
|
return `<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||||
|
padding: 40px;
|
||||||
|
line-height: 1.8;
|
||||||
|
font-size: 16px;
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
h1 { font-size: 2em; border-bottom: 3px solid #ddd; padding-bottom: 15px; margin: 30px 0 20px; }
|
||||||
|
h2 { font-size: 1.6em; margin-top: 40px; border-bottom: 2px solid #eee; padding-bottom: 10px; }
|
||||||
|
h3 { font-size: 1.3em; margin-top: 30px; }
|
||||||
|
h4 { font-size: 1.1em; margin-top: 20px; font-weight: 600; }
|
||||||
|
p { margin: 15px 0; }
|
||||||
|
img { display: block; margin: 30px auto; max-width: 100%; border: 1px solid #ddd; border-radius: 6px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
|
||||||
|
table { border-collapse: collapse; width: 100%; margin: 25px 0; font-size: 15px; }
|
||||||
|
th, td { border: 1px solid #ddd; padding: 12px 16px; text-align: left; }
|
||||||
|
th { background: #636363; font-weight: 600; }
|
||||||
|
tr:hover { background: #636363; }
|
||||||
|
ul { margin: 15px 0; padding-left: 30px; }
|
||||||
|
li { margin: 8px 0; margin-left: 40px;}
|
||||||
|
pre { background: #2f2f2f; padding: 20px; border-radius: 6px; overflow-x: auto; margin: 20px 0; border: 1px solid #e0e0e0; }
|
||||||
|
code { font-family: 'Consolas', 'Monaco', monospace; font-size: 14px; line-height: 1.6; }
|
||||||
|
hr { border: none; border-top: 2px solid #e0e0e0; margin: 30px 0; }
|
||||||
|
a { color: #0066cc; text-decoration: none; }
|
||||||
|
a:hover { text-decoration: underline; }
|
||||||
|
strong { font-weight: 600; color: #e5e5e5; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>${html}</body>
|
||||||
|
</html>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
public dispose() {
|
||||||
|
UserManualPanel.currentPanel = undefined;
|
||||||
|
this._panel.dispose();
|
||||||
|
while (this._disposables.length) {
|
||||||
|
this._disposables.pop()?.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user