From d4bac566e4e389879940c85eda44272c639d14a6 Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Wed, 14 Jan 2026 22:02:57 +0800 Subject: [PATCH] update --- index.html | 2 +- package-lock.json | 32 ++++--- package.json | 2 +- src/App.vue | 150 ++++++++++++++++++++------------- src/blockly/locale.ts | 2 +- src/blockly/pythonGenerator.ts | 8 +- src/blockly/skulptRunner.ts | 34 ++++++++ src/blockly/toolbox.ts | 6 -- src/style.css | 29 +++++-- 9 files changed, 170 insertions(+), 95 deletions(-) create mode 100644 src/blockly/skulptRunner.ts diff --git a/index.html b/index.html index 9d74b8b..614be6c 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ - 中文Blockly Python代码生成平台 + 草履虫
diff --git a/package-lock.json b/package-lock.json index af99937..8ce565e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,8 +8,8 @@ "name": "blockly", "version": "0.0.0", "dependencies": { - "@blockly/theme-modern": "^7.0.4", "blockly": "^12.3.1", + "skulpt": "^1.2.0", "vue": "^3.5.26" }, "devDependencies": { @@ -80,18 +80,6 @@ "node": ">=6.9.0" } }, - "node_modules/@blockly/theme-modern": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/@blockly/theme-modern/-/theme-modern-7.0.4.tgz", - "integrity": "sha512-34Q/oTA/hpqoPJ/7aUNgWdsjhCeo4530zuzF3NPOagPp9AbtEUwdZIdc82iIN8mhDkZjeeT69wC7OjoSoG3TbA==", - "license": "Apache-2.0", - "engines": { - "node": ">=8.17.0" - }, - "peerDependencies": { - "blockly": "^12.0.0" - } - }, "node_modules/@csstools/color-helpers": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", @@ -913,6 +901,12 @@ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", "license": "MIT" }, + "node_modules/jsbi": { + "version": "3.2.5", + "resolved": "https://registry.npmmirror.com/jsbi/-/jsbi-3.2.5.tgz", + "integrity": "sha512-aBE4n43IPvjaddScbvWRA2YlTzKEynHzu7MqOyTipdHucf/VxS63ViCjxYRg86M8Rxwbt/GfzHl1kKERkt45fQ==", + "license": "Apache-2.0" + }, "node_modules/jsdom": { "version": "26.1.0", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", @@ -1409,6 +1403,18 @@ "node": ">=v12.22.7" } }, + "node_modules/skulpt": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/skulpt/-/skulpt-1.2.0.tgz", + "integrity": "sha512-T0cv0sdSOXLlIJTuyXSeYJ3TFdWYSZfX2PcBLpRKrKZ3dTbVQXRMiYudSuko2xzcyMif47DS2ShatCwjCbsSsA==", + "license": "MIT", + "dependencies": { + "jsbi": "^3.1.4" + }, + "engines": { + "node": ">=10.4" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", diff --git a/package.json b/package.json index afaae14..946125b 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,8 @@ "preview": "vite preview" }, "dependencies": { - "@blockly/theme-modern": "^7.0.4", "blockly": "^12.3.1", + "skulpt": "^1.2.0", "vue": "^3.5.26" }, "devDependencies": { diff --git a/src/App.vue b/src/App.vue index a1e171a..fef88ca 100644 --- a/src/App.vue +++ b/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(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) + } }) @@ -134,18 +169,15 @@ onBeforeUnmount(() => {
- Blockly + 草履虫
-

中文Python可视化编程平台

-

拖拽方块,实时生成可执行的Python代码

+

拖拽式代码生成网页

+

拖拽方块,实时生成可执行的 Python 代码

- -
-
- 实时反馈 -
@@ -170,10 +199,9 @@ onBeforeUnmount(() => { diff --git a/src/blockly/locale.ts b/src/blockly/locale.ts index 7fb3a4d..20e3a63 100644 --- a/src/blockly/locale.ts +++ b/src/blockly/locale.ts @@ -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) } diff --git a/src/blockly/pythonGenerator.ts b/src/blockly/pythonGenerator.ts index 04e603f..4c36428 100644 --- a/src/blockly/pythonGenerator.ts +++ b/src/blockly/pythonGenerator.ts @@ -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') diff --git a/src/blockly/skulptRunner.ts b/src/blockly/skulptRunner.ts new file mode 100644 index 0000000..83bff30 --- /dev/null +++ b/src/blockly/skulptRunner.ts @@ -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("", false, code, true) + ) +} diff --git a/src/blockly/toolbox.ts b/src/blockly/toolbox.ts index 50229b0..34793f7 100644 --- a/src/blockly/toolbox.ts +++ b/src/blockly/toolbox.ts @@ -78,11 +78,5 @@ export const toolbox = { colour: '#A65D7B', custom: 'VARIABLE', }, - { - kind: 'category', - name: '函数', - colour: '#8B6D52', - custom: 'PROCEDURE', - }, ], } diff --git a/src/style.css b/src/style.css index 28c8fea..604db90 100644 --- a/src/style.css +++ b/src/style.css @@ -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;