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