不知名提交
This commit is contained in:
136
InfoGenie-frontend/public/toolbox/在线JavaScript执行/app.js
Normal file
136
InfoGenie-frontend/public/toolbox/在线JavaScript执行/app.js
Normal file
@@ -0,0 +1,136 @@
|
||||
(function () {
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const codeEl = $("code");
|
||||
const runBtn = $("runBtn");
|
||||
const copyBtn = $("copyBtn");
|
||||
const pasteBtn = $("pasteBtn");
|
||||
const clearBtn = $("clearBtn");
|
||||
const outputEl = $("output");
|
||||
const sandboxEl = $("sandbox");
|
||||
|
||||
// 以JS方式设置带换行的占位符,避免HTML属性中的 \n 无效
|
||||
codeEl.placeholder = "在此编写或粘贴 JavaScript 代码…\n例如:\nconsole.log('Hello, InfoGenie!');";
|
||||
|
||||
let sandboxReady = false;
|
||||
|
||||
// 沙箱页面(srcdoc)内容:拦截 console、收集错误、支持 async/await
|
||||
const sandboxHtml = `<!doctype html><html><head><meta charset=\"utf-8\"></head><body>
|
||||
<script>
|
||||
(function(){
|
||||
function serialize(value){
|
||||
try {
|
||||
if (typeof value === 'string') return value;
|
||||
if (typeof value === 'function') return value.toString();
|
||||
if (value === undefined) return 'undefined';
|
||||
if (value === null) return 'null';
|
||||
if (typeof value === 'object') return JSON.stringify(value, null, 2);
|
||||
return String(value);
|
||||
} catch (e) {
|
||||
try { return String(value); } catch(_){ return Object.prototype.toString.call(value); }
|
||||
}
|
||||
}
|
||||
|
||||
['log','info','warn','error'].forEach(level => {
|
||||
const orig = console[level];
|
||||
console[level] = (...args) => {
|
||||
try { parent.postMessage({type:'console', level, args: args.map(serialize)}, '*'); } catch(_){ }
|
||||
try { orig && orig.apply(console, args); } catch(_){ }
|
||||
};
|
||||
});
|
||||
|
||||
window.onerror = function(message, source, lineno, colno, error){
|
||||
var stack = error && error.stack ? error.stack : (source + ':' + lineno + ':' + colno);
|
||||
parent.postMessage({type:'error', message: String(message), stack}, '*');
|
||||
};
|
||||
|
||||
parent.postMessage({type:'ready'}, '*');
|
||||
|
||||
window.addEventListener('message', async (e) => {
|
||||
const data = e.data;
|
||||
if (!data || data.type !== 'code') return;
|
||||
try {
|
||||
// 用 async IIFE 包裹,支持顶层 await
|
||||
await (async () => { eval(data.code); })();
|
||||
parent.postMessage({type:'done'}, '*');
|
||||
} catch (err) {
|
||||
parent.postMessage({type:'error', message: (err && err.message) || String(err), stack: err && err.stack}, '*');
|
||||
}
|
||||
}, false);
|
||||
})();
|
||||
<\/script>
|
||||
</body></html>`;
|
||||
|
||||
// 初始化沙箱
|
||||
sandboxEl.srcdoc = sandboxHtml;
|
||||
sandboxEl.addEventListener('load', () => {
|
||||
// 等待 ready 消息
|
||||
});
|
||||
|
||||
window.addEventListener('message', (e) => {
|
||||
const data = e.data;
|
||||
if (!data) return;
|
||||
switch (data.type) {
|
||||
case 'ready':
|
||||
sandboxReady = true;
|
||||
break;
|
||||
case 'console':
|
||||
(data.args || []).forEach((text) => appendLine(text, data.level));
|
||||
break;
|
||||
case 'error':
|
||||
appendLine('[错误] ' + data.message, 'error');
|
||||
if (data.stack) appendLine(String(data.stack), 'error');
|
||||
break;
|
||||
case 'done':
|
||||
tip('执行完成。');
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
runBtn.addEventListener('click', () => {
|
||||
const code = codeEl.value || '';
|
||||
if (!code.trim()) { tip('请先输入要执行的代码。'); return; }
|
||||
outputEl.textContent = '';
|
||||
if (!sandboxReady) tip('沙箱初始化中,稍候执行…');
|
||||
// 发送代码到沙箱执行
|
||||
try {
|
||||
sandboxEl.contentWindow.postMessage({ type: 'code', code }, '*');
|
||||
} catch (err) {
|
||||
appendLine('[错误] ' + ((err && err.message) || String(err)), 'error');
|
||||
}
|
||||
});
|
||||
|
||||
copyBtn.addEventListener('click', async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(codeEl.value);
|
||||
tip('已复制到剪贴板。');
|
||||
} catch (err) {
|
||||
tip('复制失败,请检查剪贴板权限。');
|
||||
}
|
||||
});
|
||||
|
||||
pasteBtn.addEventListener('click', async () => {
|
||||
try {
|
||||
const txt = await navigator.clipboard.readText();
|
||||
if (txt) codeEl.value = txt;
|
||||
tip('已粘贴剪贴板内容。');
|
||||
} catch (err) {
|
||||
tip('粘贴失败,请允许访问剪贴板。');
|
||||
}
|
||||
});
|
||||
|
||||
clearBtn.addEventListener('click', () => {
|
||||
outputEl.textContent = '';
|
||||
});
|
||||
|
||||
function appendLine(text, level) {
|
||||
const span = document.createElement('span');
|
||||
span.className = 'line ' + (level || 'log');
|
||||
span.textContent = String(text);
|
||||
outputEl.appendChild(span);
|
||||
outputEl.appendChild(document.createTextNode('\n'));
|
||||
outputEl.scrollTop = outputEl.scrollHeight;
|
||||
}
|
||||
function tip(text){ appendLine('[提示] ' + text, 'info'); }
|
||||
})();
|
||||
42
InfoGenie-frontend/public/toolbox/在线JavaScript执行/index.html
Normal file
42
InfoGenie-frontend/public/toolbox/在线JavaScript执行/index.html
Normal file
@@ -0,0 +1,42 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover" />
|
||||
<meta name="theme-color" content="#e8f8e4" />
|
||||
<meta name="color-scheme" content="light" />
|
||||
<title>JavaScript在线执行器</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="stylesheet" href="./styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
<main class="page" role="main">
|
||||
<header class="header">
|
||||
<h1 class="title">JavaScript在线执行器</h1>
|
||||
</header>
|
||||
|
||||
<section class="editor-section">
|
||||
<label class="label" for="code">代码编辑区</label>
|
||||
<textarea id="code" class="editor" placeholder="在此编写或粘贴 JavaScript 代码… \n 例如:\n console.log('Hello, InfoGenie!');" spellcheck="false" autocomplete="off" autocapitalize="off" autocorrect="off"></textarea>
|
||||
<div class="toolbar" role="group" aria-label="编辑操作">
|
||||
<button id="pasteBtn" class="btn" type="button" title="从剪贴板粘贴">粘贴代码</button>
|
||||
<button id="copyBtn" class="btn" type="button" title="复制到剪贴板">复制代码</button>
|
||||
<button id="runBtn" class="btn primary" type="button" title="执行当前代码">执行代码</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="output-section">
|
||||
<div class="output-header">
|
||||
<span class="label">结果显示区</span>
|
||||
<button id="clearBtn" class="btn ghost" type="button" title="清空结果">清空结果</button>
|
||||
</div>
|
||||
<pre id="output" class="output" aria-live="polite" aria-atomic="false"></pre>
|
||||
</section>
|
||||
|
||||
<!-- 隐藏的沙箱 iframe,用于安全执行 JS 代码 -->
|
||||
<iframe id="sandbox" class="sandbox" sandbox="allow-scripts" title="执行沙箱" aria-hidden="true"></iframe>
|
||||
</main>
|
||||
|
||||
<script src="./app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
176
InfoGenie-frontend/public/toolbox/在线JavaScript执行/styles.css
Normal file
176
InfoGenie-frontend/public/toolbox/在线JavaScript执行/styles.css
Normal file
@@ -0,0 +1,176 @@
|
||||
/* 全局与主题 */
|
||||
:root {
|
||||
--bg-1: #eaf9e8; /* 淡绿色 */
|
||||
--bg-2: #f4ffd9; /* 淡黄绿色 */
|
||||
--panel: rgba(255, 255, 255, 0.78);
|
||||
--text: #1d2a1d;
|
||||
--muted: #486a48;
|
||||
--accent: #5bb271;
|
||||
--accent-2: #93d18f;
|
||||
--border: rgba(93, 160, 93, 0.25);
|
||||
--code-bg: rgba(255, 255, 255, 0.88);
|
||||
--error: #b00020;
|
||||
--warn: #8a6d3b;
|
||||
--info: #2f6f3a;
|
||||
}
|
||||
|
||||
/* 隐藏滚动条但保留滚动 */
|
||||
html, body {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
-ms-overflow-style: none; /* IE 10+ */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
html::-webkit-scrollbar, body::-webkit-scrollbar { width: 0; height: 0; }
|
||||
|
||||
/* 背景与排版 */
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: linear-gradient(180deg, var(--bg-1), var(--bg-2));
|
||||
color: var(--text);
|
||||
font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
}
|
||||
|
||||
.page {
|
||||
box-sizing: border-box;
|
||||
max-width: 760px;
|
||||
margin: 0 auto;
|
||||
padding: calc(env(safe-area-inset-top, 12px) + 8px) 14px calc(env(safe-area-inset-bottom, 12px) + 14px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: 22px;
|
||||
line-height: 1.2;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
.subtitle {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.editor-section, .output-section {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 10px 20px rgba(64, 129, 64, 0.06);
|
||||
backdrop-filter: saturate(1.2) blur(8px);
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.editor {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-height: 36vh;
|
||||
max-height: 48vh;
|
||||
resize: vertical;
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--border);
|
||||
outline: none;
|
||||
background: var(--code-bg);
|
||||
color: #192519;
|
||||
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
white-space: pre;
|
||||
overflow: auto;
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
.editor::-webkit-scrollbar { width: 0; height: 0; }
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
appearance: none;
|
||||
border: 1px solid var(--border);
|
||||
background: #ffffffd6;
|
||||
color: #204220;
|
||||
padding: 10px 14px;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn:hover { filter: brightness(1.02) saturate(1.02); }
|
||||
.btn:active { transform: translateY(1px); }
|
||||
.btn.primary {
|
||||
background: linear-gradient(180deg, var(--accent-2), var(--accent));
|
||||
color: #fff;
|
||||
border-color: rgba(0,0,0,0.06);
|
||||
}
|
||||
.btn.ghost {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.output-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.output {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-height: 28vh;
|
||||
max-height: 40vh;
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--code-bg);
|
||||
color: #192519;
|
||||
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
overflow: auto;
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
.output::-webkit-scrollbar { width: 0; height: 0; }
|
||||
|
||||
.sandbox { display: none; width: 0; height: 0; border: 0; }
|
||||
|
||||
/* 控制不同日志级别颜色 */
|
||||
.line.log { color: #1f2a1f; }
|
||||
.line.info { color: var(--info); }
|
||||
.line.warn { color: var(--warn); }
|
||||
.line.error { color: var(--error); }
|
||||
.line.tip { color: #507a58; font-style: italic; }
|
||||
|
||||
/* 竖屏优化 */
|
||||
@media (orientation: portrait) {
|
||||
.page { max-width: 640px; }
|
||||
.editor { min-height: 40vh; }
|
||||
.output { min-height: 30vh; }
|
||||
}
|
||||
|
||||
/* 小屏进一步优化 */
|
||||
@media (max-width: 380px) {
|
||||
.btn { padding: 9px 12px; font-size: 13px; }
|
||||
.title { font-size: 20px; }
|
||||
}
|
||||
Reference in New Issue
Block a user