update
This commit is contained in:
150
src/App.vue
150
src/App.vue
@@ -2,15 +2,20 @@
|
||||
import { onBeforeUnmount, onMounted, ref } from "vue"
|
||||
import * as Blockly from "blockly/core"
|
||||
import "blockly/blocks"
|
||||
import Theme from "@blockly/theme-modern"
|
||||
import { setBlocklyLocale } from "./blockly/locale"
|
||||
import { defineBlocks } from "./blockly/blocks"
|
||||
import { toolbox } from "./blockly/toolbox"
|
||||
import { initPythonGenerator } from "./blockly/pythonGenerator"
|
||||
import { runPython } from "./blockly/skulptRunner"
|
||||
|
||||
const workspaceHost = ref<HTMLDivElement | null>(null)
|
||||
const codePreview = ref("")
|
||||
const statusText = ref("准备就绪")
|
||||
const idleStatus = statusText.value
|
||||
const runOutput = ref("")
|
||||
const storageKey = "blockly-workspace"
|
||||
let saveTimer: number | null = null
|
||||
let suppressSave = false
|
||||
|
||||
let workspace: Blockly.WorkspaceSvg | null = null
|
||||
const generator = initPythonGenerator()
|
||||
@@ -21,53 +26,79 @@ const updateCode = () => {
|
||||
codePreview.value = code.trim() || "# 在左侧拖入方块生成Python代码"
|
||||
}
|
||||
|
||||
// const platformTheme = Blockly.Theme.defineTheme('cnPlatform', {
|
||||
// base: Blockly.Themes.Zelos,
|
||||
// componentStyles: {
|
||||
// workspaceBackgroundColour: '#f7f5ef',
|
||||
// toolboxBackgroundColour: '#f2ebe2',
|
||||
// toolboxForegroundColour: '#28343b',
|
||||
// flyoutBackgroundColour: '#fffaf2',
|
||||
// flyoutOpacity: 0.96,
|
||||
// scrollbarColour: '#c3b7aa',
|
||||
// scrollbarOpacity: 0.7,
|
||||
// },
|
||||
// fontStyle: {
|
||||
// family: '"Noto Sans SC", "Source Han Sans SC", "Microsoft YaHei", sans-serif',
|
||||
// size: 13,
|
||||
// weight: 500,
|
||||
// },
|
||||
// blockStyles: {
|
||||
// logic_blocks: { colourPrimary: '#4f7a8f', colourSecondary: '#406574', colourTertiary: '#2f4b57' },
|
||||
// loop_blocks: { colourPrimary: '#4f8f6d', colourSecondary: '#3f7458', colourTertiary: '#2e5944' },
|
||||
// math_blocks: { colourPrimary: '#c6844a', colourSecondary: '#a96f3c', colourTertiary: '#8a592f' },
|
||||
// text_blocks: { colourPrimary: '#b86a80', colourSecondary: '#9c5668', colourTertiary: '#7d4552' },
|
||||
// list_blocks: { colourPrimary: '#6d6fb8', colourSecondary: '#595a99', colourTertiary: '#444575' },
|
||||
// variable_blocks: { colourPrimary: '#aa5d7b', colourSecondary: '#8c4c64', colourTertiary: '#6f3b4e' },
|
||||
// procedure_blocks: { colourPrimary: '#8b6d52', colourSecondary: '#705640', colourTertiary: '#54402f' },
|
||||
// },
|
||||
// categoryStyles: {
|
||||
// logic_category: { colour: '#4f7a8f' },
|
||||
// loop_category: { colour: '#4f8f6d' },
|
||||
// math_category: { colour: '#c6844a' },
|
||||
// text_category: { colour: '#b86a80' },
|
||||
// list_category: { colour: '#6d6fb8' },
|
||||
// variable_category: { colour: '#aa5d7b' },
|
||||
// procedure_category: { colour: '#8b6d52' },
|
||||
// },
|
||||
// })
|
||||
|
||||
const handleResize = () => {
|
||||
if (workspace) {
|
||||
Blockly.svgResize(workspace)
|
||||
}
|
||||
}
|
||||
|
||||
const saveWorkspace = () => {
|
||||
if (!workspace || suppressSave) return
|
||||
if (saveTimer) {
|
||||
window.clearTimeout(saveTimer)
|
||||
}
|
||||
saveTimer = window.setTimeout(() => {
|
||||
const state = Blockly.serialization.workspaces.save(workspace!)
|
||||
localStorage.setItem(storageKey, JSON.stringify(state))
|
||||
}, 250)
|
||||
}
|
||||
|
||||
const restoreWorkspace = () => {
|
||||
if (!workspace) return
|
||||
const raw = localStorage.getItem(storageKey)
|
||||
if (!raw) return
|
||||
try {
|
||||
const state = JSON.parse(raw)
|
||||
suppressSave = true
|
||||
Blockly.serialization.workspaces.load(state, workspace)
|
||||
} catch {
|
||||
localStorage.removeItem(storageKey)
|
||||
} finally {
|
||||
suppressSave = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleClearWorkspace = () => {
|
||||
if (!workspace) return
|
||||
suppressSave = true
|
||||
workspace.clear()
|
||||
suppressSave = false
|
||||
localStorage.removeItem(storageKey)
|
||||
updateCode()
|
||||
}
|
||||
|
||||
const handleRun = () => {
|
||||
statusText.value = "执行环境尚未接入,可先复制代码到Python环境运行"
|
||||
window.setTimeout(() => {
|
||||
statusText.value = "准备就绪"
|
||||
}, 2200)
|
||||
if (!codePreview.value.trim()) {
|
||||
statusText.value = "No code to run."
|
||||
window.setTimeout(() => {
|
||||
statusText.value = idleStatus
|
||||
}, 1600)
|
||||
return
|
||||
}
|
||||
|
||||
runOutput.value = ""
|
||||
statusText.value = "Running..."
|
||||
|
||||
runPython(codePreview.value, (text) => {
|
||||
runOutput.value += text
|
||||
})
|
||||
.then(() => {
|
||||
if (!runOutput.value) {
|
||||
runOutput.value = ""
|
||||
}
|
||||
statusText.value = "运行成功"
|
||||
})
|
||||
.catch((error) => {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
runOutput.value =
|
||||
(runOutput.value ? `${runOutput.value}\n` : "") + `Error: ${message}`
|
||||
statusText.value = "运行失败"
|
||||
})
|
||||
.finally(() => {
|
||||
window.setTimeout(() => {
|
||||
statusText.value = idleStatus
|
||||
}, 2000)
|
||||
})
|
||||
}
|
||||
|
||||
const handleCopy = async () => {
|
||||
@@ -88,10 +119,10 @@ const handleDownload = () => {
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement("a")
|
||||
link.href = url
|
||||
link.download = "blockly_program.py"
|
||||
link.download = "demo.py"
|
||||
link.click()
|
||||
URL.revokeObjectURL(url)
|
||||
statusText.value = "已导出为 .py 文件"
|
||||
statusText.value = "已导出代码文件"
|
||||
window.setTimeout(() => {
|
||||
statusText.value = "准备就绪"
|
||||
}, 1800)
|
||||
@@ -112,12 +143,13 @@ onMounted(() => {
|
||||
minScale: 0.5,
|
||||
},
|
||||
trashcan: true,
|
||||
renderer: "geras",
|
||||
theme: Theme,
|
||||
media: "/blockly-media/",
|
||||
renderer: "zelos",
|
||||
theme: Blockly.Themes.Zelos,
|
||||
})
|
||||
workspace.addChangeListener(updateCode)
|
||||
workspace.addChangeListener(saveWorkspace)
|
||||
updateCode()
|
||||
restoreWorkspace()
|
||||
window.addEventListener("resize", handleResize)
|
||||
})
|
||||
|
||||
@@ -127,6 +159,9 @@ onBeforeUnmount(() => {
|
||||
workspace.dispose()
|
||||
workspace = null
|
||||
}
|
||||
if (saveTimer) {
|
||||
window.clearTimeout(saveTimer)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -134,18 +169,15 @@ onBeforeUnmount(() => {
|
||||
<div class="app-shell">
|
||||
<header class="app-header">
|
||||
<div class="brand">
|
||||
<span class="brand-badge">Blockly</span>
|
||||
<span class="brand-badge">草履虫</span>
|
||||
<div>
|
||||
<h1>中文Python可视化编程平台</h1>
|
||||
<p>拖拽方块,实时生成可执行的Python代码</p>
|
||||
<h1>拖拽式代码生成网页</h1>
|
||||
<p>拖拽方块,实时生成可执行的 Python 代码</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<button class="btn ghost" type="button" @click="handleCopy">
|
||||
复制代码
|
||||
</button>
|
||||
<button class="btn ghost" type="button" @click="handleDownload">
|
||||
导出 .py
|
||||
<button class="btn ghost" type="button" @click="handleClearWorkspace">
|
||||
清除
|
||||
</button>
|
||||
<button class="btn primary" type="button" @click="handleRun">
|
||||
运行
|
||||
@@ -160,9 +192,6 @@ onBeforeUnmount(() => {
|
||||
<h2>工作区</h2>
|
||||
<p>通过分类工具箱选择方块,构建你的程序</p>
|
||||
</div>
|
||||
<div class="panel-tip">
|
||||
<span>实时反馈</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="workspace-stage" ref="workspaceHost"></div>
|
||||
</section>
|
||||
@@ -170,10 +199,9 @@ onBeforeUnmount(() => {
|
||||
<aside class="side-panel">
|
||||
<div class="panel-head">
|
||||
<div>
|
||||
<h2>Python代码</h2>
|
||||
<p>符合PEP8风格,支持直接导出</p>
|
||||
<h2>Python 代码</h2>
|
||||
<p>由左侧的积木块自动生成</p>
|
||||
</div>
|
||||
<div class="panel-tip">预览</div>
|
||||
</div>
|
||||
<pre class="code-preview"><code>{{ codePreview }}</code></pre>
|
||||
<div class="status-bar">
|
||||
@@ -187,6 +215,8 @@ onBeforeUnmount(() => {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="output-label">运行结果</div>
|
||||
<pre class="code-preview output-preview"><code>{{ runOutput }}</code></pre>
|
||||
</aside>
|
||||
</main>
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@ import * as Blockly from 'blockly'
|
||||
import * as zhHans from 'blockly/msg/zh-hans'
|
||||
|
||||
export const setBlocklyLocale = () => {
|
||||
Blockly.setLocale(zhHans)
|
||||
Blockly.setLocale(zhHans as unknown as Record<string, string>)
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { pythonGenerator } from 'blockly/python'
|
||||
import { pythonGenerator, Order } from 'blockly/python'
|
||||
import type { Block } from 'blockly/core'
|
||||
|
||||
export const initPythonGenerator = () => {
|
||||
pythonGenerator.forBlock['cn_print'] = (block: Block, generator) => {
|
||||
const text = generator.valueToCode(block, 'TEXT', generator.ORDER_NONE) || "''"
|
||||
const text = generator.valueToCode(block, 'TEXT', Order.NONE) || "''"
|
||||
return `print(${text})\n`
|
||||
}
|
||||
|
||||
pythonGenerator.forBlock['cn_input'] = (block: Block, generator) => {
|
||||
const prompt = generator.valueToCode(block, 'PROMPT', generator.ORDER_NONE)
|
||||
const prompt = generator.valueToCode(block, 'PROMPT', Order.NONE)
|
||||
const code = prompt ? `input(${prompt})` : 'input()'
|
||||
return [code, generator.ORDER_FUNCTION_CALL]
|
||||
return [code, Order.FUNCTION_CALL]
|
||||
}
|
||||
|
||||
pythonGenerator.addReservedWords('input,print')
|
||||
|
||||
34
src/blockly/skulptRunner.ts
Normal file
34
src/blockly/skulptRunner.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
//@ts-ignore
|
||||
import Sk from "skulpt"
|
||||
|
||||
type OutputHandler = (text: string) => void
|
||||
|
||||
const readBuiltinFile = (path: string) => {
|
||||
if (!Sk?.builtinFiles?.files?.[path]) {
|
||||
throw new Error(`Skulpt file not found: ${path}`)
|
||||
}
|
||||
return Sk.builtinFiles.files[path]
|
||||
}
|
||||
|
||||
export const runPython = async (code: string, onOutput: OutputHandler) => {
|
||||
if (!Sk) {
|
||||
throw new Error("Skulpt failed to load.")
|
||||
}
|
||||
|
||||
Sk.configure({
|
||||
output: (text: string) => onOutput(text),
|
||||
read: readBuiltinFile,
|
||||
inputfun: (prompt: string) => {
|
||||
const response = window.prompt(prompt || "请输入")
|
||||
if (response === null) {
|
||||
throw new Error("用户取消输入")
|
||||
}
|
||||
return response
|
||||
},
|
||||
inputfunTakesPrompt: true,
|
||||
})
|
||||
|
||||
await Sk.misceval.asyncToPromise(() =>
|
||||
Sk.importMainWithBody("<stdin>", false, code, true)
|
||||
)
|
||||
}
|
||||
@@ -78,11 +78,5 @@ export const toolbox = {
|
||||
colour: '#A65D7B',
|
||||
custom: 'VARIABLE',
|
||||
},
|
||||
{
|
||||
kind: 'category',
|
||||
name: '函数',
|
||||
colour: '#8B6D52',
|
||||
custom: 'PROCEDURE',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -115,15 +115,6 @@ body {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.panel-tip {
|
||||
background: rgba(31, 122, 140, 0.12);
|
||||
color: var(--accent-strong);
|
||||
padding: 6px 12px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.workspace-stage {
|
||||
flex: 1;
|
||||
min-height: 520px;
|
||||
@@ -157,6 +148,21 @@ body {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.output-label {
|
||||
padding: 10px 18px;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
border-top: 1px solid var(--border);
|
||||
background: rgba(31, 122, 140, 0.06);
|
||||
}
|
||||
|
||||
.output-preview {
|
||||
flex: 0 0 160px;
|
||||
background: #0a1519;
|
||||
color: #d7f1f6;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.btn {
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
@@ -206,6 +212,11 @@ body {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.blocklyToolboxCategory {
|
||||
height: 40px;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.app-shell {
|
||||
padding: 20px;
|
||||
|
||||
Reference in New Issue
Block a user