js演练场

This commit is contained in:
q
2025-11-29 02:56:25 +08:00
parent 2e76af980e
commit df646b8c43
14 changed files with 3670 additions and 0 deletions

View File

@@ -0,0 +1,196 @@
<template>
<div ref="editorContainer" class="monaco-editor-container"></div>
</template>
<script>
import { ref, onMounted, onBeforeUnmount, watch } from 'vue';
export default {
name: 'MonacoEditor',
props: {
modelValue: {
type: String,
default: ''
},
language: {
type: String,
default: 'javascript'
},
theme: {
type: String,
default: 'vs'
},
options: {
type: Object,
default: () => ({})
},
height: {
type: String,
default: '500px'
}
},
emits: ['update:modelValue', 'change'],
setup(props, { emit }) {
const editorContainer = ref(null);
let editor = null;
let monaco = null;
const defaultOptions = {
value: props.modelValue,
language: props.language,
theme: props.theme,
automaticLayout: true,
fontSize: 14,
minimap: {
enabled: true
},
scrollBeyondLastLine: false,
wordWrap: 'on',
lineNumbers: 'on',
roundedSelection: false,
readOnly: false,
cursorStyle: 'line',
formatOnPaste: true,
formatOnType: true,
tabSize: 2,
insertSpaces: true,
...props.options
};
const initEditor = async () => {
try {
if (!editorContainer.value) {
console.error('编辑器容器未找到');
return;
}
// 动态导入monaco-editor loader
let loaderModule;
try {
loaderModule = await import('@monaco-editor/loader');
} catch (importError) {
console.error('导入@monaco-editor/loader失败:', importError);
return;
}
// 获取loader对象
// @monaco-editor/loader可能使用default导出或named导出
let loader;
if (loaderModule.default) {
loader = loaderModule.default;
} else if (loaderModule.loader) {
loader = loaderModule.loader;
} else {
loader = loaderModule;
}
if (!loader) {
console.error('Monaco Editor loader未找到loaderModule:', loaderModule);
return;
}
if (typeof loader.init !== 'function') {
console.error('loader.init不是函数loader对象:', loader);
return;
}
// 初始化Monaco Editor
monaco = await loader.init();
if (!monaco) {
console.error('loader.init返回null或undefined');
return;
}
if (!monaco.editor) {
console.error('monaco.editor不存在monaco对象:', monaco);
return;
}
editor = monaco.editor.create(editorContainer.value, {
...defaultOptions,
value: props.modelValue
});
// 监听内容变化
editor.onDidChangeModelContent(() => {
const value = editor.getValue();
emit('update:modelValue', value);
emit('change', value);
});
// 设置容器高度
if (editorContainer.value) {
editorContainer.value.style.height = props.height;
}
} catch (error) {
console.error('Monaco Editor初始化失败:', error);
console.error('错误详情:', error.stack);
console.error('错误对象:', error);
}
};
const updateTheme = (newTheme) => {
if (editor) {
monaco.editor.setTheme(newTheme);
}
};
const formatDocument = () => {
if (editor) {
editor.getAction('editor.action.formatDocument').run();
}
};
watch(() => props.modelValue, (newValue) => {
if (editor && editor.getValue() !== newValue) {
editor.setValue(newValue);
}
});
watch(() => props.theme, (newTheme) => {
updateTheme(newTheme);
});
watch(() => props.height, (newHeight) => {
if (editorContainer.value) {
editorContainer.value.style.height = newHeight;
if (editor) {
editor.layout();
}
}
});
onMounted(() => {
initEditor();
});
onBeforeUnmount(() => {
if (editor) {
editor.dispose();
}
});
return {
editorContainer,
formatDocument,
getEditor: () => editor,
getMonaco: () => monaco
};
}
};
</script>
<style scoped>
.monaco-editor-container {
width: 100%;
border: 1px solid #dcdfe6;
border-radius: 4px;
overflow: hidden;
}
.monaco-editor-container :deep(.monaco-editor) {
border-radius: 4px;
}
</style>

View File

@@ -0,0 +1,359 @@
/**
* Monaco Editor 代码补全配置工具
* 基于 types.js 提供完整的代码补全支持
*/
/**
* 配置Monaco Editor的类型定义和代码补全
* @param {monaco} monaco - Monaco Editor实例
*/
export async function configureMonacoTypes(monaco) {
if (!monaco) {
console.warn('Monaco Editor未初始化');
return;
}
// 注册JavaScript语言特性
monaco.languages.setLanguageConfiguration('javascript', {
comments: {
lineComment: '//',
blockComment: ['/*', '*/']
},
brackets: [
['{', '}'],
['[', ']'],
['(', ')']
],
autoClosingPairs: [
{ open: '{', close: '}' },
{ open: '[', close: ']' },
{ open: '(', close: ')' },
{ open: '"', close: '"' },
{ open: "'", close: "'" }
],
surroundingPairs: [
{ open: '{', close: '}' },
{ open: '[', close: ']' },
{ open: '(', close: ')' },
{ open: '"', close: '"' },
{ open: "'", close: "'" }
]
});
// 注册类型定义
registerTypeDefinitions(monaco);
// 注册代码补全提供者
registerCompletionProvider(monaco);
}
/**
* 注册类型定义
*/
function registerTypeDefinitions(monaco) {
// ShareLinkInfo类型定义
const shareLinkInfoType = `
interface ShareLinkInfo {
getShareUrl(): string;
getShareKey(): string;
getSharePassword(): string;
getType(): string;
getPanName(): string;
getOtherParam(key: string): any;
hasOtherParam(key: string): boolean;
getOtherParamAsString(key: string): string | null;
getOtherParamAsInteger(key: string): number | null;
getOtherParamAsBoolean(key: string): boolean | null;
}
`;
// JsHttpClient类型定义
const httpClientType = `
interface JsHttpClient {
get(url: string): JsHttpResponse;
getWithRedirect(url: string): JsHttpResponse;
getNoRedirect(url: string): JsHttpResponse;
post(url: string, data?: any): JsHttpResponse;
put(url: string, data?: any): JsHttpResponse;
delete(url: string): JsHttpResponse;
patch(url: string, data?: any): JsHttpResponse;
putHeader(name: string, value: string): JsHttpClient;
putHeaders(headers: Record<string, string>): JsHttpClient;
removeHeader(name: string): JsHttpClient;
clearHeaders(): JsHttpClient;
getHeaders(): Record<string, string>;
setTimeout(seconds: number): JsHttpClient;
sendForm(data: Record<string, any>): JsHttpResponse;
sendMultipartForm(url: string, data: Record<string, any>): JsHttpResponse;
sendJson(data: any): JsHttpResponse;
urlEncode(str: string): string;
urlDecode(str: string): string;
}
`;
// JsHttpResponse类型定义
const httpResponseType = `
interface JsHttpResponse {
body(): string;
json(): any;
statusCode(): number;
header(name: string): string | null;
headers(): Record<string, string>;
isSuccess(): boolean;
bodyBytes(): number[];
bodySize(): number;
}
`;
// JsLogger类型定义
const loggerType = `
interface JsLogger {
debug(message: string, ...args: any[]): void;
info(message: string, ...args: any[]): void;
warn(message: string, ...args: any[]): void;
error(message: string, ...args: any[]): void;
isDebugEnabled(): boolean;
isInfoEnabled(): boolean;
isWarnEnabled(): boolean;
isErrorEnabled(): boolean;
}
`;
// FileInfo类型定义
const fileInfoType = `
interface FileInfo {
fileName: string;
fileId: string;
fileType: 'file' | 'folder';
size: number;
sizeStr: string;
createTime: string;
updateTime?: string;
createBy?: string;
downloadCount?: number;
fileIcon?: string;
panType?: string;
parserUrl?: string;
previewUrl?: string;
}
`;
// 合并所有类型定义
const allTypes = `
${shareLinkInfoType}
${httpClientType}
${httpResponseType}
${loggerType}
${fileInfoType}
// 全局变量声明
declare var shareLinkInfo: ShareLinkInfo;
declare var http: JsHttpClient;
declare var logger: JsLogger;
`;
// 注册类型定义到Monaco
monaco.languages.typescript.javascriptDefaults.addExtraLib(
allTypes,
'file:///types.d.ts'
);
}
/**
* 注册代码补全提供者
*/
function registerCompletionProvider(monaco) {
monaco.languages.registerCompletionItemProvider('javascript', {
provideCompletionItems: (model, position) => {
const word = model.getWordUntilPosition(position);
const range = {
startLineNumber: position.lineNumber,
endLineNumber: position.lineNumber,
startColumn: word.startColumn,
endColumn: word.endColumn
};
const suggestions = [
// ShareLinkInfo方法
{
label: 'shareLinkInfo.getShareUrl()',
kind: monaco.languages.CompletionItemKind.Method,
insertText: 'shareLinkInfo.getShareUrl()',
documentation: '获取分享URL',
range
},
{
label: 'shareLinkInfo.getShareKey()',
kind: monaco.languages.CompletionItemKind.Method,
insertText: 'shareLinkInfo.getShareKey()',
documentation: '获取分享Key',
range
},
{
label: 'shareLinkInfo.getSharePassword()',
kind: monaco.languages.CompletionItemKind.Method,
insertText: 'shareLinkInfo.getSharePassword()',
documentation: '获取分享密码',
range
},
{
label: 'shareLinkInfo.getType()',
kind: monaco.languages.CompletionItemKind.Method,
insertText: 'shareLinkInfo.getType()',
documentation: '获取网盘类型',
range
},
{
label: 'shareLinkInfo.getPanName()',
kind: monaco.languages.CompletionItemKind.Method,
insertText: 'shareLinkInfo.getPanName()',
documentation: '获取网盘名称',
range
},
{
label: 'shareLinkInfo.getOtherParam(key)',
kind: monaco.languages.CompletionItemKind.Method,
insertText: 'shareLinkInfo.getOtherParam(${1:key})',
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
documentation: '获取其他参数',
range
},
// JsHttpClient方法
{
label: 'http.get(url)',
kind: monaco.languages.CompletionItemKind.Method,
insertText: 'http.get(${1:url})',
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
documentation: '发起GET请求',
range
},
{
label: 'http.post(url, data)',
kind: monaco.languages.CompletionItemKind.Method,
insertText: 'http.post(${1:url}, ${2:data})',
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
documentation: '发起POST请求',
range
},
{
label: 'http.putHeader(name, value)',
kind: monaco.languages.CompletionItemKind.Method,
insertText: 'http.putHeader(${1:name}, ${2:value})',
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
documentation: '设置请求头',
range
},
{
label: 'http.sendForm(data)',
kind: monaco.languages.CompletionItemKind.Method,
insertText: 'http.sendForm(${1:data})',
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
documentation: '发送表单数据',
range
},
{
label: 'http.sendJson(data)',
kind: monaco.languages.CompletionItemKind.Method,
insertText: 'http.sendJson(${1:data})',
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
documentation: '发送JSON数据',
range
},
// JsLogger方法
{
label: 'logger.info(message)',
kind: monaco.languages.CompletionItemKind.Method,
insertText: 'logger.info(${1:message})',
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
documentation: '记录信息日志',
range
},
{
label: 'logger.debug(message)',
kind: monaco.languages.CompletionItemKind.Method,
insertText: 'logger.debug(${1:message})',
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
documentation: '记录调试日志',
range
},
{
label: 'logger.warn(message)',
kind: monaco.languages.CompletionItemKind.Method,
insertText: 'logger.warn(${1:message})',
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
documentation: '记录警告日志',
range
},
{
label: 'logger.error(message)',
kind: monaco.languages.CompletionItemKind.Method,
insertText: 'logger.error(${1:message})',
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
documentation: '记录错误日志',
range
}
];
return { suggestions };
}
});
}
/**
* 从API获取types.js内容并配置
*/
export async function loadTypesFromApi(monaco) {
try {
// 先尝试从缓存加载
const cacheKey = 'playground_types_js';
const cachedContent = localStorage.getItem(cacheKey);
if (cachedContent) {
try {
monaco.languages.typescript.javascriptDefaults.addExtraLib(
cachedContent,
'file:///types.js'
);
console.log('从缓存加载types.js成功');
// 异步更新缓存
updateTypesJsCache();
return;
} catch (error) {
console.warn('使用缓存的types.js失败重新加载:', error);
localStorage.removeItem(cacheKey);
}
}
// 从API加载
const response = await fetch('/v2/playground/types.js');
if (response.ok) {
const typesJsContent = await response.text();
// 缓存到localStorage
localStorage.setItem(cacheKey, typesJsContent);
// 添加到类型定义中
monaco.languages.typescript.javascriptDefaults.addExtraLib(
typesJsContent,
'file:///types.js'
);
console.log('加载types.js成功并已缓存');
}
} catch (error) {
console.warn('加载types.js失败使用内置类型定义:', error);
}
}
/**
* 异步更新types.js缓存
*/
async function updateTypesJsCache() {
try {
const response = await fetch('/v2/playground/types.js');
if (response.ok) {
const typesJsContent = await response.text();
localStorage.setItem('playground_types_js', typesJsContent);
console.log('types.js缓存已更新');
}
} catch (error) {
console.warn('更新types.js缓存失败:', error);
}
}

View File

@@ -0,0 +1,146 @@
import axios from 'axios';
/**
* 演练场API服务
*/
export const playgroundApi = {
/**
* 测试执行JavaScript代码
* @param {string} jsCode - JavaScript代码
* @param {string} shareUrl - 分享链接
* @param {string} pwd - 密码(可选)
* @param {string} method - 测试方法parse/parseFileList/parseById
* @returns {Promise} 测试结果
*/
async testScript(jsCode, shareUrl, pwd = '', method = 'parse') {
try {
const response = await axios.post('/v2/playground/test', {
jsCode,
shareUrl,
pwd,
method
});
// 框架会自动包装成JsonResult需要从data字段获取
if (response.data && response.data.data) {
return response.data.data;
}
// 如果没有包装,直接返回
return response.data;
} catch (error) {
const errorMsg = error.response?.data?.data?.error ||
error.response?.data?.error ||
error.response?.data?.msg ||
error.message ||
'测试执行失败';
throw new Error(errorMsg);
}
},
/**
* 获取types.js文件内容
* @returns {Promise<string>} types.js内容
*/
async getTypesJs() {
try {
const response = await axios.get('/v2/playground/types.js', {
responseType: 'text'
});
return response.data;
} catch (error) {
throw new Error(error.response?.data?.error || error.message || '获取types.js失败');
}
},
/**
* 获取解析器列表
*/
async getParserList() {
try {
const response = await axios.get('/v2/playground/parsers');
// 框架会自动包装成JsonResult需要从data字段获取
if (response.data && response.data.data) {
return {
code: response.data.code || 200,
data: response.data.data,
msg: response.data.msg,
success: response.data.success
};
}
return response.data;
} catch (error) {
throw new Error(error.response?.data?.error || error.response?.data?.msg || error.message || '获取解析器列表失败');
}
},
/**
* 保存解析器
*/
async saveParser(jsCode) {
try {
const response = await axios.post('/v2/playground/parsers', { jsCode });
// 框架会自动包装成JsonResult
if (response.data && response.data.data) {
return {
code: response.data.code || 200,
data: response.data.data,
msg: response.data.msg,
success: response.data.success
};
}
return response.data;
} catch (error) {
const errorMsg = error.response?.data?.data?.error ||
error.response?.data?.error ||
error.response?.data?.msg ||
error.message ||
'保存解析器失败';
throw new Error(errorMsg);
}
},
/**
* 更新解析器
*/
async updateParser(id, jsCode, enabled = true) {
try {
const response = await axios.put(`/v2/playground/parsers/${id}`, { jsCode, enabled });
return response.data;
} catch (error) {
throw new Error(error.response?.data?.error || error.message || '更新解析器失败');
}
},
/**
* 删除解析器
*/
async deleteParser(id) {
try {
const response = await axios.delete(`/v2/playground/parsers/${id}`);
return response.data;
} catch (error) {
throw new Error(error.response?.data?.error || error.message || '删除解析器失败');
}
},
/**
* 根据ID获取解析器
*/
async getParserById(id) {
try {
const response = await axios.get(`/v2/playground/parsers/${id}`);
// 框架会自动包装成JsonResult
if (response.data && response.data.data) {
return {
code: response.data.code || 200,
data: response.data.data,
msg: response.data.msg,
success: response.data.success
};
}
return response.data;
} catch (error) {
throw new Error(error.response?.data?.error || error.response?.data?.msg || error.message || '获取解析器失败');
}
}
};