Compare commits

...

3 Commits
main ... main

13 changed files with 1086 additions and 63 deletions

BIN
doc/image-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

BIN
doc/image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

154
doc/文档tab页.md Normal file
View File

@ -0,0 +1,154 @@
# 文档页面开发记录
## 文档页面实现功能
1. 文档分页获取文件为markdown格式
代码示例
```html
<!-- webview\src\components\DocCodePanel.vue -->
<template>
<MarkdownViewer :file-path="filePath"/>
</template>
<script>
// 文档路径
// const filePath = ref('__localWebViewPath/ruanjian.md')
const filePath = ref('__localWorkspacePath/ruanjian.md')
</script>
```
- 文件支持的路径及修改方式
1. 支持插件自身目录
filePath的值以 `__localWebViewPath/` 开头
2. 支持工作区目录
filePath的值以 `__localWorkspacePath/` 开头
2. 布局:
![alt 布局](image.png)
- 左侧为标题目录
1. 支持折叠展开
1. 支持点击时 右侧文档滚动到对应位置
- 右侧为文档主题
1. 支持展示图片
1. 支持特殊标记实现点击时打开工作区内文件中的某个函数
目前暂定格式与示例\
示例:
[打开vscode中的src/test.js:testFunction](__workspace/src/test.js?functionName=resolveTripleslashReference)
格式:
[显示文字](__workspace/[文件的相对路径]?functionName=[函数名])
关键代码:
``` js
///webview\src\components\MarkdownViewer.vue
/// ...
const renderLink = renderer.link
const handleDelegateClick = (event) => {
// 通过事件冒泡捕获目标元素
const target = event.target.closest('.openFileAndGoToFunction')
if (target) {
const { filepath, functionname } = target.dataset
console.log(filepath, functionname, '_**=== filepath, functionname');
console.log(target.dataset, '_**=== target.dataset');
if (window.acquireVsCodeApi && window.vscodeApi) {
window.vscodeApi.postMessage({
type: 'openFileAndGoToFunction',
filePath: filepath,
functionName: functionname
});
}
}
}
renderer.link = function (src) {
// 工作区跳转
if (src.href.startsWith("__workspace/")) {
const { href, title, tokens } = src
const text = renderer.parser.parseInline(tokens);
// const cleanHref = cleanUrl(href);
const titleTip = href.slice("__workspace/".length);
const [filePath, functionName] = titleTip.split('?functionName=')
console.log(filePath, functionName, '_**=== filePath, functionName');
return `<a href="javascript:void 0" class="openFileAndGoToFunction" title="点击打开关联文件函数: ${titleTip}" data-href="${href}" data-filepath="${filePath}" data-functionname="${functionName}">${text}</a>`
}
return renderLink.call(renderer, src);
}
```
```typescript
// src\extension\ChatViewProvider.ts
async openFileAndGoToFunction(message: ListenerParam): Promise<void | false> {
if (message.type !== 'openFileAndGoToFunction') { return false };
const rootUri = vscode.workspace.workspaceFolders?.[0]?.uri;
const filePath = message.filePath; // 确保路径正确,相对于工作区
this._logger(rootUri + '#####' + filePath)
const targetFunc = message.functionName
if (!filePath || !targetFunc) {
vscode.window.showWarningMessage(`未提供文件: ${filePath} 或函数: ${targetFunc}`)
return
}
try {
// 打开文件
const docInfo = await this.openWorkspaceFile(filePath);
if (!docInfo) {
vscode.window.showWarningMessage(`无法打开或创建文件: ${filePath}`)
return
}
const { editor, document } = docInfo
// 获取文档符号
const symbols = await vscode.commands.executeCommand<vscode.DocumentSymbol[]>(
'vscode.executeDocumentSymbolProvider',
document.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}`)
}
}
// 打开工作区的文件
async openWorkspaceFile(relativePath: string) {
// 1. 获取工作区根路径
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri;
if (!workspaceRoot) {
vscode.window.showErrorMessage(" 未检测到工作区");
return;
}
// 2. 拼接完整路径(自动处理跨平台路径)
const targetUri = vscode.Uri.joinPath(workspaceRoot, relativePath);
// 3. 文件存在性检测
try {
await vscode.workspace.fs.stat(targetUri);
} catch {
const createNew = await vscode.window.showInformationMessage(
"文件不存在,是否创建?",
"创建", "取消"
);
if (createNew === "创建") {
await vscode.workspace.fs.writeFile(targetUri, Buffer.from(""));
} else {
return;
}
}
this._logger(targetUri + ':::targetUri')
// 4. 打开文件编辑器
const document = await vscode.workspace.openTextDocument(targetUri);
const editor = await vscode.window.showTextDocument(document);
return { document, editor }
}
```
1. 插件信息与修改: 若需要改为标题或列表, 修改 renderer 的对应方法以及触发跳转, 渲染的元素添加类名
[marked使用文档](https://marked.nodejs.cn/using_pro#hooks)
![alt text](image-1.png)

View File

@ -9,7 +9,7 @@ function log(message: string) {
if (!outputChannel) {
outputChannel = vscode.window.createOutputChannel('AI Chat Plugin');
}
const timestamp = new Date().toISOString();
outputChannel.appendLine(`[${timestamp}] ${message}`);
}
@ -18,7 +18,7 @@ export function activate(context: vscode.ExtensionContext) {
// 创建输出通道
outputChannel = vscode.window.createOutputChannel('AI Chat Plugin');
outputChannel.show(true);
log('VSCode AI Chat Plugin 已激活');
// 注册Webview视图提供程序
@ -37,7 +37,7 @@ export function activate(context: vscode.ExtensionContext) {
vscode.commands.executeCommand('workbench.view.extension.aiChatView');
})
);
// 监听配置变更
context.subscriptions.push(
vscode.workspace.onDidChangeConfiguration(e => {
@ -50,8 +50,8 @@ export function activate(context: vscode.ExtensionContext) {
export function deactivate() {
log('VSCode AI Chat Plugin 已停用');
if (outputChannel) {
outputChannel.dispose();
}
}
}

View File

@ -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<Record<string, string>>('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 {
<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脚本
if (!indexHtml.includes('acquireVsCodeApi')) {
indexHtml = indexHtml.replace(
@ -321,7 +341,7 @@ export class ChatViewProvider implements vscode.WebviewViewProvider {
</script>`
);
}
// 将资源路径替换为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 {
</html>
`;
}
// 生成随机nonce
private _getNonce() {
let text = '';
@ -412,4 +432,170 @@ export class ChatViewProvider implements vscode.WebviewViewProvider {
}
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 rootUri = vscode.workspace.workspaceFolders?.[0]?.uri;
const filePath = message.filePath; // 确保路径正确,相对于工作区
this._logger(rootUri + '#####' + filePath)
const targetFunc = message.functionName
if (!filePath || !targetFunc) {
vscode.window.showWarningMessage(`未提供文件: ${filePath} 或函数: ${targetFunc}`)
return
}
try {
// 打开文件
const docInfo = await this.openWorkspaceFile(filePath);
if (!docInfo) {
vscode.window.showWarningMessage(`无法打开或创建文件: ${filePath}`)
return
}
const { editor, document } = docInfo
// 获取文档符号
const symbols = await vscode.commands.executeCommand<vscode.DocumentSymbol[]>(
'vscode.executeDocumentSymbolProvider',
document.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) {
let filePath
const isFromWorkspace = message.mdPath.startsWith('__localWorkspacePath/')
if (isFromWorkspace) {
// 1. 获取工作区根路径
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri;
if (!workspaceRoot) {
vscode.window.showErrorMessage(" 未检测到工作区");
return;
}
// 2. 拼接完整路径(自动处理跨平台路径)
filePath = vscode.Uri.joinPath(workspaceRoot, message.mdPath.replace('__localWorkspacePath/', '')).fsPath;
// filePath = path.join(workspaceRoot.toString(), message.mdPath.replace('__localWorkspacePath/', ''))
} else {
filePath = path.join(this._extensionUri.fsPath, 'webview', 'dist', message.mdPath.replace('__localWebViewPath/', ''))
}
this._logger(filePath + '::: 当前md路径')
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文件路径' })
}
// 打开工作区的文件
async openWorkspaceFile(relativePath: string) {
// 1. 获取工作区根路径
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri;
if (!workspaceRoot) {
vscode.window.showErrorMessage(" 未检测到工作区");
return;
}
// 2. 拼接完整路径(自动处理跨平台路径)
const targetUri = vscode.Uri.joinPath(workspaceRoot, relativePath);
// 3. 文件存在性检测
try {
await vscode.workspace.fs.stat(targetUri);
} catch {
const createNew = await vscode.window.showInformationMessage(
"文件不存在,是否创建?",
"创建", "取消"
);
if (createNew === "创建") {
await vscode.workspace.fs.writeFile(targetUri, Buffer.from(""));
} else {
return;
}
}
this._logger(targetUri + ':::targetUri')
// 4. 打开文件编辑器
const document = await vscode.workspace.openTextDocument(targetUri);
const editor = await vscode.window.showTextDocument(document);
return { document, editor }
}
}

View File

@ -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",

View File

@ -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",

View File

@ -0,0 +1,45 @@
# 软件功能方案
## 一、项目概述
### (一)项目背景
在当今数字化时代,数据可视化已成为企业和个人进行数据分析、决策支持以及信息展示的重要手段。本项目旨在满足市场对于高效、便捷、美观的数据可视化工具的需求,进一步拓展和优化 VISSLM BI 的功能,为用户提供更多样化、专业化的数据可视化解决方案。
增强 VISSLM BI 的数据可视化能力、交互性、易用性以及拓展性,使其能够更好地适应不同行业、不同规模用户的多样化数据可视化需求,提升用户在数据处理、分析和展示过程中的效率和体验,为用户提供具体而有价值的数据可视化工具。
- **图表库丰富**
- 基础图库:支持 G2Plot 原生图表和 ECharts 自定义图表,提供 50 + 标准组件,涵盖柱状图、折线图、饼图、雷达图、散点图等多种常见图表类型,满足用户在不同场景下的数据展示需求。
- **快捷键:**
- 支持 win 系统的快捷键操作,提高用户操作效率,例如快速复制、粘贴、删除组件,以及切换不同的编辑模式等功能。
- **蓝图事件**
- 可视化蓝图联动:提供可视化蓝图功能,用户可以通过节点连线的方式,快速配置组件之间的联动效果。例如,当某个图表的数据发生变化时,可以自动触发其他相关图表的更新或显示特定的提示信息,实现数据可视化元素之间的高效交互,增强数据展示的动态性和连贯性。
- **快速主题与滤镜**
- 全局主题与滤镜:支持全局主题设置,用户可以一键切换不同的主题风格,包括颜色、字体、背景等元素,快速调整数据可视化的整体视觉效果。同时,提供滤镜功能,用户可以对图表进行美化处理,如添加阴影、渐变色等效果,提升数据可视化的美观度
### (二)项目目标
增强 VISSLM BI 的数据可视化能力、交互性、易用性以及拓展性,使其能够更好地适应不同行业、不同规模用户的多样化数据可视化需求,提升用户在数据处理、分析和展示过程中的效率和体验,为用户提供具体而有价值的数据可视化工具。
- **图表库丰富**
- 基础图库:支持 G2Plot 原生图表和 ECharts 自定义图表,提供 50 + 标准组件,涵盖柱状图、折线图、饼图、雷达图、散点图等多种常见图表类型,满足用户在不同场景下的数据展示需求。
- **快捷键:**
- 支持 win 系统的快捷键操作,提高用户操作效率,例如快速复制、粘贴、删除组件,以及切换不同的编辑模式等功能。
- **蓝图事件**
- 可视化蓝图联动:提供可视化蓝图功能,用户可以通过节点连线的方式,快速配置组件之间的联动效果。例如,当某个图表的数据发生变化时,可以自动触发其他相关图表的更新或显示特定的提示信息,实现数据可视化元素之间的高效交互,增强数据展示的动态性和连贯性。
- **快速主题与滤镜**
- 全局主题与滤镜:支持全局主题设置,用户可以一键切换不同的主题风格,包括颜色、字体、背景等元素,快速调整数据可视化的整体视觉效果。同时,提供滤镜功能,用户可以对图表进行美化处理,如添加阴影、渐变色等效果,提升数据可视化的美观度
## 二、功能说明
### (一)数据可视化设计
- **图表库丰富**
- 基础图库:支持 G2Plot 原生图表和 ECharts 自定义图表,提供 50 + 标准组件,涵盖柱状图、折线图、饼图、雷达图、散点图等多种常见图表类型,满足用户在不同场景下的数据展示需求。
- **快捷键:**
- 支持 win 系统的快捷键操作,提高用户操作效率,例如快速复制、粘贴、删除组件,以及切换不同的编辑模式等功能。
- **蓝图事件**
- 可视化蓝图联动:提供可视化蓝图功能,用户可以通过节点连线的方式,快速配置组件之间的联动效果。例如,当某个图表的数据发生变化时,可以自动触发其他相关图表的更新或显示特定的提示信息,实现数据可视化元素之间的高效交互,增强数据展示的动态性和连贯性。
- **快速主题与滤镜**
- 全局主题与滤镜:支持全局主题设置,用户可以一键切换不同的主题风格,包括颜色、字体、背景等元素,快速调整数据可视化的整体视觉效果。同时,提供滤镜功能,用户可以对图表进行美化处理,如添加阴影、渐变色等效果,提升数据可视化的美观度。
![image](https://img-s.msn.cn/tenant/amp/entityid/AA1DBjm3.img?w=768&h=512&m=6)
示例:
[打开vscode中的src/test.js:testFunction](__workspace/src/test.js?functionName=resolveTripleslashReference)
格式:
[显示文字](__workspace/[文件的相对路径]?functionName=[函数名])

View File

@ -8,7 +8,8 @@
<a-tab-pane key="chat" tab="CHAT"></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="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>
<div :class="$style.actions">
<a-button type="text" @click="openSettings">
@ -19,7 +20,7 @@
<component :is="currentComponent" />
</a-layout-content>
</a-layout>
<!-- 设置模态框 -->
<SettingsModal v-model:visible="showSettingsModal" />
</div>
@ -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);
}
}
}
</style>
</style>

View File

@ -0,0 +1,69 @@
<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')
const filePath = ref('__localWorkspacePath/ruanjian.md')
// if(!window.acquireVsCodeApi){
// filePath.value = '/ruanjian.md'
// }
/*
//#region wsdoc
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 }, // 330
"end": { "line": 45, "character": 41 } // 4541
},
"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)
})
}
})
}
//#endregion
*/
</script>
<style module lang="scss">
.docCodePanel {
display: flex;
flex-direction: column;
height: 100%;
background-color: var(--vscode-editor-background);
}
.header {
flex-shrink: 0;
}
</style>

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

View File

@ -0,0 +1,489 @@
<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" @click="handleDelegateClick">
<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}>`;
};
//
const renderList = renderer.listitem
// ,
renderer.listitem = (src) => {
return renderList.call(renderer, src);
}
const handleDelegateClick = (event) => {
//
const target = event.target.closest('.openFileAndGoToFunction')
if (target) {
const { filepath, functionname } = target.dataset
console.log(filepath, functionname, '_**=== filepath, functionname');
console.log(target.dataset, '_**=== target.dataset');
if (window.acquireVsCodeApi && window.vscodeApi) {
window.vscodeApi.postMessage({
type: 'openFileAndGoToFunction',
filePath: filepath,
functionName: functionname
});
}
}
}
const renderLink = renderer.link
renderer.link = function (src) {
/*
{
"type": "link",
"raw": "[打开vscode中的文件1](__workspace/)",
"href": "__workspace/",
"title": null,
"text": "打开vscode中的文件1",
"tokens": [
{
"type": "text",
"raw": "打开vscode中的文件1",
"text": "打开vscode中的文件1",
"escaped": false
}
]
}
*/
//
if (src.href.startsWith("__workspace/")) {
const { href, title, tokens } = src
const text = renderer.parser.parseInline(tokens);
// const cleanHref = cleanUrl(href);
const titleTip = href.slice("__workspace/".length);
const [filePath, functionName] = titleTip.split('?functionName=')
console.log(filePath, functionName, '_**=== filePath, functionName');
return `<a href="javascript:void 0" class="openFileAndGoToFunction" title="点击打开关联文件函数: ${titleTip}" data-href="${href}" data-filepath="${filePath}" data-functionname="${functionName}">${text}</a>`
}
return renderLink.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/') || filePath.startsWith('__localWorkspacePath/')) {
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 = () => {
toc.value = []; //
renderedContent.value = marked(markdownText.value);
// DOM
nextTick(() => {
document.querySelectorAll('pre code').forEach(block => {
hljs.highlightElement(block);
});
// , todo
// 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>

View File

@ -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({
},
},
},
});
});