From 76c084c38c0789d7cc3dd352620f3c061f64c3c1 Mon Sep 17 00:00:00 2001 From: "jiajun.song" Date: Sat, 26 Apr 2025 13:15:30 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=96=87=E6=A1=A3tab?= =?UTF-8?q?=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/extension/ChatViewProvider.ts | 234 +++++++++--- webview/package-lock.json | 17 + webview/package.json | 2 + webview/public/ruanjian.md | 40 ++ webview/src/App.vue | 15 +- webview/src/components/DocCodePanel.vue | 64 ++++ webview/src/components/MDOutlineTree.vue | 56 +++ webview/src/components/MarkdownViewer.vue | 439 ++++++++++++++++++++++ webview/vite.config.ts | 6 +- 9 files changed, 815 insertions(+), 58 deletions(-) create mode 100644 webview/public/ruanjian.md create mode 100644 webview/src/components/DocCodePanel.vue create mode 100644 webview/src/components/MDOutlineTree.vue create mode 100644 webview/src/components/MarkdownViewer.vue diff --git a/src/extension/ChatViewProvider.ts b/src/extension/ChatViewProvider.ts index 0706829..e176d05 100644 --- a/src/extension/ChatViewProvider.ts +++ b/src/extension/ChatViewProvider.ts @@ -1,13 +1,23 @@ import * as vscode from 'vscode'; import * as path from 'path'; 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 { public static readonly viewType = 'aiChatView'; - + private _view?: vscode.WebviewView; private _logger: (message: string) => void; - + private _wsInstance?: WS | null constructor( private readonly _extensionUri: vscode.Uri, logger?: (message: string) => void @@ -30,21 +40,31 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { this._extensionUri ] }; - webviewView.webview.html = this._getHtmlForWebview(webviewView.webview); + const webview = webviewView.webview + // 在生产环境中,使用打包后的Webview文件 this._logger('WebView HTML内容已设置'); - + let wsInstance = null // 处理来自Webview的消息 webviewView.webview.onDidReceiveMessage(message => { this._logger(`收到WebView消息: ${message.type}`); - + if (message.type?.startsWith('ws:')) { + this.wsConnect(message, webview) + return + } switch (message.type) { + case 'loadMd': + this.fetchMdFile(message, webview) + break + case 'openFileAndGoToFunction': + this.openFileAndGoToFunction(message) + break case 'getSettings': this._logger(`处理getSettings请求`); this._sendSettings(); break; case 'saveSettings': - this._logger(`保存设置: ${JSON.stringify(message.settings, (key, value) => + this._logger(`保存设置: ${JSON.stringify(message.settings, (key, value) => key === 'apiKey' ? '***' : value)}`); this._saveSettings(message.settings); break; @@ -60,12 +80,12 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { this._logger(`⚙️ SettingsModal日志: ${message.message}`); } // 为WebSocket连接相关日志添加更详细的输出 - else if (message.message.includes('WebSocket') || - message.message.includes('ChatStore') || + else if (message.message.includes('WebSocket') || + message.message.includes('ChatStore') || message.message.includes('initWebSocketService') ) { this._logger(`🔌 WebSocket日志: ${message.message}`); - } + } // 为重要日志添加特殊标记 else if (message.message.includes('【重要】')) { this._logger(`⚠️ ${message.message}`); @@ -85,10 +105,10 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { break; } }); - + // 初始化时发送主题颜色 this._sendThemeColors(); - + // 监听主题变化 vscode.window.onDidChangeActiveColorTheme(() => { this._sendThemeColors(); @@ -98,45 +118,45 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { // 发送主题颜色 private _sendThemeColors() { if (!this._view) return; - + // 获取当前主题颜色 const theme = vscode.window.activeColorTheme; const isDark = theme.kind === vscode.ColorThemeKind.Dark; const isLight = theme.kind === vscode.ColorThemeKind.Light; - + // 获取常用颜色 const colors = { // 基础颜色 foreground: this._getColor('foreground'), background: this._getColor('editor.background'), - + // 文本颜色 descriptionForeground: this._getColor('descriptionForeground'), errorForeground: this._getColor('errorForeground'), - + // 按钮颜色 buttonBackground: this._getColor('button.background'), buttonForeground: this._getColor('button.foreground'), buttonHoverBackground: this._getColor('button.hoverBackground'), - + // 输入框颜色 inputBackground: this._getColor('input.background'), inputForeground: this._getColor('input.foreground'), inputPlaceholderForeground: this._getColor('input.placeholderForeground'), inputBorder: this._getColor('input.border'), - + // 链接颜色 linkForeground: this._getColor('textLink.foreground'), linkActiveForeground: this._getColor('textLink.activeForeground'), - + // 其他界面元素 panelBorder: this._getColor('panel.border'), - + // 主题类型 isDark, isLight }; - + this._view.webview.postMessage({ type: 'themeColors', colors @@ -162,17 +182,17 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { 'textLink.activeForeground': '#3794ff', 'panel.border': '#80808059' }; - + try { // 尝试从配置中获取颜色 const themeOverride = vscode.workspace.getConfiguration('workbench') .get>('colorCustomizations', {}); - + // 首先尝试从用户配置的覆盖颜色中获取 if (themeOverride[colorId]) { return themeOverride[colorId]; } - + // 然后从默认颜色中获取 return defaultColors[colorId] || '#cccccc'; } catch (error) { @@ -185,13 +205,13 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { // 发送设置 private _sendSettings() { if (!this._view) return; - + const config = vscode.workspace.getConfiguration('aiChat'); const settings = { apiHost: config.get('apiHost', ''), apiKey: config.get('apiKey', '') }; - + this._view.webview.postMessage({ type: 'settings', settings @@ -201,23 +221,23 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { // 保存设置 private _saveSettings(settings: { apiHost: string; apiKey: string }) { if (!settings) return; - + this._logger(`准备保存设置: apiHost=${settings.apiHost}, apiKey=***`); - + const config = vscode.workspace.getConfiguration('aiChat'); let hasChanges = false; - + // 保存成功标志 let success = true; let errorMessage = ''; - + // 检查并更新API主机 const updateHost = async () => { if (settings.apiHost === config.get('apiHost')) return; - + hasChanges = true; this._logger('更新API主机设置'); - + try { await config.update('apiHost', settings.apiHost, vscode.ConfigurationTarget.Global); this._logger('已更新API主机设置'); @@ -227,14 +247,14 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { this._logger(`更新API主机设置失败: ${errorMessage}`); } }; - + // 检查并更新API密钥 const updateKey = async () => { if (settings.apiKey === config.get('apiKey')) return; - + hasChanges = true; this._logger('更新API密钥设置'); - + try { await config.update('apiKey', settings.apiKey, vscode.ConfigurationTarget.Global); this._logger('已更新API密钥设置'); @@ -244,12 +264,12 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { this._logger(`更新API密钥设置失败: ${errorMessage}`); } }; - + // 执行所有更新操作 const updateAll = async () => { await updateHost(); await updateKey(); - + // 通知前端设置已保存 if (this._view) { if (success) { @@ -268,11 +288,11 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { } } }; - + // 启动更新过程 updateAll().catch(error => { this._logger(`保存设置过程中发生未捕获异常: ${error}`); - + // 确保前端收到失败消息 if (this._view) { this._view.webview.postMessage({ @@ -288,10 +308,10 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { private _getHtmlForWebview(webview: vscode.Webview) { // 在生产环境中,使用打包后的Webview文件 const distPath = path.join(this._extensionUri.fsPath, 'webview', 'dist'); - + // 生成随机nonce以增强安全性 const nonce = this._getNonce(); - + // 检查dist目录是否存在 if (fs.existsSync(distPath)) { try { @@ -300,7 +320,7 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { path.join(distPath, 'index.html'), 'utf-8' ); - + // 添加CSP策略和nonce if (!indexHtml.includes('content-security-policy')) { indexHtml = indexHtml.replace( @@ -309,7 +329,7 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { ` ); } - + // 确保有VSCode API脚本 if (!indexHtml.includes('acquireVsCodeApi')) { indexHtml = indexHtml.replace( @@ -321,7 +341,7 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { ` ); } - + // 将资源路径替换为Webview可访问的URI indexHtml = indexHtml.replace( /(href|src)="([^"]*)"/g, @@ -330,21 +350,21 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { if (url.startsWith('http') || url.startsWith('data:')) { return `${attribute}="${url}"`; } - + // 转换为Webview URI const resourceUri = webview.asWebviewUri( vscode.Uri.joinPath(this._extensionUri, 'webview', 'dist', url) ); - + // 为脚本添加nonce if (attribute === 'src' && url.endsWith('.js')) { return `${attribute}="${resourceUri}" nonce="${nonce}"`; } - + return `${attribute}="${resourceUri}"`; } ); - + this._logger('WebView HTML内容已生成,包含VSCode API初始化脚本'); return indexHtml; } catch (error) { @@ -355,7 +375,7 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { return this._getFallbackHtml(webview, nonce); } } - + // 获取回退HTML内容 private _getFallbackHtml(webview: vscode.Webview, nonce: string) { // 开发环境下使用占位符HTML @@ -402,7 +422,7 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { `; } - + // 生成随机nonce private _getNonce() { let text = ''; @@ -412,4 +432,118 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { } return text; } -} \ No newline at end of file + + 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 { + 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.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文件路径' }) + } +} diff --git a/webview/package-lock.json b/webview/package-lock.json index 0286c1a..9567ad5 100644 --- a/webview/package-lock.json +++ b/webview/package-lock.json @@ -16,6 +16,8 @@ "axios": "^1.6.0", "bufferutil": "^4.0.9", "dompurify": "^3.2.4", + "highlight.js": "^11.11.1", + "highlightjs-line-numbers.js": "^2.9.0", "marked": "^15.0.7", "pinia": "^2.1.0", "utf-8-validate": "^6.0.5", @@ -1776,6 +1778,21 @@ "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": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", diff --git a/webview/package.json b/webview/package.json index 723af7d..6c093d7 100644 --- a/webview/package.json +++ b/webview/package.json @@ -18,6 +18,8 @@ "axios": "^1.6.0", "bufferutil": "^4.0.9", "dompurify": "^3.2.4", + "highlight.js": "^11.11.1", + "highlightjs-line-numbers.js": "^2.9.0", "marked": "^15.0.7", "pinia": "^2.1.0", "utf-8-validate": "^6.0.5", diff --git a/webview/public/ruanjian.md b/webview/public/ruanjian.md new file mode 100644 index 0000000..21396de --- /dev/null +++ b/webview/public/ruanjian.md @@ -0,0 +1,40 @@ +# 软件功能方案 + +## 一、项目概述 + +### (一)项目背景 +在当今数字化时代,数据可视化已成为企业和个人进行数据分析、决策支持以及信息展示的重要手段。本项目旨在满足市场对于高效、便捷、美观的数据可视化工具的需求,进一步拓展和优化 VISSLM BI 的功能,为用户提供更多样化、专业化的数据可视化解决方案。 + +增强 VISSLM BI 的数据可视化能力、交互性、易用性以及拓展性,使其能够更好地适应不同行业、不同规模用户的多样化数据可视化需求,提升用户在数据处理、分析和展示过程中的效率和体验,为用户提供具体而有价值的数据可视化工具。 +- **图表库丰富** + - 基础图库:支持 G2Plot 原生图表和 ECharts 自定义图表,提供 50 + 标准组件,涵盖柱状图、折线图、饼图、雷达图、散点图等多种常见图表类型,满足用户在不同场景下的数据展示需求。 +- **快捷键:** + - 支持 win 系统的快捷键操作,提高用户操作效率,例如快速复制、粘贴、删除组件,以及切换不同的编辑模式等功能。 +- **蓝图事件** + - 可视化蓝图联动:提供可视化蓝图功能,用户可以通过节点连线的方式,快速配置组件之间的联动效果。例如,当某个图表的数据发生变化时,可以自动触发其他相关图表的更新或显示特定的提示信息,实现数据可视化元素之间的高效交互,增强数据展示的动态性和连贯性。 +- **快速主题与滤镜** + - 全局主题与滤镜:支持全局主题设置,用户可以一键切换不同的主题风格,包括颜色、字体、背景等元素,快速调整数据可视化的整体视觉效果。同时,提供滤镜功能,用户可以对图表进行美化处理,如添加阴影、渐变色等效果,提升数据可视化的美观度 +### (二)项目目标 +增强 VISSLM BI 的数据可视化能力、交互性、易用性以及拓展性,使其能够更好地适应不同行业、不同规模用户的多样化数据可视化需求,提升用户在数据处理、分析和展示过程中的效率和体验,为用户提供具体而有价值的数据可视化工具。 +- **图表库丰富** + - 基础图库:支持 G2Plot 原生图表和 ECharts 自定义图表,提供 50 + 标准组件,涵盖柱状图、折线图、饼图、雷达图、散点图等多种常见图表类型,满足用户在不同场景下的数据展示需求。 +- **快捷键:** + - 支持 win 系统的快捷键操作,提高用户操作效率,例如快速复制、粘贴、删除组件,以及切换不同的编辑模式等功能。 +- **蓝图事件** + - 可视化蓝图联动:提供可视化蓝图功能,用户可以通过节点连线的方式,快速配置组件之间的联动效果。例如,当某个图表的数据发生变化时,可以自动触发其他相关图表的更新或显示特定的提示信息,实现数据可视化元素之间的高效交互,增强数据展示的动态性和连贯性。 +- **快速主题与滤镜** + - 全局主题与滤镜:支持全局主题设置,用户可以一键切换不同的主题风格,包括颜色、字体、背景等元素,快速调整数据可视化的整体视觉效果。同时,提供滤镜功能,用户可以对图表进行美化处理,如添加阴影、渐变色等效果,提升数据可视化的美观度 + +## 二、功能说明 + +### (一)数据可视化设计 +- **图表库丰富** + - 基础图库:支持 G2Plot 原生图表和 ECharts 自定义图表,提供 50 + 标准组件,涵盖柱状图、折线图、饼图、雷达图、散点图等多种常见图表类型,满足用户在不同场景下的数据展示需求。 +- **快捷键:** + - 支持 win 系统的快捷键操作,提高用户操作效率,例如快速复制、粘贴、删除组件,以及切换不同的编辑模式等功能。 +- **蓝图事件** + - 可视化蓝图联动:提供可视化蓝图功能,用户可以通过节点连线的方式,快速配置组件之间的联动效果。例如,当某个图表的数据发生变化时,可以自动触发其他相关图表的更新或显示特定的提示信息,实现数据可视化元素之间的高效交互,增强数据展示的动态性和连贯性。 +- **快速主题与滤镜** + - 全局主题与滤镜:支持全局主题设置,用户可以一键切换不同的主题风格,包括颜色、字体、背景等元素,快速调整数据可视化的整体视觉效果。同时,提供滤镜功能,用户可以对图表进行美化处理,如添加阴影、渐变色等效果,提升数据可视化的美观度。 + +![image-20250420143847485](./assets/image-20250420143847485.png) \ No newline at end of file diff --git a/webview/src/App.vue b/webview/src/App.vue index 5109ff5..8613917 100644 --- a/webview/src/App.vue +++ b/webview/src/App.vue @@ -8,7 +8,8 @@ - + +
@@ -19,7 +20,7 @@ - +
@@ -35,6 +36,7 @@ import FlowPanel from './components/FlowPanel.vue'; import DocsPanel from './components/DocsPanel.vue'; import SettingsModal from './components/SettingsModal.vue'; import { useThemeStore } from './store/themeStore'; +import DocCodePanel from './components/DocCodePanel.vue'; // 当前选中的选项卡 const activeKey = ref('chat'); @@ -47,6 +49,7 @@ const componentMap = { chat: ChatPanel, examples: ExamplesPanel, flow: FlowPanel, + docCode: DocCodePanel, docs: DocsPanel }; @@ -71,7 +74,7 @@ const themeStore = useThemeStore(); onMounted(() => { // 请求主题颜色 themeStore.initialize(); - + // 监听来自VSCode的消息 window.addEventListener('message', event => { const message = event.data; @@ -119,14 +122,14 @@ onMounted(() => { .actions { display: flex; margin-right: 8px; - + button { color: var(--vscode-description-foreground); - + &:hover { color: var(--vscode-foreground); background-color: var(--vscode-list-hover-background); } } } - \ No newline at end of file + \ No newline at end of file diff --git a/webview/src/components/DocCodePanel.vue b/webview/src/components/DocCodePanel.vue new file mode 100644 index 0000000..a4b3466 --- /dev/null +++ b/webview/src/components/DocCodePanel.vue @@ -0,0 +1,64 @@ + + + + + \ No newline at end of file diff --git a/webview/src/components/MDOutlineTree.vue b/webview/src/components/MDOutlineTree.vue new file mode 100644 index 0000000..886cdfb --- /dev/null +++ b/webview/src/components/MDOutlineTree.vue @@ -0,0 +1,56 @@ + + + + \ No newline at end of file diff --git a/webview/src/components/MarkdownViewer.vue b/webview/src/components/MarkdownViewer.vue new file mode 100644 index 0000000..059e9f7 --- /dev/null +++ b/webview/src/components/MarkdownViewer.vue @@ -0,0 +1,439 @@ + + + + + \ No newline at end of file diff --git a/webview/vite.config.ts b/webview/vite.config.ts index d97f2a3..f10cb39 100644 --- a/webview/vite.config.ts +++ b/webview/vite.config.ts @@ -4,7 +4,9 @@ import { resolve } from 'path'; // https://vitejs.dev/config/ export default defineConfig({ - plugins: [vue()], + plugins: [vue({ + include: [/\.vue$/, /\.md$/] // <-- allows Vue to compile Markdown files + })], resolve: { alias: { '@': resolve(__dirname, 'src'), @@ -28,4 +30,4 @@ export default defineConfig({ }, }, }, -}); \ No newline at end of file +}); \ No newline at end of file