Compare commits
1 Commits
Author | SHA1 | Date | |
---|---|---|---|
76c084c38c |
@ -1,13 +1,23 @@
|
|||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
|
import * as WS from 'ws';
|
||||||
|
|
||||||
|
type ListenerParam = {
|
||||||
|
type: string,
|
||||||
|
data: any,
|
||||||
|
filePath?: string
|
||||||
|
functionName?: string
|
||||||
|
mdPath?: string
|
||||||
|
}
|
||||||
|
type WebviewViewIns = vscode.WebviewView['webview']
|
||||||
|
|
||||||
export class ChatViewProvider implements vscode.WebviewViewProvider {
|
export class ChatViewProvider implements vscode.WebviewViewProvider {
|
||||||
public static readonly viewType = 'aiChatView';
|
public static readonly viewType = 'aiChatView';
|
||||||
|
|
||||||
private _view?: vscode.WebviewView;
|
private _view?: vscode.WebviewView;
|
||||||
private _logger: (message: string) => void;
|
private _logger: (message: string) => void;
|
||||||
|
private _wsInstance?: WS | null
|
||||||
constructor(
|
constructor(
|
||||||
private readonly _extensionUri: vscode.Uri,
|
private readonly _extensionUri: vscode.Uri,
|
||||||
logger?: (message: string) => void
|
logger?: (message: string) => void
|
||||||
@ -30,21 +40,31 @@ export class ChatViewProvider implements vscode.WebviewViewProvider {
|
|||||||
this._extensionUri
|
this._extensionUri
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
webviewView.webview.html = this._getHtmlForWebview(webviewView.webview);
|
webviewView.webview.html = this._getHtmlForWebview(webviewView.webview);
|
||||||
|
const webview = webviewView.webview
|
||||||
|
// 在生产环境中,使用打包后的Webview文件
|
||||||
this._logger('WebView HTML内容已设置');
|
this._logger('WebView HTML内容已设置');
|
||||||
|
let wsInstance = null
|
||||||
// 处理来自Webview的消息
|
// 处理来自Webview的消息
|
||||||
webviewView.webview.onDidReceiveMessage(message => {
|
webviewView.webview.onDidReceiveMessage(message => {
|
||||||
this._logger(`收到WebView消息: ${message.type}`);
|
this._logger(`收到WebView消息: ${message.type}`);
|
||||||
|
if (message.type?.startsWith('ws:')) {
|
||||||
|
this.wsConnect(message, webview)
|
||||||
|
return
|
||||||
|
}
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
|
case 'loadMd':
|
||||||
|
this.fetchMdFile(message, webview)
|
||||||
|
break
|
||||||
|
case 'openFileAndGoToFunction':
|
||||||
|
this.openFileAndGoToFunction(message)
|
||||||
|
break
|
||||||
case 'getSettings':
|
case 'getSettings':
|
||||||
this._logger(`处理getSettings请求`);
|
this._logger(`处理getSettings请求`);
|
||||||
this._sendSettings();
|
this._sendSettings();
|
||||||
break;
|
break;
|
||||||
case 'saveSettings':
|
case 'saveSettings':
|
||||||
this._logger(`保存设置: ${JSON.stringify(message.settings, (key, value) =>
|
this._logger(`保存设置: ${JSON.stringify(message.settings, (key, value) =>
|
||||||
key === 'apiKey' ? '***' : value)}`);
|
key === 'apiKey' ? '***' : value)}`);
|
||||||
this._saveSettings(message.settings);
|
this._saveSettings(message.settings);
|
||||||
break;
|
break;
|
||||||
@ -60,12 +80,12 @@ export class ChatViewProvider implements vscode.WebviewViewProvider {
|
|||||||
this._logger(`⚙️ SettingsModal日志: ${message.message}`);
|
this._logger(`⚙️ SettingsModal日志: ${message.message}`);
|
||||||
}
|
}
|
||||||
// 为WebSocket连接相关日志添加更详细的输出
|
// 为WebSocket连接相关日志添加更详细的输出
|
||||||
else if (message.message.includes('WebSocket') ||
|
else if (message.message.includes('WebSocket') ||
|
||||||
message.message.includes('ChatStore') ||
|
message.message.includes('ChatStore') ||
|
||||||
message.message.includes('initWebSocketService')
|
message.message.includes('initWebSocketService')
|
||||||
) {
|
) {
|
||||||
this._logger(`🔌 WebSocket日志: ${message.message}`);
|
this._logger(`🔌 WebSocket日志: ${message.message}`);
|
||||||
}
|
}
|
||||||
// 为重要日志添加特殊标记
|
// 为重要日志添加特殊标记
|
||||||
else if (message.message.includes('【重要】')) {
|
else if (message.message.includes('【重要】')) {
|
||||||
this._logger(`⚠️ ${message.message}`);
|
this._logger(`⚠️ ${message.message}`);
|
||||||
@ -85,10 +105,10 @@ export class ChatViewProvider implements vscode.WebviewViewProvider {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 初始化时发送主题颜色
|
// 初始化时发送主题颜色
|
||||||
this._sendThemeColors();
|
this._sendThemeColors();
|
||||||
|
|
||||||
// 监听主题变化
|
// 监听主题变化
|
||||||
vscode.window.onDidChangeActiveColorTheme(() => {
|
vscode.window.onDidChangeActiveColorTheme(() => {
|
||||||
this._sendThemeColors();
|
this._sendThemeColors();
|
||||||
@ -98,45 +118,45 @@ export class ChatViewProvider implements vscode.WebviewViewProvider {
|
|||||||
// 发送主题颜色
|
// 发送主题颜色
|
||||||
private _sendThemeColors() {
|
private _sendThemeColors() {
|
||||||
if (!this._view) return;
|
if (!this._view) return;
|
||||||
|
|
||||||
// 获取当前主题颜色
|
// 获取当前主题颜色
|
||||||
const theme = vscode.window.activeColorTheme;
|
const theme = vscode.window.activeColorTheme;
|
||||||
const isDark = theme.kind === vscode.ColorThemeKind.Dark;
|
const isDark = theme.kind === vscode.ColorThemeKind.Dark;
|
||||||
const isLight = theme.kind === vscode.ColorThemeKind.Light;
|
const isLight = theme.kind === vscode.ColorThemeKind.Light;
|
||||||
|
|
||||||
// 获取常用颜色
|
// 获取常用颜色
|
||||||
const colors = {
|
const colors = {
|
||||||
// 基础颜色
|
// 基础颜色
|
||||||
foreground: this._getColor('foreground'),
|
foreground: this._getColor('foreground'),
|
||||||
background: this._getColor('editor.background'),
|
background: this._getColor('editor.background'),
|
||||||
|
|
||||||
// 文本颜色
|
// 文本颜色
|
||||||
descriptionForeground: this._getColor('descriptionForeground'),
|
descriptionForeground: this._getColor('descriptionForeground'),
|
||||||
errorForeground: this._getColor('errorForeground'),
|
errorForeground: this._getColor('errorForeground'),
|
||||||
|
|
||||||
// 按钮颜色
|
// 按钮颜色
|
||||||
buttonBackground: this._getColor('button.background'),
|
buttonBackground: this._getColor('button.background'),
|
||||||
buttonForeground: this._getColor('button.foreground'),
|
buttonForeground: this._getColor('button.foreground'),
|
||||||
buttonHoverBackground: this._getColor('button.hoverBackground'),
|
buttonHoverBackground: this._getColor('button.hoverBackground'),
|
||||||
|
|
||||||
// 输入框颜色
|
// 输入框颜色
|
||||||
inputBackground: this._getColor('input.background'),
|
inputBackground: this._getColor('input.background'),
|
||||||
inputForeground: this._getColor('input.foreground'),
|
inputForeground: this._getColor('input.foreground'),
|
||||||
inputPlaceholderForeground: this._getColor('input.placeholderForeground'),
|
inputPlaceholderForeground: this._getColor('input.placeholderForeground'),
|
||||||
inputBorder: this._getColor('input.border'),
|
inputBorder: this._getColor('input.border'),
|
||||||
|
|
||||||
// 链接颜色
|
// 链接颜色
|
||||||
linkForeground: this._getColor('textLink.foreground'),
|
linkForeground: this._getColor('textLink.foreground'),
|
||||||
linkActiveForeground: this._getColor('textLink.activeForeground'),
|
linkActiveForeground: this._getColor('textLink.activeForeground'),
|
||||||
|
|
||||||
// 其他界面元素
|
// 其他界面元素
|
||||||
panelBorder: this._getColor('panel.border'),
|
panelBorder: this._getColor('panel.border'),
|
||||||
|
|
||||||
// 主题类型
|
// 主题类型
|
||||||
isDark,
|
isDark,
|
||||||
isLight
|
isLight
|
||||||
};
|
};
|
||||||
|
|
||||||
this._view.webview.postMessage({
|
this._view.webview.postMessage({
|
||||||
type: 'themeColors',
|
type: 'themeColors',
|
||||||
colors
|
colors
|
||||||
@ -162,17 +182,17 @@ export class ChatViewProvider implements vscode.WebviewViewProvider {
|
|||||||
'textLink.activeForeground': '#3794ff',
|
'textLink.activeForeground': '#3794ff',
|
||||||
'panel.border': '#80808059'
|
'panel.border': '#80808059'
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 尝试从配置中获取颜色
|
// 尝试从配置中获取颜色
|
||||||
const themeOverride = vscode.workspace.getConfiguration('workbench')
|
const themeOverride = vscode.workspace.getConfiguration('workbench')
|
||||||
.get<Record<string, string>>('colorCustomizations', {});
|
.get<Record<string, string>>('colorCustomizations', {});
|
||||||
|
|
||||||
// 首先尝试从用户配置的覆盖颜色中获取
|
// 首先尝试从用户配置的覆盖颜色中获取
|
||||||
if (themeOverride[colorId]) {
|
if (themeOverride[colorId]) {
|
||||||
return themeOverride[colorId];
|
return themeOverride[colorId];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 然后从默认颜色中获取
|
// 然后从默认颜色中获取
|
||||||
return defaultColors[colorId] || '#cccccc';
|
return defaultColors[colorId] || '#cccccc';
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -185,13 +205,13 @@ export class ChatViewProvider implements vscode.WebviewViewProvider {
|
|||||||
// 发送设置
|
// 发送设置
|
||||||
private _sendSettings() {
|
private _sendSettings() {
|
||||||
if (!this._view) return;
|
if (!this._view) return;
|
||||||
|
|
||||||
const config = vscode.workspace.getConfiguration('aiChat');
|
const config = vscode.workspace.getConfiguration('aiChat');
|
||||||
const settings = {
|
const settings = {
|
||||||
apiHost: config.get('apiHost', ''),
|
apiHost: config.get('apiHost', ''),
|
||||||
apiKey: config.get('apiKey', '')
|
apiKey: config.get('apiKey', '')
|
||||||
};
|
};
|
||||||
|
|
||||||
this._view.webview.postMessage({
|
this._view.webview.postMessage({
|
||||||
type: 'settings',
|
type: 'settings',
|
||||||
settings
|
settings
|
||||||
@ -201,23 +221,23 @@ export class ChatViewProvider implements vscode.WebviewViewProvider {
|
|||||||
// 保存设置
|
// 保存设置
|
||||||
private _saveSettings(settings: { apiHost: string; apiKey: string }) {
|
private _saveSettings(settings: { apiHost: string; apiKey: string }) {
|
||||||
if (!settings) return;
|
if (!settings) return;
|
||||||
|
|
||||||
this._logger(`准备保存设置: apiHost=${settings.apiHost}, apiKey=***`);
|
this._logger(`准备保存设置: apiHost=${settings.apiHost}, apiKey=***`);
|
||||||
|
|
||||||
const config = vscode.workspace.getConfiguration('aiChat');
|
const config = vscode.workspace.getConfiguration('aiChat');
|
||||||
let hasChanges = false;
|
let hasChanges = false;
|
||||||
|
|
||||||
// 保存成功标志
|
// 保存成功标志
|
||||||
let success = true;
|
let success = true;
|
||||||
let errorMessage = '';
|
let errorMessage = '';
|
||||||
|
|
||||||
// 检查并更新API主机
|
// 检查并更新API主机
|
||||||
const updateHost = async () => {
|
const updateHost = async () => {
|
||||||
if (settings.apiHost === config.get('apiHost')) return;
|
if (settings.apiHost === config.get('apiHost')) return;
|
||||||
|
|
||||||
hasChanges = true;
|
hasChanges = true;
|
||||||
this._logger('更新API主机设置');
|
this._logger('更新API主机设置');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await config.update('apiHost', settings.apiHost, vscode.ConfigurationTarget.Global);
|
await config.update('apiHost', settings.apiHost, vscode.ConfigurationTarget.Global);
|
||||||
this._logger('已更新API主机设置');
|
this._logger('已更新API主机设置');
|
||||||
@ -227,14 +247,14 @@ export class ChatViewProvider implements vscode.WebviewViewProvider {
|
|||||||
this._logger(`更新API主机设置失败: ${errorMessage}`);
|
this._logger(`更新API主机设置失败: ${errorMessage}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 检查并更新API密钥
|
// 检查并更新API密钥
|
||||||
const updateKey = async () => {
|
const updateKey = async () => {
|
||||||
if (settings.apiKey === config.get('apiKey')) return;
|
if (settings.apiKey === config.get('apiKey')) return;
|
||||||
|
|
||||||
hasChanges = true;
|
hasChanges = true;
|
||||||
this._logger('更新API密钥设置');
|
this._logger('更新API密钥设置');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await config.update('apiKey', settings.apiKey, vscode.ConfigurationTarget.Global);
|
await config.update('apiKey', settings.apiKey, vscode.ConfigurationTarget.Global);
|
||||||
this._logger('已更新API密钥设置');
|
this._logger('已更新API密钥设置');
|
||||||
@ -244,12 +264,12 @@ export class ChatViewProvider implements vscode.WebviewViewProvider {
|
|||||||
this._logger(`更新API密钥设置失败: ${errorMessage}`);
|
this._logger(`更新API密钥设置失败: ${errorMessage}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 执行所有更新操作
|
// 执行所有更新操作
|
||||||
const updateAll = async () => {
|
const updateAll = async () => {
|
||||||
await updateHost();
|
await updateHost();
|
||||||
await updateKey();
|
await updateKey();
|
||||||
|
|
||||||
// 通知前端设置已保存
|
// 通知前端设置已保存
|
||||||
if (this._view) {
|
if (this._view) {
|
||||||
if (success) {
|
if (success) {
|
||||||
@ -268,11 +288,11 @@ export class ChatViewProvider implements vscode.WebviewViewProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 启动更新过程
|
// 启动更新过程
|
||||||
updateAll().catch(error => {
|
updateAll().catch(error => {
|
||||||
this._logger(`保存设置过程中发生未捕获异常: ${error}`);
|
this._logger(`保存设置过程中发生未捕获异常: ${error}`);
|
||||||
|
|
||||||
// 确保前端收到失败消息
|
// 确保前端收到失败消息
|
||||||
if (this._view) {
|
if (this._view) {
|
||||||
this._view.webview.postMessage({
|
this._view.webview.postMessage({
|
||||||
@ -288,10 +308,10 @@ export class ChatViewProvider implements vscode.WebviewViewProvider {
|
|||||||
private _getHtmlForWebview(webview: vscode.Webview) {
|
private _getHtmlForWebview(webview: vscode.Webview) {
|
||||||
// 在生产环境中,使用打包后的Webview文件
|
// 在生产环境中,使用打包后的Webview文件
|
||||||
const distPath = path.join(this._extensionUri.fsPath, 'webview', 'dist');
|
const distPath = path.join(this._extensionUri.fsPath, 'webview', 'dist');
|
||||||
|
|
||||||
// 生成随机nonce以增强安全性
|
// 生成随机nonce以增强安全性
|
||||||
const nonce = this._getNonce();
|
const nonce = this._getNonce();
|
||||||
|
|
||||||
// 检查dist目录是否存在
|
// 检查dist目录是否存在
|
||||||
if (fs.existsSync(distPath)) {
|
if (fs.existsSync(distPath)) {
|
||||||
try {
|
try {
|
||||||
@ -300,7 +320,7 @@ export class ChatViewProvider implements vscode.WebviewViewProvider {
|
|||||||
path.join(distPath, 'index.html'),
|
path.join(distPath, 'index.html'),
|
||||||
'utf-8'
|
'utf-8'
|
||||||
);
|
);
|
||||||
|
|
||||||
// 添加CSP策略和nonce
|
// 添加CSP策略和nonce
|
||||||
if (!indexHtml.includes('content-security-policy')) {
|
if (!indexHtml.includes('content-security-policy')) {
|
||||||
indexHtml = indexHtml.replace(
|
indexHtml = indexHtml.replace(
|
||||||
@ -309,7 +329,7 @@ export class ChatViewProvider implements vscode.WebviewViewProvider {
|
|||||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; connect-src ws://localhost:8080; style-src ${webview.cspSource} 'unsafe-inline'; img-src ${webview.cspSource} https: data:; script-src 'nonce-${nonce}';">`
|
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; connect-src ws://localhost:8080; style-src ${webview.cspSource} 'unsafe-inline'; img-src ${webview.cspSource} https: data:; script-src 'nonce-${nonce}';">`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确保有VSCode API脚本
|
// 确保有VSCode API脚本
|
||||||
if (!indexHtml.includes('acquireVsCodeApi')) {
|
if (!indexHtml.includes('acquireVsCodeApi')) {
|
||||||
indexHtml = indexHtml.replace(
|
indexHtml = indexHtml.replace(
|
||||||
@ -321,7 +341,7 @@ export class ChatViewProvider implements vscode.WebviewViewProvider {
|
|||||||
</script>`
|
</script>`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将资源路径替换为Webview可访问的URI
|
// 将资源路径替换为Webview可访问的URI
|
||||||
indexHtml = indexHtml.replace(
|
indexHtml = indexHtml.replace(
|
||||||
/(href|src)="([^"]*)"/g,
|
/(href|src)="([^"]*)"/g,
|
||||||
@ -330,21 +350,21 @@ export class ChatViewProvider implements vscode.WebviewViewProvider {
|
|||||||
if (url.startsWith('http') || url.startsWith('data:')) {
|
if (url.startsWith('http') || url.startsWith('data:')) {
|
||||||
return `${attribute}="${url}"`;
|
return `${attribute}="${url}"`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 转换为Webview URI
|
// 转换为Webview URI
|
||||||
const resourceUri = webview.asWebviewUri(
|
const resourceUri = webview.asWebviewUri(
|
||||||
vscode.Uri.joinPath(this._extensionUri, 'webview', 'dist', url)
|
vscode.Uri.joinPath(this._extensionUri, 'webview', 'dist', url)
|
||||||
);
|
);
|
||||||
|
|
||||||
// 为脚本添加nonce
|
// 为脚本添加nonce
|
||||||
if (attribute === 'src' && url.endsWith('.js')) {
|
if (attribute === 'src' && url.endsWith('.js')) {
|
||||||
return `${attribute}="${resourceUri}" nonce="${nonce}"`;
|
return `${attribute}="${resourceUri}" nonce="${nonce}"`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${attribute}="${resourceUri}"`;
|
return `${attribute}="${resourceUri}"`;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
this._logger('WebView HTML内容已生成,包含VSCode API初始化脚本');
|
this._logger('WebView HTML内容已生成,包含VSCode API初始化脚本');
|
||||||
return indexHtml;
|
return indexHtml;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -355,7 +375,7 @@ export class ChatViewProvider implements vscode.WebviewViewProvider {
|
|||||||
return this._getFallbackHtml(webview, nonce);
|
return this._getFallbackHtml(webview, nonce);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取回退HTML内容
|
// 获取回退HTML内容
|
||||||
private _getFallbackHtml(webview: vscode.Webview, nonce: string) {
|
private _getFallbackHtml(webview: vscode.Webview, nonce: string) {
|
||||||
// 开发环境下使用占位符HTML
|
// 开发环境下使用占位符HTML
|
||||||
@ -402,7 +422,7 @@ export class ChatViewProvider implements vscode.WebviewViewProvider {
|
|||||||
</html>
|
</html>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成随机nonce
|
// 生成随机nonce
|
||||||
private _getNonce() {
|
private _getNonce() {
|
||||||
let text = '';
|
let text = '';
|
||||||
@ -412,4 +432,118 @@ export class ChatViewProvider implements vscode.WebviewViewProvider {
|
|||||||
}
|
}
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
wsConnect(message: ListenerParam, webview: WebviewViewIns): void | false {
|
||||||
|
switch (message.type) {
|
||||||
|
case 'ws:connect':
|
||||||
|
// 建立 WebSocket 连接
|
||||||
|
this._wsInstance = new WS('ws://47.117.75.243:8080/ws', {
|
||||||
|
headers: { 'X-Api-Key': 'simpletest2025_094' }
|
||||||
|
})
|
||||||
|
// 连接打开
|
||||||
|
this._wsInstance.on('open', () => {
|
||||||
|
webview.postMessage({ type: 'ws:open', data: true })
|
||||||
|
})
|
||||||
|
// 转发服务器消息到 Webview
|
||||||
|
this._wsInstance.on('message', (data) => {
|
||||||
|
this._logger('ws:message ===== ' + data.toString())
|
||||||
|
webview.postMessage({
|
||||||
|
type: 'ws:message',
|
||||||
|
data: data
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 错误处理
|
||||||
|
this._wsInstance.on('error', (err) => {
|
||||||
|
webview.postMessage({
|
||||||
|
type: 'ws:error',
|
||||||
|
data: '连接失败: ' + err.message
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 连接关闭处理
|
||||||
|
this._wsInstance.on('close', () => {
|
||||||
|
webview.postMessage({ type: 'ws:close' })
|
||||||
|
})
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'ws:send':
|
||||||
|
// 转发 Webview 的消息到服务器
|
||||||
|
if (this._wsInstance) {
|
||||||
|
this._logger('ws:send' + message.data)
|
||||||
|
this._wsInstance.send(message.data)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'ws:disconnect':
|
||||||
|
if (this._wsInstance) {
|
||||||
|
this._wsInstance.close()
|
||||||
|
this._wsInstance = null
|
||||||
|
}
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
findFunction(symbols: vscode.DocumentSymbol[], name: string): vscode.DocumentSymbol | false {
|
||||||
|
for (const symbol of symbols) {
|
||||||
|
if (symbol.kind === vscode.SymbolKind.Function && symbol.name === name) {
|
||||||
|
return symbol;
|
||||||
|
}
|
||||||
|
if (symbol.children) {
|
||||||
|
const found = this.findFunction(symbol.children, name);
|
||||||
|
if (found) return found;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
async openFileAndGoToFunction(message: ListenerParam): Promise<void | false> {
|
||||||
|
if (message.type !== 'openFileAndGoToFunction') { return false };
|
||||||
|
const filePath = message.filePath; // 确保路径正确,可以是绝对路径或相对于工作区
|
||||||
|
const targetFunc = message.functionName
|
||||||
|
if (!filePath || !targetFunc){
|
||||||
|
vscode.window.showWarningMessage(`未提供文件: ${filePath} 或函数: ${targetFunc}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// 打开文件
|
||||||
|
const uri = vscode.Uri.file(filePath)
|
||||||
|
const doc = await vscode.workspace.openTextDocument(uri)
|
||||||
|
const editor = await vscode.window.showTextDocument(doc)
|
||||||
|
|
||||||
|
// 获取文档符号
|
||||||
|
const symbols = await vscode.commands.executeCommand<vscode.DocumentSymbol[]>(
|
||||||
|
'vscode.executeDocumentSymbolProvider',
|
||||||
|
doc.uri
|
||||||
|
)
|
||||||
|
|
||||||
|
if (symbols) {
|
||||||
|
const funcSymbol = this.findFunction(symbols, targetFunc)
|
||||||
|
if (funcSymbol) {
|
||||||
|
// 跳转到目标位置
|
||||||
|
editor.revealRange(funcSymbol.range, vscode.TextEditorRevealType.InCenter)
|
||||||
|
editor.selection = new vscode.Selection(funcSymbol.range.start, funcSymbol.range.end)
|
||||||
|
} else {
|
||||||
|
vscode.window.showWarningMessage(`未找到函数 ${targetFunc}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
vscode.window.showErrorMessage(`无法打开文件: ${error}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 获取本地markdown文件
|
||||||
|
fetchMdFile(message: ListenerParam, webview: WebviewViewIns): false | void {
|
||||||
|
if (message.type !== 'loadMd') { return false }
|
||||||
|
if (message.mdPath) {
|
||||||
|
const filePath = path.join(this._extensionUri.fsPath, 'webview', 'dist', message.mdPath!.replace('__localWebViewPath/', ''))
|
||||||
|
try {
|
||||||
|
const content = fs.readFileSync(filePath, 'utf-8')
|
||||||
|
webview.postMessage({ type: 'mdContent', data: content })
|
||||||
|
} catch (error) {
|
||||||
|
this._logger('获取 md 内容失败' + error)
|
||||||
|
webview.postMessage({ type: 'mdContent', data: '获取 md 内容失败' })
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
webview.postMessage({ type: 'mdContent', data: '未正确传入md文件路径' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
17
webview/package-lock.json
generated
17
webview/package-lock.json
generated
@ -16,6 +16,8 @@
|
|||||||
"axios": "^1.6.0",
|
"axios": "^1.6.0",
|
||||||
"bufferutil": "^4.0.9",
|
"bufferutil": "^4.0.9",
|
||||||
"dompurify": "^3.2.4",
|
"dompurify": "^3.2.4",
|
||||||
|
"highlight.js": "^11.11.1",
|
||||||
|
"highlightjs-line-numbers.js": "^2.9.0",
|
||||||
"marked": "^15.0.7",
|
"marked": "^15.0.7",
|
||||||
"pinia": "^2.1.0",
|
"pinia": "^2.1.0",
|
||||||
"utf-8-validate": "^6.0.5",
|
"utf-8-validate": "^6.0.5",
|
||||||
@ -1776,6 +1778,21 @@
|
|||||||
"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/highlightjs-line-numbers.js": {
|
||||||
|
"version": "2.9.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/highlightjs-line-numbers.js/-/highlightjs-line-numbers.js-2.9.0.tgz",
|
||||||
|
"integrity": "sha512-hMYK5VU+Qi0HmkkdZxamV71ALu9Hq2icQk2WP8OX5q7IPMilSv47ILlJu+fBvxAQdhjW6wONnSQeypsbeRM7WQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/iconv-lite": {
|
"node_modules/iconv-lite": {
|
||||||
"version": "0.6.3",
|
"version": "0.6.3",
|
||||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||||
|
@ -18,6 +18,8 @@
|
|||||||
"axios": "^1.6.0",
|
"axios": "^1.6.0",
|
||||||
"bufferutil": "^4.0.9",
|
"bufferutil": "^4.0.9",
|
||||||
"dompurify": "^3.2.4",
|
"dompurify": "^3.2.4",
|
||||||
|
"highlight.js": "^11.11.1",
|
||||||
|
"highlightjs-line-numbers.js": "^2.9.0",
|
||||||
"marked": "^15.0.7",
|
"marked": "^15.0.7",
|
||||||
"pinia": "^2.1.0",
|
"pinia": "^2.1.0",
|
||||||
"utf-8-validate": "^6.0.5",
|
"utf-8-validate": "^6.0.5",
|
||||||
|
40
webview/public/ruanjian.md
Normal file
40
webview/public/ruanjian.md
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
# 软件功能方案
|
||||||
|
|
||||||
|
## 一、项目概述
|
||||||
|
|
||||||
|
### (一)项目背景
|
||||||
|
在当今数字化时代,数据可视化已成为企业和个人进行数据分析、决策支持以及信息展示的重要手段。本项目旨在满足市场对于高效、便捷、美观的数据可视化工具的需求,进一步拓展和优化 VISSLM BI 的功能,为用户提供更多样化、专业化的数据可视化解决方案。
|
||||||
|
|
||||||
|
增强 VISSLM BI 的数据可视化能力、交互性、易用性以及拓展性,使其能够更好地适应不同行业、不同规模用户的多样化数据可视化需求,提升用户在数据处理、分析和展示过程中的效率和体验,为用户提供具体而有价值的数据可视化工具。
|
||||||
|
- **图表库丰富**
|
||||||
|
- 基础图库:支持 G2Plot 原生图表和 ECharts 自定义图表,提供 50 + 标准组件,涵盖柱状图、折线图、饼图、雷达图、散点图等多种常见图表类型,满足用户在不同场景下的数据展示需求。
|
||||||
|
- **快捷键:**
|
||||||
|
- 支持 win 系统的快捷键操作,提高用户操作效率,例如快速复制、粘贴、删除组件,以及切换不同的编辑模式等功能。
|
||||||
|
- **蓝图事件**
|
||||||
|
- 可视化蓝图联动:提供可视化蓝图功能,用户可以通过节点连线的方式,快速配置组件之间的联动效果。例如,当某个图表的数据发生变化时,可以自动触发其他相关图表的更新或显示特定的提示信息,实现数据可视化元素之间的高效交互,增强数据展示的动态性和连贯性。
|
||||||
|
- **快速主题与滤镜**
|
||||||
|
- 全局主题与滤镜:支持全局主题设置,用户可以一键切换不同的主题风格,包括颜色、字体、背景等元素,快速调整数据可视化的整体视觉效果。同时,提供滤镜功能,用户可以对图表进行美化处理,如添加阴影、渐变色等效果,提升数据可视化的美观度
|
||||||
|
### (二)项目目标
|
||||||
|
增强 VISSLM BI 的数据可视化能力、交互性、易用性以及拓展性,使其能够更好地适应不同行业、不同规模用户的多样化数据可视化需求,提升用户在数据处理、分析和展示过程中的效率和体验,为用户提供具体而有价值的数据可视化工具。
|
||||||
|
- **图表库丰富**
|
||||||
|
- 基础图库:支持 G2Plot 原生图表和 ECharts 自定义图表,提供 50 + 标准组件,涵盖柱状图、折线图、饼图、雷达图、散点图等多种常见图表类型,满足用户在不同场景下的数据展示需求。
|
||||||
|
- **快捷键:**
|
||||||
|
- 支持 win 系统的快捷键操作,提高用户操作效率,例如快速复制、粘贴、删除组件,以及切换不同的编辑模式等功能。
|
||||||
|
- **蓝图事件**
|
||||||
|
- 可视化蓝图联动:提供可视化蓝图功能,用户可以通过节点连线的方式,快速配置组件之间的联动效果。例如,当某个图表的数据发生变化时,可以自动触发其他相关图表的更新或显示特定的提示信息,实现数据可视化元素之间的高效交互,增强数据展示的动态性和连贯性。
|
||||||
|
- **快速主题与滤镜**
|
||||||
|
- 全局主题与滤镜:支持全局主题设置,用户可以一键切换不同的主题风格,包括颜色、字体、背景等元素,快速调整数据可视化的整体视觉效果。同时,提供滤镜功能,用户可以对图表进行美化处理,如添加阴影、渐变色等效果,提升数据可视化的美观度
|
||||||
|
|
||||||
|
## 二、功能说明
|
||||||
|
|
||||||
|
### (一)数据可视化设计
|
||||||
|
- **图表库丰富**
|
||||||
|
- 基础图库:支持 G2Plot 原生图表和 ECharts 自定义图表,提供 50 + 标准组件,涵盖柱状图、折线图、饼图、雷达图、散点图等多种常见图表类型,满足用户在不同场景下的数据展示需求。
|
||||||
|
- **快捷键:**
|
||||||
|
- 支持 win 系统的快捷键操作,提高用户操作效率,例如快速复制、粘贴、删除组件,以及切换不同的编辑模式等功能。
|
||||||
|
- **蓝图事件**
|
||||||
|
- 可视化蓝图联动:提供可视化蓝图功能,用户可以通过节点连线的方式,快速配置组件之间的联动效果。例如,当某个图表的数据发生变化时,可以自动触发其他相关图表的更新或显示特定的提示信息,实现数据可视化元素之间的高效交互,增强数据展示的动态性和连贯性。
|
||||||
|
- **快速主题与滤镜**
|
||||||
|
- 全局主题与滤镜:支持全局主题设置,用户可以一键切换不同的主题风格,包括颜色、字体、背景等元素,快速调整数据可视化的整体视觉效果。同时,提供滤镜功能,用户可以对图表进行美化处理,如添加阴影、渐变色等效果,提升数据可视化的美观度。
|
||||||
|
|
||||||
|

|
@ -8,7 +8,8 @@
|
|||||||
<a-tab-pane key="chat" tab="CHAT"></a-tab-pane>
|
<a-tab-pane key="chat" tab="CHAT"></a-tab-pane>
|
||||||
<a-tab-pane key="examples" tab="用例"></a-tab-pane>
|
<a-tab-pane key="examples" tab="用例"></a-tab-pane>
|
||||||
<a-tab-pane key="flow" tab="流程图"></a-tab-pane>
|
<a-tab-pane key="flow" tab="流程图"></a-tab-pane>
|
||||||
<a-tab-pane key="docs" tab="文档"></a-tab-pane>
|
<a-tab-pane key="docCode" tab="文档"></a-tab-pane>
|
||||||
|
<a-tab-pane key="docs" tab="帮助"></a-tab-pane>
|
||||||
</a-tabs>
|
</a-tabs>
|
||||||
<div :class="$style.actions">
|
<div :class="$style.actions">
|
||||||
<a-button type="text" @click="openSettings">
|
<a-button type="text" @click="openSettings">
|
||||||
@ -19,7 +20,7 @@
|
|||||||
<component :is="currentComponent" />
|
<component :is="currentComponent" />
|
||||||
</a-layout-content>
|
</a-layout-content>
|
||||||
</a-layout>
|
</a-layout>
|
||||||
|
|
||||||
<!-- 设置模态框 -->
|
<!-- 设置模态框 -->
|
||||||
<SettingsModal v-model:visible="showSettingsModal" />
|
<SettingsModal v-model:visible="showSettingsModal" />
|
||||||
</div>
|
</div>
|
||||||
@ -35,6 +36,7 @@ import FlowPanel from './components/FlowPanel.vue';
|
|||||||
import DocsPanel from './components/DocsPanel.vue';
|
import DocsPanel from './components/DocsPanel.vue';
|
||||||
import SettingsModal from './components/SettingsModal.vue';
|
import SettingsModal from './components/SettingsModal.vue';
|
||||||
import { useThemeStore } from './store/themeStore';
|
import { useThemeStore } from './store/themeStore';
|
||||||
|
import DocCodePanel from './components/DocCodePanel.vue';
|
||||||
|
|
||||||
// 当前选中的选项卡
|
// 当前选中的选项卡
|
||||||
const activeKey = ref('chat');
|
const activeKey = ref('chat');
|
||||||
@ -47,6 +49,7 @@ const componentMap = {
|
|||||||
chat: ChatPanel,
|
chat: ChatPanel,
|
||||||
examples: ExamplesPanel,
|
examples: ExamplesPanel,
|
||||||
flow: FlowPanel,
|
flow: FlowPanel,
|
||||||
|
docCode: DocCodePanel,
|
||||||
docs: DocsPanel
|
docs: DocsPanel
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -71,7 +74,7 @@ const themeStore = useThemeStore();
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// 请求主题颜色
|
// 请求主题颜色
|
||||||
themeStore.initialize();
|
themeStore.initialize();
|
||||||
|
|
||||||
// 监听来自VSCode的消息
|
// 监听来自VSCode的消息
|
||||||
window.addEventListener('message', event => {
|
window.addEventListener('message', event => {
|
||||||
const message = event.data;
|
const message = event.data;
|
||||||
@ -119,14 +122,14 @@ onMounted(() => {
|
|||||||
.actions {
|
.actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
|
|
||||||
button {
|
button {
|
||||||
color: var(--vscode-description-foreground);
|
color: var(--vscode-description-foreground);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: var(--vscode-foreground);
|
color: var(--vscode-foreground);
|
||||||
background-color: var(--vscode-list-hover-background);
|
background-color: var(--vscode-list-hover-background);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
64
webview/src/components/DocCodePanel.vue
Normal file
64
webview/src/components/DocCodePanel.vue
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
<template>
|
||||||
|
<div :class="$style.docCodePanel">
|
||||||
|
<Header :class="$style.header" title="文档" />
|
||||||
|
<div :class="$style.content">
|
||||||
|
<MarkdownViewer :file-path="filePath"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import Header from './Header.vue';
|
||||||
|
import MarkdownViewer from './MarkdownViewer.vue';
|
||||||
|
// 输入框的值
|
||||||
|
const filePath = ref('__localWebViewPath/ruanjian.md')
|
||||||
|
if(!window.acquireVsCodeApi){
|
||||||
|
filePath.value = '/ruanjian.md'
|
||||||
|
}
|
||||||
|
if(window.vscodeApi){
|
||||||
|
window.vscodeApi.postMessage({
|
||||||
|
type: 'ws:connect',
|
||||||
|
})
|
||||||
|
window.addEventListener('message', event => {
|
||||||
|
if (event.data.type === 'ws:open' && window.vscodeApi) {
|
||||||
|
window.vscodeApi.postMessage({
|
||||||
|
type: 'log',
|
||||||
|
message: 'ws连接成功'
|
||||||
|
})
|
||||||
|
let selected_text = {
|
||||||
|
"filepath": "example\\\\game.py", // 文件路径
|
||||||
|
"range": {
|
||||||
|
"start": { "line": 33, "character": 0 }, // 从33行0字符开始
|
||||||
|
"end": { "line": 45, "character": 41 } // 到45行41字符结束
|
||||||
|
},
|
||||||
|
"text": `def change_direction(new_direction): \n\n\tglobal direction \n\n\tif new_direction == 'left': \n\t\tif direction != 'right': \n\t\t\tdirection = new_direction \n\telif new_direction == 'right': \n\t\tif direction != 'left': \n\t\t\tdirection = new_direction \n\telif new_direction == 'up': \n\t\tif direction != 'down': \n\t\t\tdirection = new_direction \n\telif new_direction == 'down': \n\t\tif direction != 'up': \n\t\t\tdirection = new_direction` // 源码片段
|
||||||
|
}
|
||||||
|
|
||||||
|
let req = {
|
||||||
|
"cmd": "exec_docstring",
|
||||||
|
"request_id": 123, // 随机生成 (必填)
|
||||||
|
"model": "local qwen2.5-coder:7b", //(必填)
|
||||||
|
"stream": false,
|
||||||
|
"selected_text": selected_text,
|
||||||
|
}
|
||||||
|
window.vscodeApi.postMessage({
|
||||||
|
type: 'ws:send',
|
||||||
|
data: JSON.stringify(req)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style module lang="scss">
|
||||||
|
.docCodePanel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
background-color: var(--vscode-editor-background);
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
</style>
|
56
webview/src/components/MDOutlineTree.vue
Normal file
56
webview/src/components/MDOutlineTree.vue
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
<template>
|
||||||
|
<Tree :tree-data="treeData" :class="$style.tree" :expandedKeys="expandedKeys" :selectedKeys="selectedKeys" :blockNode="true"
|
||||||
|
@expand="handleExpand" @select="handleSelect" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
import { Tree } from 'ant-design-vue';
|
||||||
|
|
||||||
|
interface TreeNode {
|
||||||
|
key: number;
|
||||||
|
level: number;
|
||||||
|
title: string;
|
||||||
|
anchor: number;
|
||||||
|
children?: TreeNode[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
data: TreeNode[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// 状态管理
|
||||||
|
const expandedKeys = ref<number[]>([]);
|
||||||
|
const selectedKeys = ref<number[]>([]);
|
||||||
|
|
||||||
|
// 转换为树形结构
|
||||||
|
const treeData = computed(() => {
|
||||||
|
const result: TreeNode[] = props.data;
|
||||||
|
// 默认展开一级节点
|
||||||
|
expandedKeys.value = result.map(n => n.key);
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 节点展开/折叠
|
||||||
|
const handleExpand: any = (keys: number[]) => {
|
||||||
|
expandedKeys.value = keys;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 节点选择
|
||||||
|
const handleSelect: any = (keys: number[], { node }: { node: { dataRef: TreeNode } }) => {
|
||||||
|
selectedKeys.value = keys;
|
||||||
|
// 锚点跳转逻辑
|
||||||
|
// window.location.hash = `#${node.dataRef.key}`;
|
||||||
|
let element = document.querySelector(`[id="${node.dataRef.key}"]`)
|
||||||
|
element?.scrollIntoView({
|
||||||
|
behavior: 'smooth',
|
||||||
|
block: 'center'
|
||||||
|
})
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style module lang="scss">
|
||||||
|
|
||||||
|
.tree {
|
||||||
|
background-color: var(--vscode-editor-background);
|
||||||
|
}
|
||||||
|
</style>
|
439
webview/src/components/MarkdownViewer.vue
Normal file
439
webview/src/components/MarkdownViewer.vue
Normal file
@ -0,0 +1,439 @@
|
|||||||
|
<template>
|
||||||
|
<div class="markdown-viewer">
|
||||||
|
|
||||||
|
<!-- 目录侧边栏 -->
|
||||||
|
<aside class="toc-sidebar" ref="tocSidebar">
|
||||||
|
<div class="toc-header">
|
||||||
|
<h2>目录</h2>
|
||||||
|
</div>
|
||||||
|
<div class="toc-content">
|
||||||
|
<MDOutlineTree :data="tocTreeData"/>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- 主内容区域 -->
|
||||||
|
<main class="content" ref="content">
|
||||||
|
<div v-if="loading" class="loading-indicator">加载中...</div>
|
||||||
|
<div v-else-if="error" class="error-message">{{ error }}</div>
|
||||||
|
<div v-else v-html="renderedContent" class="markdown-content"></div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, onUnmounted, nextTick, onBeforeMount, unref } from 'vue';
|
||||||
|
import { marked } from 'marked';
|
||||||
|
import { throttle } from 'lodash-es';
|
||||||
|
import MDOutlineTree from './MDOutlineTree.vue'
|
||||||
|
let hljs;
|
||||||
|
const hljsPms = import('highlight.js').then(res => {
|
||||||
|
hljs = window.hljs = res.default;
|
||||||
|
return import('highlightjs-line-numbers.js');
|
||||||
|
}).then(() => {
|
||||||
|
hljs.initLineNumbersOnLoad();
|
||||||
|
return hljs;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
filePath: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
debounceTime: {
|
||||||
|
type: Number,
|
||||||
|
default: 100
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const loading = ref(true);
|
||||||
|
const error = ref(null);
|
||||||
|
const markdownText = ref('');
|
||||||
|
const renderedContent = ref('');
|
||||||
|
const toc = ref([]);
|
||||||
|
const activeSection = ref('');
|
||||||
|
const content = ref(null);
|
||||||
|
const tocSidebar = ref(null);
|
||||||
|
const tocTreeData = ref([])
|
||||||
|
// 配置marked
|
||||||
|
const renderer = new marked.Renderer();
|
||||||
|
renderer.code = (code, language) => {
|
||||||
|
const highlighted = hljs.highlightAuto(code).value;
|
||||||
|
return `
|
||||||
|
<div class="code-block">
|
||||||
|
<pre class="hljs"><code>${highlighted}</code></pre>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
marked.setOptions({ renderer });
|
||||||
|
// 渲染h头
|
||||||
|
renderer.heading = ({ text, depth }) => {
|
||||||
|
const escapedText = toc.value.length
|
||||||
|
// text.toLowerCase().replace(/[^\w]+/g, '-');
|
||||||
|
toc.value.push({
|
||||||
|
id: escapedText,
|
||||||
|
level: depth,
|
||||||
|
title: text,
|
||||||
|
anchor: escapedText
|
||||||
|
})
|
||||||
|
const node = {
|
||||||
|
key: escapedText,
|
||||||
|
title: text
|
||||||
|
}
|
||||||
|
let parent;
|
||||||
|
let currParent;
|
||||||
|
let data;
|
||||||
|
let loopData = data = unref(tocTreeData.value);
|
||||||
|
let d = depth;
|
||||||
|
while (d >= 1) {
|
||||||
|
if (d === 1) {
|
||||||
|
loopData.push(node);
|
||||||
|
}
|
||||||
|
parent = loopData.at(-1);
|
||||||
|
if (!parent) {
|
||||||
|
loopData.push(parent = { key: Math.rendom(), children: [] });
|
||||||
|
}
|
||||||
|
loopData = parent.children = parent.children ?? [];
|
||||||
|
d--;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `
|
||||||
|
<h${depth} id="${escapedText}">
|
||||||
|
<a name="${escapedText}" class="anchor" href="#${escapedText}">
|
||||||
|
<span class="header-link"></span>
|
||||||
|
</a>
|
||||||
|
${text}
|
||||||
|
</h${depth}>`;
|
||||||
|
};
|
||||||
|
let renderList = renderer.listitem
|
||||||
|
|
||||||
|
renderer.listitem = (src)=>{
|
||||||
|
return renderList.call(renderer, src);
|
||||||
|
}
|
||||||
|
marked.setOptions({
|
||||||
|
renderer,
|
||||||
|
highlight: (code, lang) => {
|
||||||
|
if (lang && hljs.getLanguage(lang)) {
|
||||||
|
return hljs.highlight(lang, code).value;
|
||||||
|
}
|
||||||
|
return hljs.highlightAuto(code).value;
|
||||||
|
},
|
||||||
|
pedantic: false,
|
||||||
|
gfm: true,
|
||||||
|
breaks: false,
|
||||||
|
sanitize: false,
|
||||||
|
smartLists: true,
|
||||||
|
smartypants: false,
|
||||||
|
xhtml: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// 加载Markdown文件
|
||||||
|
const loadMarkdownFile = async () => {
|
||||||
|
try {
|
||||||
|
const filePath = props.filePath
|
||||||
|
loading.value = true;
|
||||||
|
error.value = null;
|
||||||
|
let fetchMdPms
|
||||||
|
|
||||||
|
if(filePath.startsWith('__localWebViewPath/')){
|
||||||
|
if(!window.vscodeApi){
|
||||||
|
console.error('获取__localWebViewPath的md文档时, 无法获取VSCode API')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
window.vscodeApi.postMessage({
|
||||||
|
type: 'log',
|
||||||
|
message: '[MarkdownViewer] 获取本地markdown...' + filePath
|
||||||
|
});
|
||||||
|
window.vscodeApi.postMessage({
|
||||||
|
type: 'loadMd',
|
||||||
|
mdPath: filePath
|
||||||
|
});
|
||||||
|
window.addEventListener('message', event => {
|
||||||
|
if (event.data.type === 'mdContent') {
|
||||||
|
window.vscodeApi.postMessage({
|
||||||
|
type: 'log',
|
||||||
|
message: '[MarkdownViewer] 获取本地markdown成功'
|
||||||
|
});
|
||||||
|
markdownText.value = event.data.data
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}else{
|
||||||
|
fetchMdPms = fetch(props.filePath);
|
||||||
|
const response = await fetchMdPms
|
||||||
|
if (!response.ok) throw new Error('无法加载Markdown文件');
|
||||||
|
markdownText.value = await response.text();
|
||||||
|
}
|
||||||
|
await hljsPms;
|
||||||
|
// 初始化高亮和行号插件
|
||||||
|
hljs.highlightAll();
|
||||||
|
renderMarkdown();
|
||||||
|
} catch (err) {
|
||||||
|
error.value = err.message;
|
||||||
|
console.error('加载Markdown文件失败:', err);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 渲染Markdown
|
||||||
|
const renderMarkdown = () => {
|
||||||
|
console.log(markdownText.value);
|
||||||
|
|
||||||
|
toc.value = []; // 清空目录
|
||||||
|
renderedContent.value = marked(markdownText.value);
|
||||||
|
// 等待DOM更新后初始化高亮
|
||||||
|
nextTick(() => {
|
||||||
|
document.querySelectorAll('pre code').forEach(block => {
|
||||||
|
hljs.highlightElement(block);
|
||||||
|
});
|
||||||
|
// setupHeadingObservers();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 设置IntersectionObserver来检测当前可见的标题
|
||||||
|
let observer = null;
|
||||||
|
const setupHeadingObservers = () => {
|
||||||
|
// 先清理之前的observer
|
||||||
|
if (observer) {
|
||||||
|
observer.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6');
|
||||||
|
observer = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
if (entry.isIntersecting && entry.intersectionRatio >= 0.5) {
|
||||||
|
activeSection.value = entry.target.id;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{
|
||||||
|
root: null,
|
||||||
|
rootMargin: '0px 0px -50% 0px',
|
||||||
|
threshold: [0, 0.5, 1]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
headings.forEach(heading => {
|
||||||
|
observer.observe(heading);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 滚动到指定标题
|
||||||
|
const scrollToHeading = (anchor) => {
|
||||||
|
const element = document.getElementById(anchor);
|
||||||
|
if (element) {
|
||||||
|
|
||||||
|
// 平滑滚动
|
||||||
|
element.scrollIntoView({
|
||||||
|
behavior: 'smooth',
|
||||||
|
block: 'start'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新URL哈希而不触发滚动
|
||||||
|
history.replaceState(null, null, `#${anchor}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// 处理窗口大小变化
|
||||||
|
const handleResize = throttle(() => {
|
||||||
|
if (window.innerWidth >= 768) {
|
||||||
|
}
|
||||||
|
}, props.debounceTime);
|
||||||
|
|
||||||
|
// 生命周期钩子
|
||||||
|
onBeforeMount(()=>{
|
||||||
|
loadMarkdownFile();
|
||||||
|
})
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('resize', handleResize);
|
||||||
|
if (observer) {
|
||||||
|
observer.disconnect();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 行号容器样式 */
|
||||||
|
.hljs-ln-numbers {
|
||||||
|
text-align: center;
|
||||||
|
border-right: 1px solid;
|
||||||
|
/* 右侧分割线 */
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 代码块容器 */
|
||||||
|
.code-block {
|
||||||
|
position: relative;
|
||||||
|
padding-left: 3em;
|
||||||
|
/* 留出行号空间 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 行号与代码对齐 */
|
||||||
|
.hljs-ln td:first-child {
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-viewer {
|
||||||
|
display: flex;
|
||||||
|
/* min-height: 100vh; */
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-sidebar {
|
||||||
|
width: min(30vw, 280px);
|
||||||
|
height: calc(100vh - 100px);
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-right: 1px solid;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-header {
|
||||||
|
padding-bottom: 15px;
|
||||||
|
border-bottom: 1px solid;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
.toc-content {
|
||||||
|
height: calc(100vh - 200px);
|
||||||
|
}
|
||||||
|
.toc-content ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-content li {
|
||||||
|
margin: 6px 0;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-content a {
|
||||||
|
display: block;
|
||||||
|
padding: 4px 8px;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-content a.active {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
flex: 1;
|
||||||
|
height: calc(100vh - 100px);
|
||||||
|
padding: 16px;
|
||||||
|
overflow-y: auto;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-indicator,
|
||||||
|
.error-message {
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* Markdown内容样式 */
|
||||||
|
.markdown-content {
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content :deep(h1),
|
||||||
|
.markdown-content :deep(h2),
|
||||||
|
.markdown-content :deep(h3),
|
||||||
|
.markdown-content :deep(h4),
|
||||||
|
.markdown-content :deep(h5),
|
||||||
|
.markdown-content :deep(h6) {
|
||||||
|
margin-top: 1.5em;
|
||||||
|
margin-bottom: 0.8em;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content :deep(h1) {
|
||||||
|
font-size: 2em;
|
||||||
|
padding-bottom: 0.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content :deep(h2) {
|
||||||
|
font-size: 1.5em;
|
||||||
|
padding-bottom: 0.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content :deep(.anchor) {
|
||||||
|
position: absolute;
|
||||||
|
left: -20px;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content :deep(h1:hover .anchor),
|
||||||
|
.markdown-content :deep(h2:hover .anchor),
|
||||||
|
.markdown-content :deep(h3:hover .anchor),
|
||||||
|
.markdown-content :deep(h4:hover .anchor),
|
||||||
|
.markdown-content :deep(h5:hover .anchor),
|
||||||
|
.markdown-content :deep(h6:hover .anchor) {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端样式 */
|
||||||
|
@media (max-width: 400px) {
|
||||||
|
.mobile-menu-button {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-sidebar {
|
||||||
|
position: fixed;
|
||||||
|
top: 100px;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
transform: translateX(-100%);
|
||||||
|
width: 30%;
|
||||||
|
max-width: 300px;
|
||||||
|
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-viewer.mobile-menu-open .toc-sidebar {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 20px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 代码块样式 */
|
||||||
|
.markdown-content :deep(pre) {
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 16px;
|
||||||
|
overflow: auto;
|
||||||
|
line-height: 1.45;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content :deep(code) {
|
||||||
|
font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace;
|
||||||
|
font-size: 85%;
|
||||||
|
}
|
||||||
|
</style>
|
@ -4,7 +4,9 @@ import { resolve } from 'path';
|
|||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [vue()],
|
plugins: [vue({
|
||||||
|
include: [/\.vue$/, /\.md$/] // <-- allows Vue to compile Markdown files
|
||||||
|
})],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@': resolve(__dirname, 'src'),
|
'@': resolve(__dirname, 'src'),
|
||||||
@ -28,4 +30,4 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
Loading…
x
Reference in New Issue
Block a user