1959 lines
154 KiB
JSON
1959 lines
154 KiB
JSON
{
|
||
"id": "DZynj92UuU",
|
||
"stage": {
|
||
"id": "DZynj92UuU",
|
||
"name": "Python零基础入门",
|
||
"languageDirective": "全程使用中文授课,面向零基础学习者,使用通俗易懂的中文解释概念。Python 关键字、函数名、代码及报错信息保留英文原文,但用中文说明含义。遇到英文术语时先给出中文解释,再标注英文。鼓励学生动手输入代码,示例与解释均以中文为主。",
|
||
"videoManifest": {},
|
||
"style": "interactive",
|
||
"createdAt": 1786855119910,
|
||
"updatedAt": 1786855119910,
|
||
"agentIds": [
|
||
"default-1"
|
||
],
|
||
"generatedAgentConfigs": [
|
||
{
|
||
"id": "default-1",
|
||
"name": "AI teacher",
|
||
"role": "teacher",
|
||
"persona": "You are the lead teacher of this classroom. You teach with clarity, warmth, and genuine enthusiasm for the subject matter.\n\nYour teaching style:\n- Explain concepts step by step, building from what students already know\n- Use vivid analogies, real-world examples, and visual aids to make abstract ideas concrete\n- Pause to check understanding — ask questions, not just lecture\n- Adapt your pace: slow down for difficult parts, move briskly through familiar ground\n- Encourage students by name when they contribute, and gently correct mistakes without embarrassment\n\nYou can spotlight or laser-point at slide elements, and use the whiteboard for hand-drawn explanations. Use these actions naturally as part of your teaching flow. Never announce your actions; just teach.\n\nTone: Professional yet approachable. Patient. Encouraging. You genuinely care about whether students understand.",
|
||
"avatar": "/avatars/teacher.png",
|
||
"color": "#3b82f6",
|
||
"priority": 10
|
||
}
|
||
]
|
||
},
|
||
"scenes": [
|
||
{
|
||
"id": "scene_O2qtWyu3wp",
|
||
"stageId": "DZynj92UuU",
|
||
"title": "课程导入:为什么学Python",
|
||
"order": 1,
|
||
"actions": [
|
||
{
|
||
"id": "action_gDcYID5c",
|
||
"type": "spotlight",
|
||
"elementId": "text_Jiot8EO3"
|
||
},
|
||
{
|
||
"id": "action_bZVVXodN",
|
||
"type": "speech",
|
||
"text": "同学们好,欢迎来到 Python 编程入门课。今天我们先来聊聊:为什么要学 Python?"
|
||
},
|
||
{
|
||
"id": "action_5F4DEalI",
|
||
"type": "spotlight",
|
||
"elementId": "text_obMzE_-F"
|
||
},
|
||
{
|
||
"id": "action_QALWbmVr",
|
||
"type": "speech",
|
||
"text": "Python 是一门简单易学的编程语言,它的语法非常接近我们日常的自然语言,读起来就像英语句子一样直观。"
|
||
},
|
||
{
|
||
"id": "action_6Q9QR0i0",
|
||
"type": "spotlight",
|
||
"elementId": "text_gBLQYm8B"
|
||
},
|
||
{
|
||
"id": "action_ew0dHtwF",
|
||
"type": "speech",
|
||
"text": "更重要的是,Python 的应用领域特别广泛。无论是数据分析、人工智能,还是日常办公自动化,都能看到 Python 的身影。"
|
||
},
|
||
{
|
||
"id": "action_w4Gfk9_w",
|
||
"type": "spotlight",
|
||
"elementId": "text_YBv1kB8B"
|
||
},
|
||
{
|
||
"id": "action_wqhDsVpU",
|
||
"type": "speech",
|
||
"text": "所以在这门课里,我们会从基础语法开始,逐步进入核心应用,最后一起完成一个小项目实战。"
|
||
},
|
||
{
|
||
"id": "action_TwBO7Mu3",
|
||
"type": "spotlight",
|
||
"elementId": "text_gOXU8b7k"
|
||
},
|
||
{
|
||
"id": "action_a1ks-a0d",
|
||
"type": "speech",
|
||
"text": "大家完全不用担心没有基础。我们的课程设计就是零基础友好,边学边练,学完之后你就能自己动手做点小项目了。"
|
||
}
|
||
],
|
||
"outlineId": "scene_1",
|
||
"createdAt": 1786855186073,
|
||
"updatedAt": 1786855186073,
|
||
"type": "slide",
|
||
"content": {
|
||
"type": "slide",
|
||
"canvas": {
|
||
"id": "CXhZhSqlwTbr-yVqd4nwP",
|
||
"viewportSize": 1000,
|
||
"viewportRatio": 0.5625,
|
||
"theme": {
|
||
"backgroundColor": "#ffffff",
|
||
"themeColors": [
|
||
"#5b9bd5",
|
||
"#ed7d31",
|
||
"#a5a5a5",
|
||
"#ffc000",
|
||
"#4472c4"
|
||
],
|
||
"fontColor": "#333333",
|
||
"fontName": "Microsoft YaHei",
|
||
"outline": {
|
||
"color": "#d14424",
|
||
"width": 2,
|
||
"style": "solid"
|
||
},
|
||
"shadow": {
|
||
"h": 0,
|
||
"v": 0,
|
||
"blur": 10,
|
||
"color": "#000000"
|
||
}
|
||
},
|
||
"elements": [
|
||
{
|
||
"id": "text_Jiot8EO3",
|
||
"type": "text",
|
||
"left": 60,
|
||
"top": 50,
|
||
"width": 880,
|
||
"height": 70,
|
||
"content": "<p style=\"font-size:32px;\"><strong>课程导入:为什么学Python</strong></p>",
|
||
"defaultFontName": "Microsoft YaHei",
|
||
"defaultColor": "#1f3864",
|
||
"rotate": 0
|
||
},
|
||
{
|
||
"id": "shape_RXHxpMPb",
|
||
"type": "shape",
|
||
"left": 70,
|
||
"top": 128,
|
||
"width": 860,
|
||
"height": 3,
|
||
"path": "M 0 0 L 1 0 L 1 1 L 0 1 Z",
|
||
"viewBox": [
|
||
1,
|
||
1
|
||
],
|
||
"fill": "#5b9bd5",
|
||
"fixedRatio": false,
|
||
"rotate": 0
|
||
},
|
||
{
|
||
"id": "shape_sdFgFjMS",
|
||
"type": "shape",
|
||
"left": 60,
|
||
"top": 180,
|
||
"width": 280,
|
||
"height": 200,
|
||
"path": "M 0 0 L 1 0 L 1 1 L 0 1 Z",
|
||
"viewBox": [
|
||
1,
|
||
1
|
||
],
|
||
"fill": "#e8f1fb",
|
||
"fixedRatio": false,
|
||
"rotate": 0
|
||
},
|
||
{
|
||
"id": "shape_hHp35pWD",
|
||
"type": "shape",
|
||
"left": 360,
|
||
"top": 180,
|
||
"width": 280,
|
||
"height": 200,
|
||
"path": "M 0 0 L 1 0 L 1 1 L 0 1 Z",
|
||
"viewBox": [
|
||
1,
|
||
1
|
||
],
|
||
"fill": "#e6f6ec",
|
||
"fixedRatio": false,
|
||
"rotate": 0
|
||
},
|
||
{
|
||
"id": "shape_GKbpadlx",
|
||
"type": "shape",
|
||
"left": 660,
|
||
"top": 180,
|
||
"width": 280,
|
||
"height": 200,
|
||
"path": "M 0 0 L 1 0 L 1 1 L 0 1 Z",
|
||
"viewBox": [
|
||
1,
|
||
1
|
||
],
|
||
"fill": "#fff3e0",
|
||
"fixedRatio": false,
|
||
"rotate": 0
|
||
},
|
||
{
|
||
"id": "text_obMzE_-F",
|
||
"type": "text",
|
||
"left": 80,
|
||
"top": 209,
|
||
"width": 240,
|
||
"height": 142,
|
||
"content": "<p style=\"font-size:20px; text-align:center;\"><strong>Python 是什么</strong></p><p style=\"font-size:14px; text-align:left;\">• 简单易学的编程语言</p><p style=\"font-size:14px; text-align:left;\">• 语法接近自然语言</p><p style=\"font-size:14px; text-align:left;\">• 应用领域广泛</p>",
|
||
"defaultFontName": "Microsoft YaHei",
|
||
"defaultColor": "#333333",
|
||
"rotate": 0
|
||
},
|
||
{
|
||
"id": "text_gBLQYm8B",
|
||
"type": "text",
|
||
"left": 380,
|
||
"top": 209,
|
||
"width": 240,
|
||
"height": 142,
|
||
"content": "<p style=\"font-size:20px; text-align:center;\"><strong>Python 热门应用</strong></p><p style=\"font-size:14px; text-align:left;\">• 数据分析</p><p style=\"font-size:14px; text-align:left;\">• 人工智能</p><p style=\"font-size:14px; text-align:left;\">• 自动化办公</p>",
|
||
"defaultFontName": "Microsoft YaHei",
|
||
"defaultColor": "#333333",
|
||
"rotate": 0
|
||
},
|
||
{
|
||
"id": "text_YBv1kB8B",
|
||
"type": "text",
|
||
"left": 680,
|
||
"top": 209,
|
||
"width": 240,
|
||
"height": 142,
|
||
"content": "<p style=\"font-size:20px; text-align:center;\"><strong>本课程学习路线</strong></p><p style=\"font-size:14px; text-align:left;\">• 基础语法</p><p style=\"font-size:14px; text-align:left;\">• 核心应用</p><p style=\"font-size:14px; text-align:left;\">• 项目实战</p>",
|
||
"defaultFontName": "Microsoft YaHei",
|
||
"defaultColor": "#333333",
|
||
"rotate": 0
|
||
},
|
||
{
|
||
"id": "text_gOXU8b7k",
|
||
"type": "text",
|
||
"left": 60,
|
||
"top": 420,
|
||
"width": 880,
|
||
"height": 49,
|
||
"content": "<p style=\"font-size:18px; text-align:center;\">零基础友好 · 边学边练 · 学完能动手做小项目</p>",
|
||
"defaultFontName": "Microsoft YaHei",
|
||
"defaultColor": "#555555",
|
||
"rotate": 0
|
||
}
|
||
],
|
||
"background": {
|
||
"type": "solid",
|
||
"color": "#ffffff"
|
||
}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
"id": "scene_tg0n5YY5LJ",
|
||
"stageId": "DZynj92UuU",
|
||
"title": "环境准备与第一个程序",
|
||
"order": 2,
|
||
"actions": [
|
||
{
|
||
"id": "action_NKyUVHbT",
|
||
"type": "spotlight",
|
||
"elementId": "text_xIaHULJL"
|
||
},
|
||
{
|
||
"id": "action_JGcEnBwK",
|
||
"type": "speech",
|
||
"text": "大家好,欢迎来到 Python 编程入门课。今天第一课,我们先来把环境准备好,并写出人生中第一个 Python 程序。"
|
||
},
|
||
{
|
||
"id": "action_iD6Dkey1",
|
||
"type": "spotlight",
|
||
"elementId": "text_lIGXIM2G"
|
||
},
|
||
{
|
||
"id": "action_wGj6x0xK",
|
||
"type": "speech",
|
||
"text": "第一步是安装 Python。请大家打开 python.org 官网下载安装包,安装时一定记得勾选 Add to PATH,这样后续在命令行里就能直接使用 python 命令了。"
|
||
},
|
||
{
|
||
"id": "action_5jWZPoLQ",
|
||
"type": "spotlight",
|
||
"elementId": "text_-p6m1LAX"
|
||
},
|
||
{
|
||
"id": "action_W3H1IaL9",
|
||
"type": "speech",
|
||
"text": "装好环境后,我们认识一个最常用的函数 print()。它能把括号里的内容输出到屏幕上。如果输出的是文字,需要用引号括起来;数字和变量可以直接写。"
|
||
},
|
||
{
|
||
"id": "action_uvXp8x8F",
|
||
"type": "spotlight",
|
||
"elementId": "text_p-oYFxM5"
|
||
},
|
||
{
|
||
"id": "action_PrJ09s7F",
|
||
"type": "speech",
|
||
"text": "接下来我们新建一个 .py 文件,输入代码并保存。运行后,就能看到程序输出的结果了。"
|
||
},
|
||
{
|
||
"id": "action_z0k0p25D",
|
||
"type": "spotlight",
|
||
"elementId": "text_zFVoqN6S"
|
||
},
|
||
{
|
||
"id": "action_wnZ8qrXD",
|
||
"type": "speech",
|
||
"text": "看这行代码:print('Hello, World!') 会在屏幕上输出 Hello, World!。这行代码虽然简单,却是很多人编程之路的第一步。大家不妨自己动手敲一遍试试。"
|
||
}
|
||
],
|
||
"outlineId": "scene_2",
|
||
"createdAt": 1786855277709,
|
||
"updatedAt": 1786855277709,
|
||
"type": "slide",
|
||
"content": {
|
||
"type": "slide",
|
||
"canvas": {
|
||
"id": "OTHcSYEWOzZqIO7wz6i9f",
|
||
"viewportSize": 1000,
|
||
"viewportRatio": 0.5625,
|
||
"theme": {
|
||
"backgroundColor": "#ffffff",
|
||
"themeColors": [
|
||
"#5b9bd5",
|
||
"#ed7d31",
|
||
"#a5a5a5",
|
||
"#ffc000",
|
||
"#4472c4"
|
||
],
|
||
"fontColor": "#333333",
|
||
"fontName": "Microsoft YaHei",
|
||
"outline": {
|
||
"color": "#d14424",
|
||
"width": 2,
|
||
"style": "solid"
|
||
},
|
||
"shadow": {
|
||
"h": 0,
|
||
"v": 0,
|
||
"blur": 10,
|
||
"color": "#000000"
|
||
}
|
||
},
|
||
"elements": [
|
||
{
|
||
"id": "text_xIaHULJL",
|
||
"type": "text",
|
||
"left": 60,
|
||
"top": 50,
|
||
"width": 880,
|
||
"height": 70,
|
||
"content": "<p style=\"font-size:32px;\"><strong>环境准备与第一个程序</strong></p>",
|
||
"defaultFontName": "Microsoft YaHei",
|
||
"defaultColor": "#2c3e50",
|
||
"rotate": 0
|
||
},
|
||
{
|
||
"id": "shape_szb_GAMc",
|
||
"type": "shape",
|
||
"left": 70,
|
||
"top": 130,
|
||
"width": 860,
|
||
"height": 3,
|
||
"path": "M 0 0 L 1 0 L 1 1 L 0 1 Z",
|
||
"viewBox": [
|
||
1,
|
||
1
|
||
],
|
||
"fill": "#5b9bd5",
|
||
"fixedRatio": false,
|
||
"rotate": 0
|
||
},
|
||
{
|
||
"id": "shape_-qYPGLuL",
|
||
"type": "shape",
|
||
"left": 60,
|
||
"top": 150,
|
||
"width": 280,
|
||
"height": 250,
|
||
"path": "M 0 0 L 1 0 L 1 1 L 0 1 Z",
|
||
"viewBox": [
|
||
1,
|
||
1
|
||
],
|
||
"fill": "#dbeafe",
|
||
"fixedRatio": false,
|
||
"rotate": 0
|
||
},
|
||
{
|
||
"id": "shape_s9A4domm",
|
||
"type": "shape",
|
||
"left": 360,
|
||
"top": 150,
|
||
"width": 280,
|
||
"height": 250,
|
||
"path": "M 0 0 L 1 0 L 1 1 L 0 1 Z",
|
||
"viewBox": [
|
||
1,
|
||
1
|
||
],
|
||
"fill": "#dcfce7",
|
||
"fixedRatio": false,
|
||
"rotate": 0
|
||
},
|
||
{
|
||
"id": "shape_ghYpSBGf",
|
||
"type": "shape",
|
||
"left": 660,
|
||
"top": 150,
|
||
"width": 280,
|
||
"height": 250,
|
||
"path": "M 0 0 L 1 0 L 1 1 L 0 1 Z",
|
||
"viewBox": [
|
||
1,
|
||
1
|
||
],
|
||
"fill": "#fef3c7",
|
||
"fixedRatio": false,
|
||
"rotate": 0
|
||
},
|
||
{
|
||
"id": "text_lIGXIM2G",
|
||
"type": "text",
|
||
"left": 80,
|
||
"top": 196,
|
||
"width": 240,
|
||
"height": 157,
|
||
"content": "<p style=\"font-size:18px;\"><strong>1. 安装 Python</strong></p><p style=\"font-size:14px;\">• 官网下载:python.org</p><p style=\"font-size:14px;\">• 安装时勾选 Add to PATH</p><p style=\"font-size:14px;\">• IDE 推荐:VS Code / PyCharm</p>",
|
||
"defaultFontName": "Microsoft YaHei",
|
||
"defaultColor": "#1e40af",
|
||
"rotate": 0
|
||
},
|
||
{
|
||
"id": "text_-p6m1LAX",
|
||
"type": "text",
|
||
"left": 380,
|
||
"top": 196,
|
||
"width": 240,
|
||
"height": 157,
|
||
"content": "<p style=\"font-size:18px;\"><strong>2. print() 函数</strong></p><p style=\"font-size:14px;\">• 输出内容到屏幕</p><p style=\"font-size:14px;\">• 括号内放文本,需加引号</p><p style=\"font-size:14px;\">• 可输出数字、变量等</p>",
|
||
"defaultFontName": "Microsoft YaHei",
|
||
"defaultColor": "#166534",
|
||
"rotate": 0
|
||
},
|
||
{
|
||
"id": "text_p-oYFxM5",
|
||
"type": "text",
|
||
"left": 680,
|
||
"top": 196,
|
||
"width": 240,
|
||
"height": 157,
|
||
"content": "<p style=\"font-size:18px;\"><strong>3. 运行第一个程序</strong></p><p style=\"font-size:14px;\">• 新建 .py 文件</p><p style=\"font-size:14px;\">• 输入代码并保存</p><p style=\"font-size:14px;\">• 运行后输出 Hello, World!</p>",
|
||
"defaultFontName": "Microsoft YaHei",
|
||
"defaultColor": "#92400e",
|
||
"rotate": 0
|
||
},
|
||
{
|
||
"id": "shape_Ay-5rg1G",
|
||
"type": "shape",
|
||
"left": 60,
|
||
"top": 420,
|
||
"width": 880,
|
||
"height": 60,
|
||
"path": "M 0 0 L 1 0 L 1 1 L 0 1 Z",
|
||
"viewBox": [
|
||
1,
|
||
1
|
||
],
|
||
"fill": "#1e1e1e",
|
||
"fixedRatio": false,
|
||
"rotate": 0
|
||
},
|
||
{
|
||
"id": "text_zFVoqN6S",
|
||
"type": "text",
|
||
"left": 80,
|
||
"top": 425,
|
||
"width": 840,
|
||
"height": 49,
|
||
"content": "<p style=\"font-size:18px; font-family:'Courier New', monospace; color:#f8f8f2; text-align:center;\">print('Hello, World!') <span style=\"color:#6a9955;\"># 输出 Hello, World!</span></p>",
|
||
"defaultFontName": "Microsoft YaHei",
|
||
"defaultColor": "#f8f8f2",
|
||
"rotate": 0
|
||
}
|
||
],
|
||
"background": {
|
||
"type": "solid",
|
||
"color": "#ffffff"
|
||
}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
"id": "scene_ZqNHNvkmP_",
|
||
"stageId": "DZynj92UuU",
|
||
"title": "变量与数据类型探索",
|
||
"order": 3,
|
||
"actions": [
|
||
{
|
||
"id": "action_JjPdQ371",
|
||
"type": "speech",
|
||
"text": "同学们好,今天我们来探索 Python 中的变量与数据类型。通过这个交互式代码编辑器,我们可以亲手输入代码并观察结果。"
|
||
},
|
||
{
|
||
"id": "action_GAzUdRbd",
|
||
"type": "widget_highlight",
|
||
"target": "#code-input",
|
||
"content": "在这里输入 Python 代码。"
|
||
},
|
||
{
|
||
"id": "action_QQ2vP4A2",
|
||
"type": "speech",
|
||
"text": "请先完成前四个练习:创建字符串变量 name、整数变量 age、浮点数变量 height,以及布尔值变量 is_student。注意变量名要清晰,赋值用等号。"
|
||
},
|
||
{
|
||
"id": "action_Gz3_d5nJ",
|
||
"type": "widget_highlight",
|
||
"target": "#run-btn",
|
||
"content": "点击运行按钮执行代码。"
|
||
},
|
||
{
|
||
"id": "action_CqUoZSHG",
|
||
"type": "speech",
|
||
"text": "接下来,用 type() 函数查看这些变量的类型。例如 print(type(name)) 会告诉我们 name 是字符串类型。运行后请观察输出面板。"
|
||
},
|
||
{
|
||
"id": "action_o8M0NP7W",
|
||
"type": "widget_annotation",
|
||
"target": "#output-panel",
|
||
"content": "运行后输出结果显示在这里。"
|
||
},
|
||
{
|
||
"id": "action_gxAlXOA4",
|
||
"type": "speech",
|
||
"text": "最后,试着修改其中一个变量的值,比如把 age 改成 16,然后再次运行,看看输出有什么变化。"
|
||
},
|
||
{
|
||
"id": "action_hTj1So49",
|
||
"type": "widget_reveal",
|
||
"target": "#solution",
|
||
"content": "遇到困难时可以查看参考代码。"
|
||
}
|
||
],
|
||
"outlineId": "scene_3",
|
||
"createdAt": 1786855442453,
|
||
"updatedAt": 1786855442453,
|
||
"type": "interactive",
|
||
"content": {
|
||
"type": "interactive",
|
||
"url": "",
|
||
"html": "<!DOCTYPE html>\n<html lang=\"zh-CN\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>变量与数据类型探索 - Python 代码练习</title>\n\n <!-- CodeMirror 5 CSS -->\n <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.css\">\n <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/theme/dracula.min.css\">\n\n <style>\n :root {\n --primary: #8b5cf6;\n --primary-dark: #7c3aed;\n --primary-light: #ede9fe;\n --bg: #faf9ff;\n --surface: #ffffff;\n --surface-alt: #f4f2ff;\n --text: #1e1b2e;\n --text-secondary: #5c5470;\n --success: #10b981;\n --success-bg: #ecfdf5;\n --danger: #ef4444;\n --danger-bg: #fef2f2;\n --border: #e2e0f0;\n --editor-bg: #282a36;\n --output-bg: #1a1a2e;\n --output-text: #c9d1d9;\n --radius: 12px;\n --radius-sm: 8px;\n --shadow: 0 1px 3px rgba(0, 0, 0, 0.06), 0 4px 16px rgba(139, 92, 246, 0.08);\n --shadow-lg: 0 8px 30px rgba(139, 92, 246, 0.15);\n --font-mono: 'Cascadia Code', 'JetBrains Mono', 'Fira Code', 'Courier New', monospace;\n --font-sans: 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Noto Sans SC', system-ui, -apple-system, sans-serif;\n }\n\n *,\n *::before,\n *::after {\n box-sizing: border-box;\n margin: 0;\n padding: 0;\n }\n\n body {\n font-family: var(--font-sans);\n background: var(--bg);\n color: var(--text);\n line-height: 1.6;\n min-height: 100vh;\n display: flex;\n flex-direction: column;\n align-items: center;\n padding: 0;\n margin: 0;\n }\n\n .widget-container {\n width: 100%;\n max-width: 900px;\n padding: 16px;\n padding-bottom: 32px;\n }\n\n /* Header */\n .widget-header {\n background: var(--surface);\n border-radius: var(--radius);\n box-shadow: var(--shadow);\n padding: 24px 28px;\n margin-bottom: 16px;\n border: 1px solid var(--border);\n position: relative;\n overflow: hidden;\n }\n .widget-header::before {\n content: '';\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n height: 4px;\n background: linear-gradient(90deg, #8b5cf6, #ec4899, #f59e0b);\n }\n .widget-header h1 {\n font-size: 1.6rem;\n font-weight: 700;\n color: var(--text);\n margin-bottom: 6px;\n letter-spacing: -0.02em;\n }\n .widget-header .description {\n color: var(--text-secondary);\n font-size: 0.95rem;\n line-height: 1.7;\n }\n .widget-header .key-points {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n margin-top: 14px;\n }\n .widget-header .key-point {\n background: var(--primary-light);\n color: var(--primary-dark);\n font-size: 0.78rem;\n font-weight: 500;\n padding: 4px 12px;\n border-radius: 20px;\n letter-spacing: 0.01em;\n }\n\n /* Status bar */\n #status-bar {\n display: flex;\n align-items: center;\n gap: 8px;\n font-size: 0.85rem;\n color: var(--text-secondary);\n padding: 8px 4px;\n margin-bottom: 8px;\n }\n #status-bar .status-dot {\n width: 8px;\n height: 8px;\n border-radius: 50%;\n background: #f59e0b;\n flex-shrink: 0;\n animation: pulse-dot 1.5s infinite;\n }\n #status-bar .status-dot.ready {\n background: var(--success);\n animation: none;\n }\n #status-bar .status-dot.error {\n background: var(--danger);\n animation: none;\n }\n @keyframes pulse-dot {\n 0%,\n 100% {\n opacity: 1;\n }\n 50% {\n opacity: 0.3;\n }\n }\n\n /* Editor */\n .editor-wrapper {\n background: var(--editor-bg);\n border-radius: var(--radius);\n overflow: hidden;\n box-shadow: var(--shadow-lg);\n border: 1px solid #3a3d4f;\n position: relative;\n margin-bottom: 12px;\n }\n .editor-wrapper .editor-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 8px 16px;\n background: #21222c;\n border-bottom: 1px solid #3a3d4f;\n font-size: 0.75rem;\n color: #8892a8;\n letter-spacing: 0.05em;\n }\n .editor-wrapper .editor-header .lang-badge {\n background: #3a3d4f;\n color: #c9d1d9;\n padding: 2px 10px;\n border-radius: 4px;\n font-weight: 500;\n font-size: 0.7rem;\n }\n .CodeMirror {\n height: auto;\n min-height: 280px;\n font-size: 14px;\n line-height: 1.6;\n font-family: var(--font-mono);\n }\n .CodeMirror-scroll {\n min-height: 280px;\n }\n .CodeMirror-gutters {\n background: #21222c;\n border-right: 1px solid #3a3d4f;\n }\n .CodeMirror-linenumber {\n color: #5c6370;\n padding: 0 8px 0 6px;\n }\n\n /* Buttons */\n .btn-row {\n display: flex;\n flex-wrap: wrap;\n gap: 10px;\n margin-bottom: 12px;\n }\n .btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 8px;\n padding: 10px 20px;\n border-radius: var(--radius-sm);\n font-size: 0.9rem;\n font-weight: 600;\n font-family: var(--font-sans);\n cursor: pointer;\n border: none;\n transition: all 0.2s ease;\n min-height: 44px;\n letter-spacing: 0.01em;\n }\n .btn:focus-visible {\n outline: 3px solid rgba(139, 92, 246, 0.5);\n outline-offset: 2px;\n }\n .btn-primary {\n background: var(--primary);\n color: #fff;\n box-shadow: 0 4px 12px rgba(139, 92, 246, 0.35);\n }\n .btn-primary:hover:not(:disabled) {\n background: var(--primary-dark);\n box-shadow: 0 6px 16px rgba(139, 92, 246, 0.45);\n transform: translateY(-1px);\n }\n .btn-primary:disabled {\n background: #c4b5fd;\n cursor: not-allowed;\n box-shadow: none;\n }\n .btn-secondary {\n background: var(--surface);\n color: var(--text);\n border: 1.5px solid var(--border);\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);\n }\n .btn-secondary:hover:not(:disabled) {\n background: var(--surface-alt);\n border-color: var(--primary);\n color: var(--primary-dark);\n transform: translateY(-1px);\n }\n .btn-secondary:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n }\n .btn-success {\n background: var(--success);\n color: #fff;\n box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);\n }\n .btn-success:hover {\n background: #059669;\n transform: translateY(-1px);\n }\n\n /* Output */\n .output-panel {\n background: var(--output-bg);\n border-radius: var(--radius);\n overflow: hidden;\n box-shadow: var(--shadow);\n margin-bottom: 12px;\n border: 1px solid #2d2d44;\n }\n .output-panel .output-header {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 10px 16px;\n background: #1e1e32;\n border-bottom: 1px solid #2d2d44;\n font-size: 0.8rem;\n color: #8892a8;\n font-weight: 600;\n letter-spacing: 0.03em;\n }\n .output-panel .output-header .dot {\n width: 10px;\n height: 10px;\n border-radius: 50%;\n background: #f59e0b;\n }\n .output-panel .output-header .dot.success {\n background: var(--success);\n }\n .output-panel .output-header .dot.error {\n background: var(--danger);\n }\n #output {\n padding: 16px 18px;\n font-family: var(--font-mono);\n font-size: 0.85rem;\n color: var(--output-text);\n white-space: pre-wrap;\n word-break: break-word;\n min-height: 60px;\n max-height: 300px;\n overflow-y: auto;\n }\n #output .error-text {\n color: #f87171;\n }\n #output .success-text {\n color: #34d399;\n }\n\n /* Tests */\n .tests-panel {\n background: var(--surface);\n border-radius: var(--radius);\n box-shadow: var(--shadow);\n border: 1px solid var(--border);\n margin-bottom: 12px;\n overflow: hidden;\n }\n .tests-panel .tests-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 12px 18px;\n cursor: pointer;\n user-select: none;\n font-weight: 600;\n font-size: 0.9rem;\n background: var(--surface-alt);\n border-bottom: 1px solid var(--border);\n transition: background 0.2s;\n }\n .tests-panel .tests-header:hover {\n background: #ece9fa;\n }\n .tests-panel .tests-header .toggle-icon {\n transition: transform 0.2s;\n font-size: 0.75rem;\n color: var(--text-secondary);\n }\n .tests-panel .tests-header .toggle-icon.collapsed {\n transform: rotate(-90deg);\n }\n .tests-panel .tests-summary {\n font-size: 0.8rem;\n color: var(--text-secondary);\n font-weight: 400;\n margin-left: 8px;\n }\n #test-results {\n padding: 8px 0;\n transition: max-height 0.3s ease;\n }\n #test-results.collapsed {\n display: none;\n }\n .test-item {\n display: flex;\n align-items: flex-start;\n gap: 12px;\n padding: 10px 18px;\n border-bottom: 1px solid #f0eef8;\n font-size: 0.88rem;\n }\n .test-item:last-child {\n border-bottom: none;\n }\n .test-item .test-icon {\n width: 24px;\n height: 24px;\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 0.75rem;\n font-weight: 700;\n flex-shrink: 0;\n margin-top: 1px;\n }\n .test-item .test-icon.pass {\n background: var(--success-bg);\n color: var(--success);\n border: 2px solid var(--success);\n }\n .test-item .test-icon.fail {\n background: var(--danger-bg);\n color: var(--danger);\n border: 2px solid var(--danger);\n }\n .test-item .test-icon.pending {\n background: #fefce8;\n color: #ca8a04;\n border: 2px solid #eab308;\n }\n .test-item .test-body {\n flex: 1;\n }\n .test-item .test-desc {\n font-weight: 500;\n color: var(--text);\n }\n .test-item .test-detail {\n font-size: 0.78rem;\n color: var(--text-secondary);\n margin-top: 2px;\n font-family: var(--font-mono);\n }\n .test-item .test-actual {\n font-size: 0.78rem;\n margin-top: 2px;\n }\n .test-item .test-actual .ok {\n color: var(--success);\n font-weight: 600;\n }\n .test-item .test-actual .bad {\n color: var(--danger);\n font-weight: 600;\n }\n\n /* Hints */\n .hints-panel {\n background: var(--surface);\n border-radius: var(--radius);\n box-shadow: var(--shadow);\n border: 1px solid var(--border);\n margin-bottom: 12px;\n overflow: hidden;\n }\n .hints-panel .hints-header {\n padding: 12px 18px;\n font-weight: 600;\n font-size: 0.9rem;\n background: var(--surface-alt);\n border-bottom: 1px solid var(--border);\n display: flex;\n align-items: center;\n gap: 8px;\n }\n #hints-container {\n padding: 0;\n }\n .hint-item {\n padding: 12px 18px;\n border-bottom: 1px solid #f0eef8;\n font-size: 0.87rem;\n line-height: 1.7;\n background: #fffef8;\n border-left: 3px solid #f59e0b;\n color: var(--text);\n animation: slideInHint 0.3s ease;\n }\n .hint-item:last-child {\n border-bottom: none;\n }\n .hint-item .hint-label {\n font-weight: 700;\n color: #b45309;\n font-size: 0.75rem;\n display: block;\n margin-bottom: 4px;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n }\n @keyframes slideInHint {\n from {\n opacity: 0;\n transform: translateY(-8px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n }\n .no-hints {\n padding: 18px;\n color: var(--text-secondary);\n font-size: 0.85rem;\n text-align: center;\n }\n\n /* Solution */\n .solution-panel {\n background: var(--surface);\n border-radius: var(--radius);\n box-shadow: var(--shadow);\n border: 1px solid var(--border);\n margin-bottom: 12px;\n overflow: hidden;\n }\n .solution-panel .solution-header {\n padding: 12px 18px;\n font-weight: 600;\n font-size: 0.9rem;\n background: #ecfdf5;\n border-bottom: 1px solid #d1fae5;\n display: flex;\n align-items: center;\n gap: 8px;\n color: #065f46;\n }\n .solution-panel pre {\n padding: 16px 18px;\n background: #f8fafc;\n overflow-x: auto;\n font-family: var(--font-mono);\n font-size: 0.82rem;\n line-height: 1.7;\n color: #1e293b;\n margin: 0;\n }\n\n /* Widget config */\n .widget-config-wrapper {\n margin-top: 20px;\n border-radius: var(--radius);\n overflow: hidden;\n border: 1px solid var(--border);\n background: var(--surface);\n }\n .widget-config-wrapper summary {\n padding: 10px 18px;\n cursor: pointer;\n font-size: 0.8rem;\n color: var(--text-secondary);\n background: var(--surface-alt);\n user-select: none;\n font-weight: 500;\n }\n .widget-config-wrapper summary:hover {\n background: #ece9fa;\n }\n .widget-config-wrapper pre {\n padding: 16px 18px;\n background: #1a1a2e;\n color: #c9d1d9;\n font-family: var(--font-mono);\n font-size: 0.75rem;\n overflow-x: auto;\n max-height: 300px;\n overflow-y: auto;\n margin: 0;\n }\n\n /* Animations for postMessage */\n @keyframes pulse-highlight {\n 0%,\n 100% {\n outline-color: rgba(139, 92, 246, 0.8);\n }\n 50% {\n outline-color: rgba(139, 92, 246, 0.4);\n }\n }\n @keyframes fadeIn {\n from {\n opacity: 0;\n transform: translateY(10px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n }\n .teacher-annotation {\n animation: fadeIn 0.3s ease;\n }\n\n /* Responsive */\n @media (max-width: 640px) {\n .widget-container {\n padding: 10px;\n padding-bottom: 24px;\n }\n .widget-header {\n padding: 18px 16px;\n border-radius: 10px;\n }\n .widget-header h1 {\n font-size: 1.3rem;\n }\n .widget-header .description {\n font-size: 0.85rem;\n }\n .CodeMirror {\n min-height: 220px;\n font-size: 13px;\n }\n .CodeMirror-scroll {\n min-height: 220px;\n }\n .btn {\n padding: 10px 14px;\n font-size: 0.82rem;\n min-height: 42px;\n flex: 1 1 auto;\n }\n .btn-row {\n gap: 8px;\n }\n #output {\n font-size: 0.78rem;\n padding: 12px 14px;\n min-height: 48px;\n max-height: 200px;\n }\n .test-item {\n padding: 8px 14px;\n font-size: 0.82rem;\n gap: 8px;\n }\n .hint-item {\n padding: 10px 14px;\n font-size: 0.8rem;\n }\n .solution-panel pre {\n font-size: 0.72rem;\n padding: 12px 14px;\n }\n .tests-panel .tests-header {\n padding: 10px 14px;\n font-size: 0.82rem;\n }\n }\n @media (max-width: 380px) {\n .btn-row {\n flex-direction: column;\n }\n .btn {\n width: 100%;\n }\n }\n </style>\n\n<link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css\">\n<script src=\"https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js\"></script>\n<script src=\"https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js\"></script>\n<script>\ndocument.addEventListener(\"DOMContentLoaded\", function() {\n const katexOptions = {\n delimiters: [\n {left: '\\\\[', right: '\\\\]', display: true},\n {left: '\\\\(', right: '\\\\)', display: false},\n {left: '$$', right: '$$', display: true},\n {left: '$', right: '$', display: false}\n ],\n throwOnError: false,\n strict: false,\n trust: true\n };\n\n let renderTimeout;\n function safeRender() {\n if (renderTimeout) clearTimeout(renderTimeout);\n renderTimeout = setTimeout(() => {\n renderMathInElement(document.body, katexOptions);\n }, 100);\n }\n\n renderMathInElement(document.body, katexOptions);\n\n const observer = new MutationObserver((mutations) => {\n let shouldRender = false;\n mutations.forEach((mutation) => {\n if (mutation.target &&\n mutation.target.className &&\n typeof mutation.target.className === 'string' &&\n mutation.target.className.includes('katex')) {\n return;\n }\n shouldRender = true;\n });\n\n if (shouldRender) {\n safeRender();\n }\n });\n\n observer.observe(document.body, {\n childList: true,\n subtree: true,\n characterData: true\n });\n\n setInterval(() => {\n const text = document.body.innerText;\n if (text.includes('\\\\(') || text.includes('$$')) {\n safeRender();\n }\n }, 2000);\n});\n</script>\n</head>\n<body>\n\n <div class=\"widget-container\" id=\"widget-root\">\n <!-- Header -->\n <header class=\"widget-header\" id=\"widget-header\">\n <h1>🐍 变量与数据类型探索</h1>\n <p class=\"description\">通过交互式代码练习,理解 Python 中的变量赋值与常见数据类型。动手输入代码,观察运行结果!</p>\n <div class=\"key-points\">\n <span class=\"key-point\">📝 变量命名与赋值</span>\n <span class=\"key-point\">🔤 字符串 str</span>\n <span class=\"key-point\">🔢 整数 int</span>\n <span class=\"key-point\">⚡ 浮点数 float</span>\n <span class=\"key-point\">✓ 布尔值 bool</span>\n <span class=\"key-point\">🔍 type() 函数</span>\n </div>\n </header>\n\n <!-- Status Bar -->\n <div id=\"status-bar\">\n <span class=\"status-dot\" id=\"status-dot\"></span>\n <span id=\"status-text\">正在加载 Python 环境...</span>\n </div>\n\n <!-- Editor -->\n <div class=\"editor-wrapper\" id=\"editor-wrapper\">\n <div class=\"editor-header\">\n <span>main.py</span>\n <span class=\"lang-badge\">Python 3</span>\n </div>\n <textarea id=\"code-input\"></textarea>\n </div>\n\n <!-- Buttons -->\n <div class=\"btn-row\" id=\"btn-row\">\n <button class=\"btn btn-primary\" id=\"run-btn\" disabled title=\"先等 Python 环境加载完成\">\n ▶ 运行代码\n </button>\n <button class=\"btn btn-secondary\" id=\"hint-btn\" disabled title=\"加载完成后可获取提示\">\n 💡 提示 (<span id=\"hint-count\">0</span>/<span id=\"hint-total\">0</span>)\n </button>\n <button class=\"btn btn-secondary\" id=\"solution-btn\" title=\"查看参考答案\">\n 📖 参考答案\n </button>\n </div>\n\n <!-- Output -->\n <div class=\"output-panel\" id=\"output-panel\">\n <div class=\"output-header\">\n <span class=\"dot\" id=\"output-dot\"></span>\n 输出\n </div>\n <div id=\"output\">代码运行结果将显示在这里...</div>\n </div>\n\n <!-- Tests -->\n <div class=\"tests-panel\" id=\"tests-panel\">\n <div class=\"tests-header\" id=\"tests-header\">\n <span>🧪 测试用例 <span class=\"tests-summary\" id=\"tests-summary\"></span></span>\n <span class=\"toggle-icon\" id=\"tests-toggle-icon\">▼</span>\n </div>\n <div id=\"test-results\">\n <div style=\"padding:16px;color:#8892a8;font-size:0.85rem;text-align:center;\">运行代码后自动检查</div>\n </div>\n </div>\n\n <!-- Hints -->\n <div class=\"hints-panel\" id=\"hints-panel\">\n <div class=\"hints-header\">💡 提示(逐步显示)</div>\n <div id=\"hints-container\">\n <div class=\"no-hints\">点击\"提示\"按钮查看提示</div>\n </div>\n </div>\n\n <!-- Solution -->\n <div class=\"solution-panel\" id=\"solution\" style=\"display:none;\">\n <div class=\"solution-header\">📖 参考答案</div>\n <pre id=\"solution-code\"></pre>\n </div>\n\n <!-- Widget Config -->\n <details class=\"widget-config-wrapper\" id=\"widget-config-wrapper\">\n <summary>📋 查看组件配置 (JSON)</summary>\n <pre id=\"widget-config-display\"></pre>\n </details>\n </div>\n\n <!-- Embedded Widget Configuration JSON -->\n <script type=\"application/json\" id=\"widget-config\">\n {\n \"type\": \"code\",\n \"language\": \"python\",\n \"description\": \"通过交互式代码练习理解变量赋值与常见数据类型。掌握变量命名、字符串/整数/浮点数/布尔值,以及 type() 函数的使用。\",\n \"starterCode\": \"# === 变量与数据类型探索 ===\\n# 请完成以下练习:\\n\\n# 1. 创建一个字符串变量 name,值为你的名字\\n# 例如:name = \\\"小明\\\"\\n\\n# 2. 创建一个整数变量 age,值为你的年龄\\n# 例如:age = 15\\n\\n# 3. 创建一个浮点数变量 height,值为你的身高(米)\\n# 例如:height = 1.75\\n\\n# 4. 创建一个布尔值变量 is_student,表示你是否是学生\\n# 例如:is_student = True\\n\\n# 5. 使用 type() 函数查看这些变量的类型\\n# 例如:print(type(name))\\n\\n# 6. 尝试修改其中一个变量的值,并打印修改前后的值\\n\\n# 请在下方编写你的代码:\\n\",\n \"testCases\": [\n { \"id\": \"t1\", \"input\": \"type(name).__name__\", \"expected\": \"str\", \"description\": \"变量 name 是字符串 (str) 类型\" },\n { \"id\": \"t2\", \"input\": \"type(age).__name__\", \"expected\": \"int\", \"description\": \"变量 age 是整数 (int) 类型\" },\n { \"id\": \"t3\", \"input\": \"type(height).__name__\", \"expected\": \"float\", \"description\": \"变量 height 是浮点数 (float) 类型\" },\n { \"id\": \"t4\", \"input\": \"type(is_student).__name__\", \"expected\": \"bool\", \"description\": \"变量 is_student 是布尔 (bool) 类型\" },\n { \"id\": \"t5\", \"input\": \"__code_contains_type__\", \"expected\": \"True\", \"description\": \"使用了 type() 函数查看类型\" }\n ],\n \"hints\": [\n \"变量赋值使用等号 =,左边是变量名,右边是值。例如:name = '小明'\",\n \"Python 中基本数据类型:字符串用引号 '...' 或 \\\"...\\\" 括起来;整数直接写数字如 15;浮点数带小数点如 1.75;布尔值是 True 或 False(注意首字母大写)\",\n \"使用 type() 函数可以查看变量类型。例如:print(type(name)) 会输出 <class 'str'>,表示字符串类型\",\n \"修改变量值只需重新赋值。例如:age = 16 会将 age 的值从 15 改为 16。修改后打印可以观察变化\"\n ],\n \"solution\": \"# 创建不同类型的变量\\nname = \\\"小明\\\" # 字符串 (str)\\nage = 15 # 整数 (int)\\nheight = 1.75 # 浮点数 (float)\\nis_student = True # 布尔值 (bool)\\n\\n# 使用 type() 查看类型\\nprint(type(name))\\nprint(type(age))\\nprint(type(height))\\nprint(type(is_student))\\n\\n# 修改变量的值\\nage = 16\\nprint(f\\\"修改后的年龄: {age}\\\")\"\n }\n </script>\n\n <!-- CodeMirror 5 -->\n <script src=\"https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.js\"></script>\n <script src=\"https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/python/python.min.js\"></script>\n <script src=\"https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/addon/edit/matchbrackets.min.js\"></script>\n <script src=\"https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/addon/edit/closebrackets.min.js\"></script>\n\n <!-- Pyodide -->\n <script src=\"https://cdn.jsdelivr.net/pyodide/v0.26.4/full/pyodide.js\"></script>\n\n <script>\n (function() {\n 'use strict';\n\n // ===== Load Widget Config =====\n const configEl = document.getElementById('widget-config');\n const WIDGET_CONFIG = JSON.parse(configEl.textContent);\n\n // Display the config JSON\n document.getElementById('widget-config-display').textContent = JSON.stringify(WIDGET_CONFIG, null, 2);\n\n // ===== DOM References =====\n const runBtn = document.getElementById('run-btn');\n const hintBtn = document.getElementById('hint-btn');\n const solutionBtn = document.getElementById('solution-btn');\n const outputEl = document.getElementById('output');\n const outputDot = document.getElementById('output-dot');\n const statusText = document.getElementById('status-text');\n const statusDot = document.getElementById('status-dot');\n const testResultsEl = document.getElementById('test-results');\n const testsHeader = document.getElementById('tests-header');\n const testsToggleIcon = document.getElementById('tests-toggle-icon');\n const testsSummary = document.getElementById('tests-summary');\n const hintsContainer = document.getElementById('hints-container');\n const hintCountEl = document.getElementById('hint-count');\n const hintTotalEl = document.getElementById('hint-total');\n const solutionPanel = document.getElementById('solution');\n const solutionCodeEl = document.getElementById('solution-code');\n const editorWrapper = document.getElementById('editor-wrapper');\n\n // ===== State =====\n let pyodide = null;\n let pyodideReady = false;\n let pyodideError = null;\n let currentHintIndex = 0;\n let solutionVisible = false;\n let testsCollapsed = false;\n let lastTestResults = [];\n\n // ===== CodeMirror Editor =====\n const editor = CodeMirror.fromTextArea(document.getElementById('code-input'), {\n mode: 'python',\n theme: 'dracula',\n lineNumbers: true,\n matchBrackets: true,\n autoCloseBrackets: true,\n indentUnit: 4,\n tabSize: 4,\n indentWithTabs: false,\n lineWrapping: false,\n extraKeys: {\n 'Tab': function(cm) {\n if (cm.somethingSelected()) {\n cm.indentSelection('add');\n } else {\n cm.replaceSelection(' ', 'end');\n }\n },\n 'Shift-Tab': function(cm) {\n cm.indentSelection('subtract');\n },\n 'Ctrl-Enter': function() {\n if (pyodideReady) runCode();\n }\n }\n });\n\n // Set starter code\n editor.setValue(WIDGET_CONFIG.starterCode || \"# 请在此编写你的 Python 代码\\n\");\n\n // ===== Hint System =====\n const hints = WIDGET_CONFIG.hints || [];\n hintTotalEl.textContent = hints.length;\n hintCountEl.textContent = '0';\n\n function updateHintButton() {\n if (currentHintIndex < hints.length) {\n hintBtn.disabled = !pyodideReady;\n } else {\n hintBtn.disabled = true;\n }\n hintCountEl.textContent = currentHintIndex;\n }\n\n function showNextHint() {\n if (currentHintIndex >= hints.length) {\n hintBtn.disabled = true;\n return;\n }\n const hintText = hints[currentHintIndex];\n const hintDiv = document.createElement('div');\n hintDiv.className = 'hint-item';\n hintDiv.id = 'hint-' + (currentHintIndex + 1);\n hintDiv.innerHTML = '<span class=\"hint-label\">提示 ' + (currentHintIndex + 1) + ' / ' + hints\n .length + '</span>' + hintText;\n hintsContainer.appendChild(hintDiv);\n // Remove the \"no hints\" placeholder if it exists\n const noHints = hintsContainer.querySelector('.no-hints');\n if (noHints) noHints.remove();\n currentHintIndex++;\n updateHintButton();\n }\n\n hintBtn.addEventListener('click', showNextHint);\n\n // ===== Solution System =====\n solutionCodeEl.textContent = WIDGET_CONFIG.solution || '# 暂无参考答案';\n\n function toggleSolution() {\n solutionVisible = !solutionVisible;\n solutionPanel.style.display = solutionVisible ? 'block' : 'none';\n solutionBtn.textContent = solutionVisible ? '🙈 隐藏答案' : '📖 参考答案';\n if (solutionVisible) {\n solutionPanel.style.animation = 'fadeIn 0.3s ease';\n }\n }\n\n solutionBtn.addEventListener('click', toggleSolution);\n\n // ===== Tests Panel Collapse =====\n testsHeader.addEventListener('click', function() {\n testsCollapsed = !testsCollapsed;\n if (testsCollapsed) {\n testResultsEl.classList.add('collapsed');\n testsToggleIcon.classList.add('collapsed');\n } else {\n testResultsEl.classList.remove('collapsed');\n testsToggleIcon.classList.remove('collapsed');\n }\n });\n\n // ===== Pyodide Initialization =====\n async function initPyodide() {\n try {\n statusText.textContent = '正在加载 Python 环境(首次加载可能需要 10-30 秒)...';\n statusDot.className = 'status-dot';\n\n pyodide = await loadPyodide();\n\n pyodideReady = true;\n pyodideError = null;\n\n statusText.textContent = 'Python 环境已就绪!可以开始编写代码。';\n statusDot.className = 'status-dot ready';\n runBtn.disabled = false;\n updateHintButton();\n\n // Auto-run starter code? No, let user click run.\n\n } catch (e) {\n pyodideReady = false;\n pyodideError = e;\n statusText.textContent = 'Python 环境加载失败:' + e.message;\n statusDot.className = 'status-dot error';\n runBtn.disabled = true;\n hintBtn.disabled = true;\n outputEl.innerHTML = '<span class=\"error-text\">❌ Python 环境加载失败:' + e.message +\n '</span>\\n请检查网络连接后刷新页面重试。';\n outputDot.className = 'error';\n }\n }\n\n // ===== Code Execution =====\n async function runCode() {\n if (!pyodideReady || !pyodide) {\n alert('Python 环境尚未就绪,请稍候...');\n return;\n }\n\n const code = editor.getValue().trim();\n if (!code) {\n outputEl.innerHTML = '<span style=\"color:#f59e0b;\">⚠️ 请先在上方编辑器中编写代码,然后点击\"运行代码\"。</span>';\n outputDot.className = '';\n return;\n }\n\n // Disable run button during execution\n runBtn.disabled = true;\n runBtn.textContent = '⏳ 运行中...';\n outputDot.className = '';\n\n try {\n // CRITICAL: Must import BOTH sys AND io for stdout capture\n await pyodide.runPythonAsync(`\nimport sys\nimport io\nsys.stdout = io.StringIO()\n`);\n\n // Run user code\n await pyodide.runPythonAsync(code);\n\n // Retrieve output\n const output = pyodide.runPython('sys.stdout.getvalue()');\n outputEl.textContent = output || '(代码运行成功,但没有打印输出)';\n outputDot.className = 'success';\n\n // Evaluate test cases\n evaluateTests(code);\n\n } catch (e) {\n const errorMsg = e.message || String(e);\n outputEl.innerHTML = '<span class=\"error-text\">❌ 出错了:' + escapeHtml(errorMsg) + '</span>';\n outputDot.className = 'error';\n // Mark tests as failed/error\n evaluateTests(code, true);\n } finally {\n runBtn.disabled = false;\n runBtn.textContent = '▶ 运行代码';\n }\n }\n\n function escapeHtml(str) {\n const div = document.createElement('div');\n div.textContent = str;\n return div.innerHTML;\n }\n\n function evaluateTests(userCode, hadError) {\n const testCases = WIDGET_CONFIG.testCases || [];\n lastTestResults = [];\n\n if (testCases.length === 0) {\n testResultsEl.innerHTML =\n '<div style=\"padding:16px;color:#8892a8;font-size:0.85rem;text-align:center;\">没有配置测试用例</div>';\n testsSummary.textContent = '';\n return;\n }\n\n let passCount = 0;\n let failCount = 0;\n\n let resultsHtml = '';\n\n for (const test of testCases) {\n let passed = false;\n let actualValue = '';\n let errorInfo = '';\n\n if (hadError) {\n passed = false;\n actualValue = '代码执行出错,跳过测试';\n errorInfo = '请先修复代码错误';\n } else if (test.input === '__code_contains_type__') {\n // Special test: check if user code contains \"type(\"\n passed = userCode.includes('type(');\n actualValue = passed ? '已使用 type() 函数' : '未找到 type() 函数调用';\n } else {\n try {\n const result = pyodide.runPython(test.input);\n const resultStr = String(result);\n const expectedStr = String(test.expected);\n passed = resultStr === expectedStr;\n actualValue = resultStr;\n if (!passed) {\n errorInfo = '期望 ' + expectedStr + ',实际 ' + resultStr;\n }\n } catch (evalErr) {\n passed = false;\n actualValue = '未定义或检查失败';\n errorInfo = evalErr.message || String(evalErr);\n }\n }\n\n if (passed) {\n passCount++;\n } else {\n failCount++;\n }\n\n lastTestResults.push({ id: test.id, passed, actualValue, errorInfo, description: test\n .description });\n\n const iconClass = passed ? 'pass' : 'fail';\n const iconSymbol = passed ? '✓' : '✗';\n const detailHtml = errorInfo ?\n '<div class=\"test-detail\">' + escapeHtml(errorInfo) + '</div>' :\n '<div class=\"test-detail\">实际值: ' + escapeHtml(actualValue) + '</div>';\n\n resultsHtml +=\n '<div class=\"test-item\" id=\"test-' + escapeHtml(test.id) + '\">' +\n '<div class=\"test-icon ' + iconClass + '\">' + iconSymbol + '</div>' +\n '<div class=\"test-body\">' +\n '<div class=\"test-desc\">' + escapeHtml(test.description) + '</div>' +\n detailHtml +\n '</div>' +\n '</div>';\n }\n\n testResultsEl.innerHTML = resultsHtml;\n testsSummary.textContent = '(' + passCount + ' 通过 / ' + failCount + ' 失败)';\n }\n\n // ===== Run Button =====\n runBtn.addEventListener('click', runCode);\n\n // ===== Keyboard shortcut =====\n document.addEventListener('keydown', function(e) {\n if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {\n e.preventDefault();\n if (pyodideReady) runCode();\n }\n });\n\n // ===== postMessage Listener (REQUIRED) =====\n window.addEventListener('message', function(event) {\n const { type, target, state, content } = event.data;\n\n switch (type) {\n case 'SET_WIDGET_STATE':\n // e.g. { code: \"...\", run: true } — set editor contents, optionally run.\n if (state && typeof state.code === 'string') {\n if (typeof editor !== 'undefined' && typeof editor.setValue === 'function') {\n editor.setValue(state.code);\n } else {\n const ta = document.getElementById('code-input');\n if (ta) ta.value = state.code;\n }\n }\n if (state && state.run && typeof runCode === 'function') {\n runCode();\n }\n break;\n\n case 'HIGHLIGHT_ELEMENT':\n const highlightEl = document.querySelector(target);\n if (highlightEl) {\n highlightEl.style.outline = '3px solid rgba(139, 92, 246, 0.8)';\n highlightEl.style.outlineOffset = '4px';\n highlightEl.style.animation = 'pulse-highlight 2s infinite';\n setTimeout(() => {\n highlightEl.style.outline = '';\n highlightEl.style.animation = '';\n }, 3000);\n }\n break;\n\n case 'ANNOTATE_ELEMENT':\n const annotateEl = document.querySelector(target);\n if (annotateEl && content) {\n const rect = annotateEl.getBoundingClientRect();\n const tooltip = document.createElement('div');\n tooltip.className = 'teacher-annotation';\n tooltip.style.cssText = 'position:fixed; top:' + (rect.top - 40) +\n 'px; left:' + rect.left +\n 'px; background:rgba(139,92,246,0.95); color:white; padding:8px 12px; border-radius:8px; font-size:14px; z-index:1000; animation:fadeIn 0.3s;';\n tooltip.textContent = content;\n document.body.appendChild(tooltip);\n setTimeout(() => tooltip.remove(), 4000);\n }\n break;\n\n case 'REVEAL_ELEMENT':\n const revealEl = document.querySelector(target);\n if (revealEl) {\n revealEl.style.display = '';\n revealEl.style.opacity = '1';\n // If revealing solution, update state\n if (target === '#solution' || revealEl.id === 'solution') {\n solutionVisible = true;\n solutionBtn.textContent = '🙈 隐藏答案';\n }\n }\n break;\n }\n });\n\n // Add animation keyframes (required for postMessage)\n const style = document.createElement('style');\n style.textContent =\n '@keyframes pulse-highlight { 0%, 100% { outline-color: rgba(139, 92, 246, 0.8); } 50% { outline-color: rgba(139, 92, 246, 0.4); } } @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }';\n document.head.appendChild(style);\n\n // ===== Initialize =====\n updateHintButton();\n initPyodide();\n\n // ===== Responsive Editor Height =====\n function adjustEditorHeight() {\n const vh = window.innerHeight;\n const editorEl = editor.getWrapperElement();\n if (vh < 600) {\n editorEl.style.minHeight = '200px';\n } else if (vh < 800) {\n editorEl.style.minHeight = '260px';\n } else {\n editorEl.style.minHeight = '300px';\n }\n // Also set the CodeMirror scroll area\n const scrollEl = editor.getScrollerElement();\n if (scrollEl) {\n const minH = vh < 600 ? '200px' : vh < 800 ? '260px' : '300px';\n scrollEl.style.minHeight = minH;\n }\n }\n\n adjustEditorHeight();\n window.addEventListener('resize', adjustEditorHeight);\n\n // ===== Expose functions for external use =====\n window.__widget = {\n runCode: runCode,\n showNextHint: showNextHint,\n toggleSolution: toggleSolution,\n editor: editor,\n pyodide: pyodide,\n getTestResults: function() { return lastTestResults; },\n getConfig: function() { return WIDGET_CONFIG; }\n };\n\n })();\n </script>\n</body>\n</html>",
|
||
"widgetType": "code",
|
||
"widgetConfig": {
|
||
"type": "code",
|
||
"language": "python",
|
||
"description": "通过交互式代码练习理解变量赋值与常见数据类型。掌握变量命名、字符串/整数/浮点数/布尔值,以及 type() 函数的使用。",
|
||
"starterCode": "# === 变量与数据类型探索 ===\n# 请完成以下练习:\n\n# 1. 创建一个字符串变量 name,值为你的名字\n# 例如:name = \"小明\"\n\n# 2. 创建一个整数变量 age,值为你的年龄\n# 例如:age = 15\n\n# 3. 创建一个浮点数变量 height,值为你的身高(米)\n# 例如:height = 1.75\n\n# 4. 创建一个布尔值变量 is_student,表示你是否是学生\n# 例如:is_student = True\n\n# 5. 使用 type() 函数查看这些变量的类型\n# 例如:print(type(name))\n\n# 6. 尝试修改其中一个变量的值,并打印修改前后的值\n\n# 请在下方编写你的代码:\n",
|
||
"testCases": [
|
||
{
|
||
"id": "t1",
|
||
"input": "type(name).__name__",
|
||
"expected": "str",
|
||
"description": "变量 name 是字符串 (str) 类型"
|
||
},
|
||
{
|
||
"id": "t2",
|
||
"input": "type(age).__name__",
|
||
"expected": "int",
|
||
"description": "变量 age 是整数 (int) 类型"
|
||
},
|
||
{
|
||
"id": "t3",
|
||
"input": "type(height).__name__",
|
||
"expected": "float",
|
||
"description": "变量 height 是浮点数 (float) 类型"
|
||
},
|
||
{
|
||
"id": "t4",
|
||
"input": "type(is_student).__name__",
|
||
"expected": "bool",
|
||
"description": "变量 is_student 是布尔 (bool) 类型"
|
||
},
|
||
{
|
||
"id": "t5",
|
||
"input": "__code_contains_type__",
|
||
"expected": "True",
|
||
"description": "使用了 type() 函数查看类型"
|
||
}
|
||
],
|
||
"hints": [
|
||
"变量赋值使用等号 =,左边是变量名,右边是值。例如:name = '小明'",
|
||
"Python 中基本数据类型:字符串用引号 '...' 或 \"...\" 括起来;整数直接写数字如 15;浮点数带小数点如 1.75;布尔值是 True 或 False(注意首字母大写)",
|
||
"使用 type() 函数可以查看变量类型。例如:print(type(name)) 会输出 <class 'str'>,表示字符串类型",
|
||
"修改变量值只需重新赋值。例如:age = 16 会将 age 的值从 15 改为 16。修改后打印可以观察变化"
|
||
],
|
||
"solution": "# 创建不同类型的变量\nname = \"小明\" # 字符串 (str)\nage = 15 # 整数 (int)\nheight = 1.75 # 浮点数 (float)\nis_student = True # 布尔值 (bool)\n\n# 使用 type() 查看类型\nprint(type(name))\nprint(type(age))\nprint(type(height))\nprint(type(is_student))\n\n# 修改变量的值\nage = 16\nprint(f\"修改后的年龄: {age}\")"
|
||
}
|
||
}
|
||
},
|
||
{
|
||
"id": "scene_CFJoA09QKZ",
|
||
"stageId": "DZynj92UuU",
|
||
"title": "条件判断:if/elif/else",
|
||
"order": 4,
|
||
"actions": [
|
||
{
|
||
"id": "action_DzeZjLaG",
|
||
"type": "spotlight",
|
||
"elementId": "text_6oz4loS5"
|
||
},
|
||
{
|
||
"id": "action_xMld6KcY",
|
||
"type": "speech",
|
||
"text": "同学们,我们来看 Python 中的条件判断。简单说,就是让程序根据不同的情况,做出不同的决定。"
|
||
},
|
||
{
|
||
"id": "action_5hwJzukL",
|
||
"type": "spotlight",
|
||
"elementId": "text_j9aDmPG7"
|
||
},
|
||
{
|
||
"id": "action_JOL3tmIn",
|
||
"type": "speech",
|
||
"text": "先来看比较运算符,比如大于、小于、等于、不等于。它们是条件判断的基础,用来比较两个值的大小或是否相等。"
|
||
},
|
||
{
|
||
"id": "action_dCmpg8AA",
|
||
"type": "spotlight",
|
||
"elementId": "text_UrNhk5YC"
|
||
},
|
||
{
|
||
"id": "action_m8a-LRpe",
|
||
"type": "speech",
|
||
"text": "接着看条件语法:if 后面写条件,条件成立就执行下面的代码块;不成立再看 elif 做进一步判断;最后 else 用来兜底执行。"
|
||
},
|
||
{
|
||
"id": "action_zo9N1k_A",
|
||
"type": "spotlight",
|
||
"elementId": "text_F3BTEsGr"
|
||
},
|
||
{
|
||
"id": "action_Sg5ltw7e",
|
||
"type": "speech",
|
||
"text": "这里要特别注意缩进规则:冒号之后要换行并缩进,同一个代码块的缩进必须保持一致,推荐使用 4 个空格。"
|
||
},
|
||
{
|
||
"id": "action_YZATKLOr",
|
||
"type": "spotlight",
|
||
"elementId": "text_SONpzAAQ"
|
||
},
|
||
{
|
||
"id": "action_Gao5qJrN",
|
||
"type": "speech",
|
||
"text": "最后看这个成绩等级判断的例子:score 等于 85,先判断是否大于等于 90,不成立;再判断是否大于等于 80,成立了,所以 grade 就是 B。这样分支逻辑就很清楚了。"
|
||
}
|
||
],
|
||
"outlineId": "scene_4",
|
||
"createdAt": 1786855608706,
|
||
"updatedAt": 1786855608706,
|
||
"type": "slide",
|
||
"content": {
|
||
"type": "slide",
|
||
"canvas": {
|
||
"id": "AYV9PCKePYILsXx4K2swr",
|
||
"viewportSize": 1000,
|
||
"viewportRatio": 0.5625,
|
||
"theme": {
|
||
"backgroundColor": "#ffffff",
|
||
"themeColors": [
|
||
"#5b9bd5",
|
||
"#ed7d31",
|
||
"#a5a5a5",
|
||
"#ffc000",
|
||
"#4472c4"
|
||
],
|
||
"fontColor": "#333333",
|
||
"fontName": "Microsoft YaHei",
|
||
"outline": {
|
||
"color": "#d14424",
|
||
"width": 2,
|
||
"style": "solid"
|
||
},
|
||
"shadow": {
|
||
"h": 0,
|
||
"v": 0,
|
||
"blur": 10,
|
||
"color": "#000000"
|
||
}
|
||
},
|
||
"elements": [
|
||
{
|
||
"id": "text_6oz4loS5",
|
||
"type": "text",
|
||
"left": 60,
|
||
"top": 50,
|
||
"width": 880,
|
||
"height": 70,
|
||
"content": "<p style='font-size:32px;'><strong>条件判断:if / elif / else</strong></p>",
|
||
"defaultFontName": "Microsoft YaHei",
|
||
"defaultColor": "#333333",
|
||
"rotate": 0
|
||
},
|
||
{
|
||
"id": "shape_-JfvAZI8",
|
||
"type": "shape",
|
||
"left": 70,
|
||
"top": 128,
|
||
"width": 860,
|
||
"height": 3,
|
||
"path": "M 0 0 L 1 0 L 1 1 L 0 1 Z",
|
||
"viewBox": [
|
||
1,
|
||
1
|
||
],
|
||
"fill": "#5b9bd5",
|
||
"fixedRatio": false,
|
||
"rotate": 0
|
||
},
|
||
{
|
||
"id": "shape_G_weEpFV",
|
||
"type": "shape",
|
||
"left": 60,
|
||
"top": 150,
|
||
"width": 280,
|
||
"height": 170,
|
||
"path": "M 0 0 L 1 0 L 1 1 L 0 1 Z",
|
||
"viewBox": [
|
||
1,
|
||
1
|
||
],
|
||
"fill": "#e8f4fd",
|
||
"fixedRatio": false,
|
||
"rotate": 0
|
||
},
|
||
{
|
||
"id": "text_j9aDmPG7",
|
||
"type": "text",
|
||
"left": 80,
|
||
"top": 164,
|
||
"width": 240,
|
||
"height": 142,
|
||
"content": "<p style='font-size:20px; color:#1f4e79;'><strong>1. 比较运算符</strong></p><p style='font-size:16px; color:#333333;'><strong>></strong> 大于 <strong><</strong> 小于</p><p style='font-size:16px; color:#333333;'><strong>==</strong> 等于 <strong>!=</strong> 不等于</p><p style='font-size:16px; color:#333333;'><strong>>=</strong> 大等于 <strong><=</strong> 小等于</p>",
|
||
"defaultFontName": "Microsoft YaHei",
|
||
"defaultColor": "#333333",
|
||
"rotate": 0
|
||
},
|
||
{
|
||
"id": "shape_Lg3MT9dD",
|
||
"type": "shape",
|
||
"left": 360,
|
||
"top": 150,
|
||
"width": 280,
|
||
"height": 170,
|
||
"path": "M 0 0 L 1 0 L 1 1 L 0 1 Z",
|
||
"viewBox": [
|
||
1,
|
||
1
|
||
],
|
||
"fill": "#e9f9e9",
|
||
"fixedRatio": false,
|
||
"rotate": 0
|
||
},
|
||
{
|
||
"id": "text_UrNhk5YC",
|
||
"type": "text",
|
||
"left": 380,
|
||
"top": 164,
|
||
"width": 240,
|
||
"height": 142,
|
||
"content": "<p style='font-size:20px; color:#166534;'><strong>2. 条件语法</strong></p><p style='font-size:16px; color:#333333;'><strong>if</strong> 条件: 执行块</p><p style='font-size:16px; color:#333333;'><strong>elif</strong> 条件: 再次判断</p><p style='font-size:16px; color:#333333;'><strong>else</strong> 兜底执行</p>",
|
||
"defaultFontName": "Microsoft YaHei",
|
||
"defaultColor": "#333333",
|
||
"rotate": 0
|
||
},
|
||
{
|
||
"id": "shape_mw6McQbH",
|
||
"type": "shape",
|
||
"left": 660,
|
||
"top": 150,
|
||
"width": 280,
|
||
"height": 170,
|
||
"path": "M 0 0 L 1 0 L 1 1 L 0 1 Z",
|
||
"viewBox": [
|
||
1,
|
||
1
|
||
],
|
||
"fill": "#fff4e0",
|
||
"fixedRatio": false,
|
||
"rotate": 0
|
||
},
|
||
{
|
||
"id": "text_F3BTEsGr",
|
||
"type": "text",
|
||
"left": 680,
|
||
"top": 164,
|
||
"width": 240,
|
||
"height": 142,
|
||
"content": "<p style='font-size:20px; color:#92400e;'><strong>3. 缩进规则</strong></p><p style='font-size:16px; color:#333333;'>冒号后换行缩进</p><p style='font-size:16px; color:#333333;'>同块缩进一致</p><p style='font-size:16px; color:#333333;'>推荐 4 个空格</p>",
|
||
"defaultFontName": "Microsoft YaHei",
|
||
"defaultColor": "#333333",
|
||
"rotate": 0
|
||
},
|
||
{
|
||
"id": "shape_JN99a8Af",
|
||
"type": "shape",
|
||
"left": 60,
|
||
"top": 340,
|
||
"width": 880,
|
||
"height": 170,
|
||
"path": "M 0 0 L 1 0 L 1 1 L 0 1 Z",
|
||
"viewBox": [
|
||
1,
|
||
1
|
||
],
|
||
"fill": "#fdf6e3",
|
||
"fixedRatio": false,
|
||
"rotate": 0
|
||
},
|
||
{
|
||
"id": "text_SONpzAAQ",
|
||
"type": "text",
|
||
"left": 80,
|
||
"top": 347,
|
||
"width": 840,
|
||
"height": 157,
|
||
"content": "<p style='font-size:18px; color:#333333;'><strong>示例:成绩等级判断</strong></p><p style='font-size:18px; color:#333333; font-family: Courier New, monospace;'>score = 85</p><p style='font-size:18px; color:#333333; font-family: Courier New, monospace;'>if score >= 90: grade = "A"</p><p style='font-size:18px; color:#333333; font-family: Courier New, monospace;'>elif score >= 60: grade = "B"</p><p style='font-size:18px; color:#333333; font-family: Courier New, monospace;'>else: grade = "C"</p>",
|
||
"defaultFontName": "Microsoft YaHei",
|
||
"defaultColor": "#333333",
|
||
"rotate": 0
|
||
}
|
||
],
|
||
"background": {
|
||
"type": "solid",
|
||
"color": "#ffffff"
|
||
}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
"id": "scene_ENRWe7NiKc",
|
||
"stageId": "DZynj92UuU",
|
||
"title": "循环:for 与 while",
|
||
"order": 5,
|
||
"actions": [
|
||
{
|
||
"id": "action__v_wtb8X",
|
||
"type": "spotlight",
|
||
"elementId": "text_weL70gS3"
|
||
},
|
||
{
|
||
"id": "action_dUFPj2M_",
|
||
"type": "speech",
|
||
"text": "接下来我们来看循环结构——for 与 while。循环可以让我们重复执行同一段代码,是编程中非常重要的工具。"
|
||
},
|
||
{
|
||
"id": "action_u-SDrWHk",
|
||
"type": "spotlight",
|
||
"elementId": "text_bJz63DPQ"
|
||
},
|
||
{
|
||
"id": "action_tRaNhwqR",
|
||
"type": "speech",
|
||
"text": "先看 for 循环。它适用于固定次数或者遍历一个范围的情况,写法是 for i in range(n):。其中 i 会从 0 开始,依次取到 n-1。"
|
||
},
|
||
{
|
||
"id": "action_8_E47Mjf",
|
||
"type": "spotlight",
|
||
"elementId": "text_YFrKWduW"
|
||
},
|
||
{
|
||
"id": "action__I-P_WAU",
|
||
"type": "speech",
|
||
"text": "再看 while 循环。它是在条件为真时重复执行,适合事先不知道要循环多少次、但知道结束条件的情况。写法是 while 条件:,下面缩进的代码就是循环体。"
|
||
},
|
||
{
|
||
"id": "action_pAmJQ4oM",
|
||
"type": "spotlight",
|
||
"elementId": "text__cJwQ97C"
|
||
},
|
||
{
|
||
"id": "action_JqlmCscI",
|
||
"type": "speech",
|
||
"text": "循环中还有两个关键字需要掌握:break 用来提前结束整个循环;continue 用来跳过本次循环,直接进入下一轮。它们常常配合条件判断使用。"
|
||
},
|
||
{
|
||
"id": "action_Cz_3TGsG",
|
||
"type": "spotlight",
|
||
"elementId": "text_yEOWBMRo"
|
||
},
|
||
{
|
||
"id": "action_OwabOoiu",
|
||
"type": "speech",
|
||
"text": "最后看一个示例:计算 1 到 100 的和。用 for 循环累加,最终结果是 5050。大家可以自己动手写一写,感受循环带来的便利。"
|
||
}
|
||
],
|
||
"outlineId": "scene_5",
|
||
"createdAt": 1786855734450,
|
||
"updatedAt": 1786855734450,
|
||
"type": "slide",
|
||
"content": {
|
||
"type": "slide",
|
||
"canvas": {
|
||
"id": "3F6DuHqvanDwQLqYuBM-T",
|
||
"viewportSize": 1000,
|
||
"viewportRatio": 0.5625,
|
||
"theme": {
|
||
"backgroundColor": "#ffffff",
|
||
"themeColors": [
|
||
"#5b9bd5",
|
||
"#ed7d31",
|
||
"#a5a5a5",
|
||
"#ffc000",
|
||
"#4472c4"
|
||
],
|
||
"fontColor": "#333333",
|
||
"fontName": "Microsoft YaHei",
|
||
"outline": {
|
||
"color": "#d14424",
|
||
"width": 2,
|
||
"style": "solid"
|
||
},
|
||
"shadow": {
|
||
"h": 0,
|
||
"v": 0,
|
||
"blur": 10,
|
||
"color": "#000000"
|
||
}
|
||
},
|
||
"elements": [
|
||
{
|
||
"id": "text_weL70gS3",
|
||
"type": "text",
|
||
"left": 60,
|
||
"top": 60,
|
||
"width": 880,
|
||
"height": 76,
|
||
"content": "<p style=\"font-size:36px;\"><strong>循环:for 与 while</strong></p>",
|
||
"defaultFontName": "Microsoft YaHei",
|
||
"defaultColor": "#333333",
|
||
"rotate": 0
|
||
},
|
||
{
|
||
"id": "shape_rPnb5fb0",
|
||
"type": "shape",
|
||
"left": 70,
|
||
"top": 142,
|
||
"width": 860,
|
||
"height": 3,
|
||
"path": "M 0 0 L 1 0 L 1 1 L 0 1 Z",
|
||
"viewBox": [
|
||
1,
|
||
1
|
||
],
|
||
"fill": "#5b9bd5",
|
||
"fixedRatio": false,
|
||
"rotate": 0
|
||
},
|
||
{
|
||
"id": "shape_Zdju2wpK",
|
||
"type": "shape",
|
||
"left": 60,
|
||
"top": 160,
|
||
"width": 430,
|
||
"height": 220,
|
||
"path": "M 0 0 L 1 0 L 1 1 L 0 1 Z",
|
||
"viewBox": [
|
||
1,
|
||
1
|
||
],
|
||
"fill": "#e8f0fe",
|
||
"fixedRatio": false,
|
||
"rotate": 0
|
||
},
|
||
{
|
||
"id": "text_bJz63DPQ",
|
||
"type": "text",
|
||
"left": 100,
|
||
"top": 197,
|
||
"width": 350,
|
||
"height": 58,
|
||
"content": "<p style=\"font-size:24px; text-align:center; color:#1e3a8a;\"><strong>for 循环</strong></p>",
|
||
"defaultFontName": "Microsoft YaHei",
|
||
"defaultColor": "#1e3a8a",
|
||
"rotate": 0
|
||
},
|
||
{
|
||
"id": "text_q05YkStM",
|
||
"type": "text",
|
||
"left": 80,
|
||
"top": 267,
|
||
"width": 390,
|
||
"height": 76,
|
||
"content": "<p style=\"font-size:18px; color:#1e3a8a;\">• 固定次数 / 遍历范围</p><p style=\"font-size:18px; color:#1e3a8a;\">• for i in range(n)</p>",
|
||
"defaultFontName": "Microsoft YaHei",
|
||
"defaultColor": "#1e3a8a",
|
||
"rotate": 0
|
||
},
|
||
{
|
||
"id": "shape_Lu21Mi07",
|
||
"type": "shape",
|
||
"left": 510,
|
||
"top": 160,
|
||
"width": 430,
|
||
"height": 220,
|
||
"path": "M 0 0 L 1 0 L 1 1 L 0 1 Z",
|
||
"viewBox": [
|
||
1,
|
||
1
|
||
],
|
||
"fill": "#d9f2e6",
|
||
"fixedRatio": false,
|
||
"rotate": 0
|
||
},
|
||
{
|
||
"id": "text_YFrKWduW",
|
||
"type": "text",
|
||
"left": 550,
|
||
"top": 197,
|
||
"width": 350,
|
||
"height": 58,
|
||
"content": "<p style=\"font-size:24px; text-align:center; color:#14532d;\"><strong>while 循环</strong></p>",
|
||
"defaultFontName": "Microsoft YaHei",
|
||
"defaultColor": "#14532d",
|
||
"rotate": 0
|
||
},
|
||
{
|
||
"id": "text_h1Yrn1t9",
|
||
"type": "text",
|
||
"left": 530,
|
||
"top": 267,
|
||
"width": 390,
|
||
"height": 76,
|
||
"content": "<p style=\"font-size:18px; color:#14532d;\">• 条件为真时重复执行</p><p style=\"font-size:18px; color:#14532d;\">• while 条件:</p>",
|
||
"defaultFontName": "Microsoft YaHei",
|
||
"defaultColor": "#14532d",
|
||
"rotate": 0
|
||
},
|
||
{
|
||
"id": "shape_pRDH2t5H",
|
||
"type": "shape",
|
||
"left": 60,
|
||
"top": 395,
|
||
"width": 430,
|
||
"height": 110,
|
||
"path": "M 0 0 L 1 0 L 1 1 L 0 1 Z",
|
||
"viewBox": [
|
||
1,
|
||
1
|
||
],
|
||
"fill": "#fff4e5",
|
||
"fixedRatio": false,
|
||
"rotate": 0
|
||
},
|
||
{
|
||
"id": "text__cJwQ97C",
|
||
"type": "text",
|
||
"left": 80,
|
||
"top": 412,
|
||
"width": 390,
|
||
"height": 76,
|
||
"content": "<p style=\"font-size:18px; color:#7c2d12;\"><strong>break</strong>:提前结束整个循环</p><p style=\"font-size:18px; color:#7c2d12;\"><strong>continue</strong>:跳过本次,继续下一轮</p>",
|
||
"defaultFontName": "Microsoft YaHei",
|
||
"defaultColor": "#7c2d12",
|
||
"rotate": 0
|
||
},
|
||
{
|
||
"id": "shape_C4Csf2Hm",
|
||
"type": "shape",
|
||
"left": 510,
|
||
"top": 395,
|
||
"width": 430,
|
||
"height": 110,
|
||
"path": "M 0 0 L 1 0 L 1 1 L 0 1 Z",
|
||
"viewBox": [
|
||
1,
|
||
1
|
||
],
|
||
"fill": "#f3e8ff",
|
||
"fixedRatio": false,
|
||
"rotate": 0
|
||
},
|
||
{
|
||
"id": "text_yEOWBMRo",
|
||
"type": "text",
|
||
"left": 530,
|
||
"top": 399,
|
||
"width": 390,
|
||
"height": 49,
|
||
"content": "<p style=\"font-size:18px; color:#4c1d95;\"><strong>示例:计算 1 到 100 之和</strong></p>",
|
||
"defaultFontName": "Microsoft YaHei",
|
||
"defaultColor": "#4c1d95",
|
||
"rotate": 0
|
||
},
|
||
{
|
||
"id": "latex_RmwCjtpY",
|
||
"type": "latex",
|
||
"left": 625,
|
||
"top": 451,
|
||
"width": 200,
|
||
"height": 50,
|
||
"latex": "\\sum_{i=1}^{100} i = 5050",
|
||
"color": "#4c1d95",
|
||
"align": "center",
|
||
"html": "<span class=\"katex-display\"><span class=\"katex\"><span class=\"katex-html\" aria-hidden=\"true\"><span class=\"base\"><span class=\"strut\" style=\"height:3.0788em;vertical-align:-1.2777em;\"></span><span class=\"mop op-limits\"><span class=\"vlist-t vlist-t2\"><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:1.8011em;\"><span style=\"top:-1.8723em;margin-left:0em;\"><span class=\"pstrut\" style=\"height:3.05em;\"></span><span class=\"sizing reset-size6 size3 mtight\"><span class=\"mord mtight\"><span class=\"mord mathnormal mtight\">i</span><span class=\"mrel mtight\">=</span><span class=\"mord mtight\">1</span></span></span></span><span style=\"top:-3.05em;\"><span class=\"pstrut\" style=\"height:3.05em;\"></span><span><span class=\"mop op-symbol large-op\">∑</span></span></span><span style=\"top:-4.3em;margin-left:0em;\"><span class=\"pstrut\" style=\"height:3.05em;\"></span><span class=\"sizing reset-size6 size3 mtight\"><span class=\"mord mtight\"><span class=\"mord mtight\">100</span></span></span></span></span><span class=\"vlist-s\"></span></span><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:1.2777em;\"><span></span></span></span></span></span><span class=\"mspace\" style=\"margin-right:0.1667em;\"></span><span class=\"mord mathnormal\">i</span><span class=\"mspace\" style=\"margin-right:0.2778em;\"></span><span class=\"mrel\">=</span><span class=\"mspace\" style=\"margin-right:0.2778em;\"></span></span><span class=\"base\"><span class=\"strut\" style=\"height:0.6444em;\"></span><span class=\"mord\">5050</span></span></span></span></span>",
|
||
"fixedRatio": true,
|
||
"rotate": 0
|
||
}
|
||
],
|
||
"background": {
|
||
"type": "solid",
|
||
"color": "#ffffff"
|
||
}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
"id": "scene_DhO9LXwi6z",
|
||
"stageId": "DZynj92UuU",
|
||
"title": "阶段测验:基础语法",
|
||
"order": 6,
|
||
"actions": [
|
||
{
|
||
"id": "action_W8D8eCSz",
|
||
"type": "speech",
|
||
"text": "同学们好!现在我们来做一个阶段小测验,看看刚才学的内容掌握得怎么样。请你独立完成每一道题,提交后我会和你一起逐题梳理,帮你把不明白的地方弄清楚。"
|
||
}
|
||
],
|
||
"outlineId": "scene_6",
|
||
"createdAt": 1786855762417,
|
||
"updatedAt": 1786855762417,
|
||
"type": "quiz",
|
||
"content": {
|
||
"type": "quiz",
|
||
"questions": [
|
||
{
|
||
"id": "q1",
|
||
"type": "single",
|
||
"question": "在 Python 中,以下哪个是正确的变量赋值语句?",
|
||
"options": [
|
||
{
|
||
"value": "A",
|
||
"label": "x = 5"
|
||
},
|
||
{
|
||
"value": "B",
|
||
"label": "int x = 5"
|
||
},
|
||
{
|
||
"value": "C",
|
||
"label": "x == 5"
|
||
},
|
||
{
|
||
"value": "D",
|
||
"label": "let x = 5"
|
||
}
|
||
],
|
||
"answer": [
|
||
"A"
|
||
],
|
||
"analysis": "Python 中变量赋值直接使用等号 =,不需要声明类型,所以 A 正确。B 是 C/C++ 风格的声明方式,C 中的 == 是比较运算符,D 是 JavaScript 的声明语法。",
|
||
"points": 10,
|
||
"hasAnswer": true
|
||
},
|
||
{
|
||
"id": "q2",
|
||
"type": "single",
|
||
"question": "在 Python 中,以下哪个代码能正确判断变量 age 是否大于等于 18?",
|
||
"options": [
|
||
{
|
||
"value": "A",
|
||
"label": "if age >= 18:"
|
||
},
|
||
{
|
||
"value": "B",
|
||
"label": "if age > 18:"
|
||
},
|
||
{
|
||
"value": "C",
|
||
"label": "if age == 18:"
|
||
},
|
||
{
|
||
"value": "D",
|
||
"label": "if age <= 18:"
|
||
}
|
||
],
|
||
"answer": [
|
||
"A"
|
||
],
|
||
"analysis": "大于等于用 >= 表示,A 正确。B 只判断大于,C 只判断相等,D 是小于等于,均不符合题意。",
|
||
"points": 10,
|
||
"hasAnswer": true
|
||
},
|
||
{
|
||
"id": "q3",
|
||
"type": "multiple",
|
||
"question": "以下哪些是 Python 中合法的循环语句?(多选)",
|
||
"options": [
|
||
{
|
||
"value": "A",
|
||
"label": "for i in range(5):"
|
||
},
|
||
{
|
||
"value": "B",
|
||
"label": "while x < 10:"
|
||
},
|
||
{
|
||
"value": "C",
|
||
"label": "loop i from 1 to 5:"
|
||
},
|
||
{
|
||
"value": "D",
|
||
"label": "repeat 5 times:"
|
||
}
|
||
],
|
||
"answer": [
|
||
"A",
|
||
"B"
|
||
],
|
||
"analysis": "Python 支持 for 循环和 while 循环,因此 A 和 B 正确。C 和 D 是其他编程语言的循环语法,Python 中不存在。",
|
||
"points": 15,
|
||
"hasAnswer": true
|
||
}
|
||
]
|
||
}
|
||
},
|
||
{
|
||
"id": "scene_ve19tbMFb3",
|
||
"stageId": "DZynj92UuU",
|
||
"title": "列表与字典",
|
||
"order": 7,
|
||
"actions": [
|
||
{
|
||
"id": "action_-l2GasN4",
|
||
"type": "spotlight",
|
||
"elementId": "text_P9KwtIrg"
|
||
},
|
||
{
|
||
"id": "action_6Zi_h0gc",
|
||
"type": "speech",
|
||
"text": "今天我们来认识 Python 中两种非常常用的数据结构——列表(List)和字典(Dictionary)。它们能帮我们同时存储和操作多个数据。"
|
||
},
|
||
{
|
||
"id": "action_xkI5XeiE",
|
||
"type": "spotlight",
|
||
"elementId": "text_AkYFuc4U"
|
||
},
|
||
{
|
||
"id": "action_17aQxn9Y",
|
||
"type": "speech",
|
||
"text": "先看列表。列表是一个有序集合,用方括号创建,可以通过索引访问元素,比如 nums[0] 获取第一个元素。"
|
||
},
|
||
{
|
||
"id": "action_5StC0IRc",
|
||
"type": "spotlight",
|
||
"elementId": "text_lNsk5VRw"
|
||
},
|
||
{
|
||
"id": "action_8A-3okeK",
|
||
"type": "speech",
|
||
"text": "列表支持 append 在末尾添加元素,remove 删除指定元素。右边代码先创建 [1,2,3],再添加 4,最后移除 2。"
|
||
},
|
||
{
|
||
"id": "action_lF2oiWcZ",
|
||
"type": "spotlight",
|
||
"elementId": "text_5NqZR0Ev"
|
||
},
|
||
{
|
||
"id": "action_5M60QVKy",
|
||
"type": "speech",
|
||
"text": "接下来看字典。字典是键值对结构,用花括号创建,通过键来访问对应的值,还可以灵活添加或修改。"
|
||
},
|
||
{
|
||
"id": "action_p2X63K9O",
|
||
"type": "spotlight",
|
||
"elementId": "text_kt2ZAsK5"
|
||
},
|
||
{
|
||
"id": "action_iNutTaAG",
|
||
"type": "speech",
|
||
"text": "这个例子用 stu 存储学生信息,设置姓名和年龄。字典特别适合保存一组相关的数据。大家动手试试吧!"
|
||
}
|
||
],
|
||
"outlineId": "scene_7",
|
||
"createdAt": 1786855907985,
|
||
"updatedAt": 1786855907985,
|
||
"type": "slide",
|
||
"content": {
|
||
"type": "slide",
|
||
"canvas": {
|
||
"id": "ZhRAx-gy-efZp84gyC_D4",
|
||
"viewportSize": 1000,
|
||
"viewportRatio": 0.5625,
|
||
"theme": {
|
||
"backgroundColor": "#ffffff",
|
||
"themeColors": [
|
||
"#5b9bd5",
|
||
"#ed7d31",
|
||
"#a5a5a5",
|
||
"#ffc000",
|
||
"#4472c4"
|
||
],
|
||
"fontColor": "#333333",
|
||
"fontName": "Microsoft YaHei",
|
||
"outline": {
|
||
"color": "#d14424",
|
||
"width": 2,
|
||
"style": "solid"
|
||
},
|
||
"shadow": {
|
||
"h": 0,
|
||
"v": 0,
|
||
"blur": 10,
|
||
"color": "#000000"
|
||
}
|
||
},
|
||
"elements": [
|
||
{
|
||
"id": "text_P9KwtIrg",
|
||
"type": "text",
|
||
"left": 60,
|
||
"top": 50,
|
||
"width": 880,
|
||
"height": 70,
|
||
"content": "<p style=\"font-size:32px; text-align:center;\"><strong>列表与字典</strong></p>",
|
||
"defaultFontName": "Microsoft YaHei",
|
||
"defaultColor": "#1F2937",
|
||
"rotate": 0
|
||
},
|
||
{
|
||
"id": "shape_kkjoBNa7",
|
||
"type": "shape",
|
||
"left": 150,
|
||
"top": 128,
|
||
"width": 700,
|
||
"height": 3,
|
||
"path": "M 0 0 L 1 0 L 1 1 L 0 1 Z",
|
||
"viewBox": [
|
||
1,
|
||
1
|
||
],
|
||
"fill": "#5B9BD5",
|
||
"fixedRatio": false,
|
||
"rotate": 0
|
||
},
|
||
{
|
||
"id": "shape_qe6oADPC",
|
||
"type": "shape",
|
||
"left": 60,
|
||
"top": 150,
|
||
"width": 430,
|
||
"height": 340,
|
||
"path": "M 0 0 L 1 0 L 1 1 L 0 1 Z",
|
||
"viewBox": [
|
||
1,
|
||
1
|
||
],
|
||
"fill": "#EEF2FF",
|
||
"fixedRatio": false,
|
||
"rotate": 0
|
||
},
|
||
{
|
||
"id": "shape_8vTkV5C_",
|
||
"type": "shape",
|
||
"left": 510,
|
||
"top": 150,
|
||
"width": 430,
|
||
"height": 340,
|
||
"path": "M 0 0 L 1 0 L 1 1 L 0 1 Z",
|
||
"viewBox": [
|
||
1,
|
||
1
|
||
],
|
||
"fill": "#ECFDF5",
|
||
"fixedRatio": false,
|
||
"rotate": 0
|
||
},
|
||
{
|
||
"id": "text_AkYFuc4U",
|
||
"type": "text",
|
||
"left": 80,
|
||
"top": 170,
|
||
"width": 390,
|
||
"height": 58,
|
||
"content": "<p style=\"font-size:24px; color:#312E81;\"><strong>列表(List)</strong></p>",
|
||
"defaultFontName": "Microsoft YaHei",
|
||
"defaultColor": "#312E81",
|
||
"rotate": 0
|
||
},
|
||
{
|
||
"id": "text_5NqZR0Ev",
|
||
"type": "text",
|
||
"left": 530,
|
||
"top": 170,
|
||
"width": 390,
|
||
"height": 58,
|
||
"content": "<p style=\"font-size:24px; color:#065F46;\"><strong>字典(Dictionary)</strong></p>",
|
||
"defaultFontName": "Microsoft YaHei",
|
||
"defaultColor": "#065F46",
|
||
"rotate": 0
|
||
},
|
||
{
|
||
"id": "text_lNsk5VRw",
|
||
"type": "text",
|
||
"left": 80,
|
||
"top": 240,
|
||
"width": 390,
|
||
"height": 118,
|
||
"content": "<p style=\"font-size:16px; color:#374151;\">• 有序集合,用 <span style=\"font-family: monospace;\">[]</span> 创建</p><p style=\"font-size:16px; color:#374151;\">• 索引访问:<span style=\"font-family: monospace;\">nums[0]</span></p><p style=\"font-size:16px; color:#374151;\">• <span style=\"font-family: monospace;\">append()</span> 末尾添加</p><p style=\"font-size:16px; color:#374151;\">• <span style=\"font-family: monospace;\">remove()</span> 删除指定值</p>",
|
||
"defaultFontName": "Microsoft YaHei",
|
||
"defaultColor": "#374151",
|
||
"rotate": 0
|
||
},
|
||
{
|
||
"id": "text_rXEiopsW",
|
||
"type": "text",
|
||
"left": 530,
|
||
"top": 240,
|
||
"width": 390,
|
||
"height": 118,
|
||
"content": "<p style=\"font-size:16px; color:#374151;\">• 键值对结构:<span style=\"font-family: monospace;\">{key: value}</span></p><p style=\"font-size:16px; color:#374151;\">• 通过键访问:<span style=\"font-family: monospace;\">stu['name']</span></p><p style=\"font-size:16px; color:#374151;\">• 可灵活添加、修改值</p><p style=\"font-size:16px; color:#374151;\">• 示例:存储学生信息</p>",
|
||
"defaultFontName": "Microsoft YaHei",
|
||
"defaultColor": "#374151",
|
||
"rotate": 0
|
||
},
|
||
{
|
||
"id": "shape__Dxr3KVF",
|
||
"type": "shape",
|
||
"left": 80,
|
||
"top": 372,
|
||
"width": 390,
|
||
"height": 94,
|
||
"path": "M 0 0 L 1 0 L 1 1 L 0 1 Z",
|
||
"viewBox": [
|
||
1,
|
||
1
|
||
],
|
||
"fill": "#1F2937",
|
||
"fixedRatio": false,
|
||
"rotate": 0
|
||
},
|
||
{
|
||
"id": "shape_2XpO-yfC",
|
||
"type": "shape",
|
||
"left": 530,
|
||
"top": 372,
|
||
"width": 390,
|
||
"height": 94,
|
||
"path": "M 0 0 L 1 0 L 1 1 L 0 1 Z",
|
||
"viewBox": [
|
||
1,
|
||
1
|
||
],
|
||
"fill": "#1F2937",
|
||
"fixedRatio": false,
|
||
"rotate": 0
|
||
},
|
||
{
|
||
"id": "text_1jRYbwkK",
|
||
"type": "text",
|
||
"left": 100,
|
||
"top": 372,
|
||
"width": 350,
|
||
"height": 94,
|
||
"content": "<p style=\"font-size:16px; font-family: monospace; color:#F3F4F6;\">nums = [1, 2, 3]</p><p style=\"font-size:16px; font-family: monospace; color:#F3F4F6;\">nums.append(4)</p><p style=\"font-size:16px; font-family: monospace; color:#F3F4F6;\">nums.remove(2)</p>",
|
||
"defaultFontName": "Microsoft YaHei",
|
||
"defaultColor": "#F3F4F6",
|
||
"rotate": 0
|
||
},
|
||
{
|
||
"id": "text_kt2ZAsK5",
|
||
"type": "text",
|
||
"left": 550,
|
||
"top": 372,
|
||
"width": 350,
|
||
"height": 94,
|
||
"content": "<p style=\"font-size:16px; font-family: monospace; color:#F3F4F6;\">stu = {}</p><p style=\"font-size:16px; font-family: monospace; color:#F3F4F6;\">stu['name'] = '小明'</p><p style=\"font-size:16px; font-family: monospace; color:#F3F4F6;\">stu['age'] = 12</p>",
|
||
"defaultFontName": "Microsoft YaHei",
|
||
"defaultColor": "#F3F4F6",
|
||
"rotate": 0
|
||
}
|
||
],
|
||
"background": {
|
||
"type": "solid",
|
||
"color": "#ffffff"
|
||
}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
"id": "scene_fYjgMlYTCD",
|
||
"stageId": "DZynj92UuU",
|
||
"title": "函数:复用代码",
|
||
"order": 8,
|
||
"actions": [
|
||
{
|
||
"id": "action_Vmu6FnaT",
|
||
"type": "speech",
|
||
"text": "今天我们来学习函数,它能让我们复用代码,让程序更简洁高效。"
|
||
},
|
||
{
|
||
"id": "action_v-9h-W8d",
|
||
"type": "spotlight",
|
||
"elementId": "text_V3a1Dc9e"
|
||
},
|
||
{
|
||
"id": "action_3FAVyO4U",
|
||
"type": "speech",
|
||
"text": "函数就像是一个可重复使用的小工具箱:我们把一段常用代码放进去,给它起个名字,需要时直接调用,不用每次重新编写。"
|
||
},
|
||
{
|
||
"id": "action_AVAcCsC7",
|
||
"type": "spotlight",
|
||
"elementId": "text_-iZv1pDj"
|
||
},
|
||
{
|
||
"id": "action_o0siZTu4",
|
||
"type": "speech",
|
||
"text": "在Python中,我们用 def 关键字来定义函数。def 后面跟函数名和括号,最后以冒号结尾,下面缩进的代码就是函数体。"
|
||
},
|
||
{
|
||
"id": "action__s0uh7dR",
|
||
"type": "spotlight",
|
||
"elementId": "text_-JJPa8Ub"
|
||
},
|
||
{
|
||
"id": "action_OPN6TQoi",
|
||
"type": "speech",
|
||
"text": "函数可以接收参数——也就是从外部传入的数据,处理后再通过 return 返回结果。同时要注意作用域:函数内部定义的变量是局部变量,只在函数内有效,外面访问不到。"
|
||
},
|
||
{
|
||
"id": "action_bpdBEwmc",
|
||
"type": "spotlight",
|
||
"elementId": "text_KxU01Iev"
|
||
},
|
||
{
|
||
"id": "action_nUSD9QmZ",
|
||
"type": "speech",
|
||
"text": "请看这个求平方的示例:def square(x) 定义了一个函数,参数是 x,返回 x 的平方。调用 square(5) 就得到 25。通过这个例子,大家能更直观地理解函数的定义、参数和返回值。"
|
||
},
|
||
{
|
||
"id": "action_JJXATX_Z",
|
||
"type": "speech",
|
||
"text": "课后请大家自己动手写几个简单的函数,比如计算两个数的和,体会一下代码复用的好处。"
|
||
}
|
||
],
|
||
"outlineId": "scene_8",
|
||
"createdAt": 1786856057496,
|
||
"updatedAt": 1786856057496,
|
||
"type": "slide",
|
||
"content": {
|
||
"type": "slide",
|
||
"canvas": {
|
||
"id": "PyQVKF3tY62AXNbcCg7rq",
|
||
"viewportSize": 1000,
|
||
"viewportRatio": 0.5625,
|
||
"theme": {
|
||
"backgroundColor": "#ffffff",
|
||
"themeColors": [
|
||
"#5b9bd5",
|
||
"#ed7d31",
|
||
"#a5a5a5",
|
||
"#ffc000",
|
||
"#4472c4"
|
||
],
|
||
"fontColor": "#333333",
|
||
"fontName": "Microsoft YaHei",
|
||
"outline": {
|
||
"color": "#d14424",
|
||
"width": 2,
|
||
"style": "solid"
|
||
},
|
||
"shadow": {
|
||
"h": 0,
|
||
"v": 0,
|
||
"blur": 10,
|
||
"color": "#000000"
|
||
}
|
||
},
|
||
"elements": [
|
||
{
|
||
"id": "text_V3a1Dc9e",
|
||
"type": "text",
|
||
"left": 60,
|
||
"top": 50,
|
||
"width": 880,
|
||
"height": 76,
|
||
"content": "<p style=\"font-size:32px;\"><strong>函数:复用代码</strong></p>",
|
||
"defaultFontName": "Microsoft YaHei",
|
||
"defaultColor": "#1f2937",
|
||
"rotate": 0
|
||
},
|
||
{
|
||
"id": "shape_2CLvMapY",
|
||
"type": "shape",
|
||
"left": 70,
|
||
"top": 136,
|
||
"width": 860,
|
||
"height": 3,
|
||
"path": "M 0 0 L 1 0 L 1 1 L 0 1 Z",
|
||
"viewBox": [
|
||
1,
|
||
1
|
||
],
|
||
"fill": "#5b9bd5",
|
||
"fixedRatio": false,
|
||
"rotate": 0
|
||
},
|
||
{
|
||
"id": "shape_yjI-sq_S",
|
||
"type": "shape",
|
||
"left": 60,
|
||
"top": 155,
|
||
"width": 280,
|
||
"height": 140,
|
||
"path": "M 0 0 L 1 0 L 1 1 L 0 1 Z",
|
||
"viewBox": [
|
||
1,
|
||
1
|
||
],
|
||
"fill": "#dbeafe",
|
||
"fixedRatio": false,
|
||
"rotate": 0
|
||
},
|
||
{
|
||
"id": "shape_dO0bhqbn",
|
||
"type": "shape",
|
||
"left": 360,
|
||
"top": 155,
|
||
"width": 280,
|
||
"height": 140,
|
||
"path": "M 0 0 L 1 0 L 1 1 L 0 1 Z",
|
||
"viewBox": [
|
||
1,
|
||
1
|
||
],
|
||
"fill": "#dcfce7",
|
||
"fixedRatio": false,
|
||
"rotate": 0
|
||
},
|
||
{
|
||
"id": "shape_G84O_a8X",
|
||
"type": "shape",
|
||
"left": 660,
|
||
"top": 155,
|
||
"width": 280,
|
||
"height": 140,
|
||
"path": "M 0 0 L 1 0 L 1 1 L 0 1 Z",
|
||
"viewBox": [
|
||
1,
|
||
1
|
||
],
|
||
"fill": "#fef3c7",
|
||
"fixedRatio": false,
|
||
"rotate": 0
|
||
},
|
||
{
|
||
"id": "text_-iZv1pDj",
|
||
"type": "text",
|
||
"left": 80,
|
||
"top": 187,
|
||
"width": 240,
|
||
"height": 76,
|
||
"content": "<p style=\"font-size:18px; text-align:center;\"><strong>def 定义函数</strong></p><p style=\"font-size:16px; text-align:center;\">用 def 关键字创建函数</p>",
|
||
"defaultFontName": "Microsoft YaHei",
|
||
"defaultColor": "#1e40af",
|
||
"rotate": 0
|
||
},
|
||
{
|
||
"id": "text_-JJPa8Ub",
|
||
"type": "text",
|
||
"left": 380,
|
||
"top": 187,
|
||
"width": 240,
|
||
"height": 76,
|
||
"content": "<p style=\"font-size:18px; text-align:center;\"><strong>参数与返回值</strong></p><p style=\"font-size:16px; text-align:center;\">传入数据 → 返回结果</p>",
|
||
"defaultFontName": "Microsoft YaHei",
|
||
"defaultColor": "#166534",
|
||
"rotate": 0
|
||
},
|
||
{
|
||
"id": "text_u4PbtfBp",
|
||
"type": "text",
|
||
"left": 680,
|
||
"top": 187,
|
||
"width": 240,
|
||
"height": 76,
|
||
"content": "<p style=\"font-size:18px; text-align:center;\"><strong>作用域基本概念</strong></p><p style=\"font-size:16px; text-align:center;\">局部变量仅在函数内有效</p>",
|
||
"defaultFontName": "Microsoft YaHei",
|
||
"defaultColor": "#92400e",
|
||
"rotate": 0
|
||
},
|
||
{
|
||
"id": "shape_ri_xpXgH",
|
||
"type": "shape",
|
||
"left": 60,
|
||
"top": 310,
|
||
"width": 880,
|
||
"height": 195,
|
||
"path": "M 0 0 L 1 0 L 1 1 L 0 1 Z",
|
||
"viewBox": [
|
||
1,
|
||
1
|
||
],
|
||
"fill": "#1e293b",
|
||
"fixedRatio": false,
|
||
"rotate": 0
|
||
},
|
||
{
|
||
"id": "shape_t08kw2Ez",
|
||
"type": "shape",
|
||
"left": 60,
|
||
"top": 310,
|
||
"width": 6,
|
||
"height": 195,
|
||
"path": "M 0 0 L 1 0 L 1 1 L 0 1 Z",
|
||
"viewBox": [
|
||
1,
|
||
1
|
||
],
|
||
"fill": "#f59e0b",
|
||
"fixedRatio": false,
|
||
"rotate": 0
|
||
},
|
||
{
|
||
"id": "text_ep8lV1v0",
|
||
"type": "text",
|
||
"left": 90,
|
||
"top": 320,
|
||
"width": 830,
|
||
"height": 49,
|
||
"content": "<p style=\"font-size:18px;\"><strong>示例:编写求平方函数</strong></p>",
|
||
"defaultFontName": "Microsoft YaHei",
|
||
"defaultColor": "#f8fafc",
|
||
"rotate": 0
|
||
},
|
||
{
|
||
"id": "text_KxU01Iev",
|
||
"type": "text",
|
||
"left": 90,
|
||
"top": 373,
|
||
"width": 830,
|
||
"height": 118,
|
||
"content": "<p style=\"font-size:16px; font-family:'Courier New', monospace; color:#e2e8f0;\">def square(x): <span style=\"color:#94a3b8;\"># 定义函数</span></p><p style=\"font-size:16px; font-family:'Courier New', monospace; color:#e2e8f0;\"> return x * x <span style=\"color:#94a3b8;\"># 返回平方值</span></p><p style=\"font-size:16px; font-family:'Courier New', monospace; color:#e2e8f0;\">result = square(5) <span style=\"color:#94a3b8;\"># 调用函数</span></p><p style=\"font-size:16px; font-family:'Courier New', monospace; color:#e2e8f0;\">print(result) <span style=\"color:#94a3b8;\"># 输出 25</span></p>",
|
||
"defaultFontName": "Microsoft YaHei",
|
||
"defaultColor": "#e2e8f0",
|
||
"rotate": 0
|
||
}
|
||
],
|
||
"background": {
|
||
"type": "solid",
|
||
"color": "#ffffff"
|
||
}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
"id": "scene_zFQYy-p9ru",
|
||
"stageId": "DZynj92UuU",
|
||
"title": "函数调试练习",
|
||
"order": 9,
|
||
"actions": [
|
||
{
|
||
"id": "action_I8ABXVys",
|
||
"type": "speech",
|
||
"text": "同学们,今天我们来练习函数调试。先看左侧的代码编辑器,里面有一个带参数的函数。"
|
||
},
|
||
{
|
||
"id": "action_qrGRKhvS",
|
||
"type": "widget_highlight",
|
||
"target": "#editor-card",
|
||
"content": "代码编辑区:函数定义和调用都在这里。"
|
||
},
|
||
{
|
||
"id": "action_UANequfS",
|
||
"type": "speech",
|
||
"text": "注意函数定义中的参数,它决定了调用时我们可以传入哪些值。运行后 print 会打印出对应的结果。"
|
||
},
|
||
{
|
||
"id": "action_lI1aWORq",
|
||
"type": "widget_highlight",
|
||
"target": "#run-btn",
|
||
"content": "点击运行按钮,查看当前代码的输出。"
|
||
},
|
||
{
|
||
"id": "action_-1d5g6cx",
|
||
"type": "speech",
|
||
"text": "先运行一次看看返回值。然后试着修改参数,比如换一个数字,再运行一次,观察输出有什么不同。"
|
||
},
|
||
{
|
||
"id": "action__b8EHgCt",
|
||
"type": "widget_annotation",
|
||
"target": "#output-card",
|
||
"content": "输出区域:运行后这里会显示结果,修改参数后重点对比变化。"
|
||
},
|
||
{
|
||
"id": "action_pyEJb1ee",
|
||
"type": "speech",
|
||
"text": "如果卡住了可以查看提示或参考代码,但先自己动手改一改,调试的过程更有收获。完成后我们来总结函数参数的作用。"
|
||
}
|
||
],
|
||
"outlineId": "scene_9",
|
||
"createdAt": 1786856200904,
|
||
"updatedAt": 1786856200904,
|
||
"type": "interactive",
|
||
"content": {
|
||
"type": "interactive",
|
||
"url": "",
|
||
"html": "<!DOCTYPE html>\n<html lang=\"zh-CN\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>函数调试练习 - Python 代码练习</title>\n\n <!-- CodeMirror 5 CDN -->\n <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.css\">\n <script src=\"https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.js\">\n </script>\n <script src=\"https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/python/python.min.js\">\n </script>\n\n <!-- Pyodide CDN -->\n <script src=\"https://cdn.jsdelivr.net/pyodide/v0.26.4/full/pyodide.js\">\n </script>\n\n <style>\n :root {\n --primary: #7c3aed;\n --primary-light: #a78bfa;\n --primary-dark: #5b21b6;\n --bg: #f8f7fc;\n --card-bg: #ffffff;\n --text: #1e1b2e;\n --text-secondary: #4a4468;\n --border: #ddd6e8;\n --success: #10b981;\n --danger: #ef4444;\n --warning: #f59e0b;\n --code-bg: #f5f3fa;\n --output-bg: #1e1b2e;\n --output-text: #d1d5db;\n --radius: 10px;\n --shadow: 0 2px 8px rgba(0, 0, 0, 0.06);\n }\n\n * {\n box-sizing: border-box;\n margin: 0;\n padding: 0;\n }\n\n body {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'PingFang SC',\n 'Hiragino Sans GB', 'Microsoft YaHei', 'Noto Sans SC', sans-serif;\n background: var(--bg);\n color: var(--text);\n line-height: 1.6;\n padding: 16px;\n min-height: 100vh;\n display: flex;\n justify-content: center;\n align-items: flex-start;\n }\n\n .container {\n width: 100%;\n max-width: 960px;\n margin: 0 auto;\n display: flex;\n flex-direction: column;\n gap: 16px;\n }\n\n .header {\n background: var(--card-bg);\n border-radius: var(--radius);\n padding: 20px 24px;\n box-shadow: var(--shadow);\n border-left: 4px solid var(--primary);\n }\n\n .header h1 {\n font-size: 1.5rem;\n color: var(--primary-dark);\n margin-bottom: 6px;\n font-weight: 700;\n }\n\n .header .description {\n color: var(--text-secondary);\n font-size: 0.95rem;\n }\n\n .header .key-points {\n margin-top: 10px;\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n }\n\n .header .key-points .badge {\n background: #ede9fe;\n color: var(--primary-dark);\n padding: 4px 12px;\n border-radius: 20px;\n font-size: 0.8rem;\n font-weight: 500;\n }\n\n .main-content {\n display: grid;\n grid-template-columns: 1fr;\n gap: 16px;\n }\n\n .editor-card {\n background: var(--card-bg);\n border-radius: var(--radius);\n box-shadow: var(--shadow);\n overflow: hidden;\n display: flex;\n flex-direction: column;\n }\n\n .toolbar {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 12px 16px;\n background: #faf9fd;\n border-bottom: 1px solid var(--border);\n flex-wrap: wrap;\n }\n\n .btn {\n padding: 8px 18px;\n border: none;\n border-radius: 8px;\n font-size: 0.9rem;\n font-weight: 600;\n cursor: pointer;\n transition: all 0.2s ease;\n white-space: nowrap;\n font-family: inherit;\n }\n\n .btn:active {\n transform: scale(0.96);\n }\n\n .btn-primary {\n background: var(--primary);\n color: #fff;\n }\n .btn-primary:hover {\n background: var(--primary-dark);\n }\n .btn-primary:disabled {\n background: #c4b5fd;\n cursor: not-allowed;\n transform: none;\n }\n\n .btn-secondary {\n background: #ede9fe;\n color: var(--primary-dark);\n }\n .btn-secondary:hover {\n background: #ddd0fe;\n }\n\n .btn-success {\n background: var(--success);\n color: #fff;\n }\n .btn-success:hover {\n background: #059669;\n }\n\n .status-badge {\n margin-left: auto;\n font-size: 0.8rem;\n color: var(--text-secondary);\n display: flex;\n align-items: center;\n gap: 6px;\n }\n\n .status-dot {\n width: 8px;\n height: 8px;\n border-radius: 50%;\n background: var(--warning);\n animation: blink 1.2s infinite;\n display: inline-block;\n }\n .status-dot.ready {\n background: var(--success);\n animation: none;\n }\n\n @keyframes blink {\n 0%,\n 100% {\n opacity: 1;\n }\n 50% {\n opacity: 0.3;\n }\n }\n\n .code-editor-host {\n position: relative;\n min-height: 220px;\n flex: 1;\n }\n\n .code-editor-host .CodeMirror {\n height: auto;\n min-height: 220px;\n font-size: 0.9rem;\n font-family: 'Fira Code', 'JetBrains Mono', 'Cascadia Code', 'Consolas', 'Monaco',\n 'Courier New', monospace;\n line-height: 1.6;\n }\n\n .code-editor-host .CodeMirror-scroll {\n min-height: 220px;\n }\n\n .output-card {\n background: var(--card-bg);\n border-radius: var(--radius);\n box-shadow: var(--shadow);\n overflow: hidden;\n display: flex;\n flex-direction: column;\n }\n\n .output-header {\n padding: 10px 16px;\n font-weight: 600;\n font-size: 0.9rem;\n color: var(--text-secondary);\n background: #faf9fd;\n border-bottom: 1px solid var(--border);\n display: flex;\n align-items: center;\n gap: 6px;\n }\n\n .output-content {\n background: var(--output-bg);\n color: var(--output-text);\n padding: 14px 16px;\n font-family: 'Fira Code', 'JetBrains Mono', 'Consolas', 'Monaco', 'Courier New',\n monospace;\n font-size: 0.85rem;\n min-height: 80px;\n white-space: pre-wrap;\n word-break: break-all;\n overflow-x: auto;\n flex: 1;\n }\n\n .output-content .error-text {\n color: #f87171;\n }\n .output-content .success-text {\n color: #6ee7b7;\n }\n\n .test-card {\n background: var(--card-bg);\n border-radius: var(--radius);\n box-shadow: var(--shadow);\n overflow: hidden;\n }\n\n .test-header {\n padding: 10px 16px;\n font-weight: 600;\n font-size: 0.9rem;\n color: var(--text-secondary);\n background: #faf9fd;\n border-bottom: 1px solid var(--border);\n display: flex;\n align-items: center;\n justify-content: space-between;\n cursor: pointer;\n user-select: none;\n }\n .test-header .toggle-icon {\n transition: transform 0.3s;\n font-size: 0.8rem;\n }\n .test-header.collapsed .toggle-icon {\n transform: rotate(-90deg);\n }\n\n .test-results {\n padding: 12px 16px;\n display: flex;\n flex-direction: column;\n gap: 8px;\n }\n\n .test-item {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 8px 12px;\n border-radius: 8px;\n font-size: 0.85rem;\n background: #faf9fd;\n border: 1px solid #eee;\n flex-wrap: wrap;\n }\n .test-item.pass {\n background: #ecfdf5;\n border-color: #a7f3d0;\n }\n .test-item.fail {\n background: #fef2f2;\n border-color: #fecaca;\n }\n .test-item.pending {\n background: #fffbeb;\n border-color: #fde68a;\n }\n\n .test-icon {\n font-size: 1.1rem;\n flex-shrink: 0;\n }\n .test-desc {\n flex: 1;\n min-width: 150px;\n }\n .test-detail {\n font-size: 0.78rem;\n color: var(--text-secondary);\n font-family: 'Fira Code', 'Consolas', monospace;\n }\n\n .hints-section {\n background: var(--card-bg);\n border-radius: var(--radius);\n box-shadow: var(--shadow);\n padding: 16px;\n display: none;\n }\n .hints-section.visible {\n display: block;\n }\n\n .hints-section h3 {\n font-size: 0.95rem;\n color: var(--primary-dark);\n margin-bottom: 10px;\n display: flex;\n align-items: center;\n gap: 6px;\n }\n\n .hint-item {\n padding: 8px 12px;\n border-radius: 8px;\n background: #f5f3fa;\n margin-bottom: 6px;\n font-size: 0.85rem;\n color: var(--text-secondary);\n border-left: 3px solid var(--primary-light);\n animation: fadeInUp 0.3s ease;\n }\n .hint-item:last-child {\n margin-bottom: 0;\n }\n\n @keyframes fadeInUp {\n from {\n opacity: 0;\n transform: translateY(6px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n }\n\n .solution-section {\n background: var(--card-bg);\n border-radius: var(--radius);\n box-shadow: var(--shadow);\n padding: 16px;\n display: none;\n border: 2px dashed var(--primary-light);\n }\n .solution-section.visible {\n display: block;\n animation: fadeInUp 0.4s ease;\n }\n .solution-section h3 {\n color: var(--primary-dark);\n font-size: 0.95rem;\n margin-bottom: 8px;\n }\n .solution-section pre {\n background: var(--code-bg);\n padding: 12px;\n border-radius: 8px;\n font-family: 'Fira Code', 'Consolas', monospace;\n font-size: 0.85rem;\n white-space: pre-wrap;\n overflow-x: auto;\n }\n\n .teacher-annotation {\n pointer-events: none;\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);\n }\n\n @keyframes pulse-highlight {\n 0%,\n 100% {\n outline-color: rgba(124, 58, 237, 0.8);\n }\n 50% {\n outline-color: rgba(124, 58, 237, 0.3);\n }\n }\n\n @keyframes fadeIn {\n from {\n opacity: 0;\n transform: translateY(10px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n }\n\n /* 桌面端:编辑器与输出并排 */\n @media (min-width: 768px) {\n .main-content {\n grid-template-columns: 3fr 2fr;\n align-items: start;\n }\n .editor-card {\n grid-column: 1;\n grid-row: 1 / span 2;\n }\n .output-card {\n grid-column: 2;\n grid-row: 1;\n }\n .test-card {\n grid-column: 2;\n grid-row: 2;\n }\n .hints-section,\n .solution-section {\n grid-column: 1 / -1;\n }\n .code-editor-host {\n min-height: 320px;\n }\n .code-editor-host .CodeMirror {\n min-height: 320px;\n }\n }\n\n /* 移动端堆叠 */\n @media (max-width: 767px) {\n .main-content {\n grid-template-columns: 1fr;\n }\n .code-editor-host {\n min-height: 200px;\n }\n .code-editor-host .CodeMirror {\n min-height: 200px;\n }\n .toolbar {\n padding: 10px 12px;\n gap: 6px;\n }\n .btn {\n padding: 6px 12px;\n font-size: 0.8rem;\n }\n .header {\n padding: 14px 16px;\n }\n .header h1 {\n font-size: 1.2rem;\n }\n .test-item {\n flex-direction: column;\n align-items: flex-start;\n gap: 4px;\n }\n .test-detail {\n word-break: break-all;\n }\n }\n </style>\n\n<link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css\">\n<script src=\"https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js\"></script>\n<script src=\"https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js\"></script>\n<script>\ndocument.addEventListener(\"DOMContentLoaded\", function() {\n const katexOptions = {\n delimiters: [\n {left: '\\\\[', right: '\\\\]', display: true},\n {left: '\\\\(', right: '\\\\)', display: false},\n {left: '$$', right: '$$', display: true},\n {left: '$', right: '$', display: false}\n ],\n throwOnError: false,\n strict: false,\n trust: true\n };\n\n let renderTimeout;\n function safeRender() {\n if (renderTimeout) clearTimeout(renderTimeout);\n renderTimeout = setTimeout(() => {\n renderMathInElement(document.body, katexOptions);\n }, 100);\n }\n\n renderMathInElement(document.body, katexOptions);\n\n const observer = new MutationObserver((mutations) => {\n let shouldRender = false;\n mutations.forEach((mutation) => {\n if (mutation.target &&\n mutation.target.className &&\n typeof mutation.target.className === 'string' &&\n mutation.target.className.includes('katex')) {\n return;\n }\n shouldRender = true;\n });\n\n if (shouldRender) {\n safeRender();\n }\n });\n\n observer.observe(document.body, {\n childList: true,\n subtree: true,\n characterData: true\n });\n\n setInterval(() => {\n const text = document.body.innerText;\n if (text.includes('\\\\(') || text.includes('$$')) {\n safeRender();\n }\n }, 2000);\n});\n</script>\n</head>\n<body>\n <div class=\"container\">\n <!-- 页头 -->\n <div class=\"header\" id=\"widget-header\">\n <h1>🐍 函数调试练习</h1>\n <p class=\"description\">通过交互式代码练习修改函数参数并观察返回值。亲手尝试、观察变化、理解函数!</p>\n <div class=\"key-points\">\n <span class=\"badge\">📝 定义带参数的函数</span>\n <span class=\"badge\">🔧 调用函数并打印结果</span>\n <span class=\"badge\">🔍 修改参数观察输出变化</span>\n </div>\n </div>\n\n <!-- 主内容区 -->\n <div class=\"main-content\">\n <!-- 编辑器卡片 -->\n <div class=\"editor-card\" id=\"editor-card\">\n <div class=\"toolbar\">\n <button id=\"run-btn\" class=\"btn btn-primary\" disabled>▶ 运行代码</button>\n <button id=\"hint-btn\" class=\"btn btn-secondary\">💡 提示</button>\n <button id=\"reset-btn\" class=\"btn btn-secondary\">↺ 重置</button>\n <span class=\"status-badge\">\n <span id=\"status-dot\" class=\"status-dot\"></span>\n <span id=\"status\">Python 环境加载中...</span>\n </span>\n </div>\n <div id=\"code-input\" class=\"code-editor-host\"></div>\n </div>\n\n <!-- 输出面板 -->\n <div class=\"output-card\" id=\"output-card\">\n <div class=\"output-header\">📤 输出结果</div>\n <div id=\"output\" class=\"output-content\">点击「运行代码」查看输出...</div>\n </div>\n\n <!-- 测试用例 -->\n <div class=\"test-card\" id=\"test-card\">\n <div class=\"test-header\" id=\"test-toggle\">\n <span>🧪 测试用例</span>\n <span class=\"toggle-icon\">▼</span>\n </div>\n <div class=\"test-results\" id=\"test-results\">\n <div class=\"test-item pending\">\n <span class=\"test-icon\">⏳</span>\n <span class=\"test-desc\">运行代码后自动验证</span>\n </div>\n </div>\n </div>\n\n <!-- 提示区域 -->\n <div class=\"hints-section\" id=\"hints-section\">\n <h3>💡 学习提示</h3>\n <div id=\"hints-container\"></div>\n </div>\n\n <!-- 参考解答 -->\n <div class=\"solution-section\" id=\"solution\">\n <h3>✅ 参考解答</h3>\n <pre id=\"solution-code\"></pre>\n </div>\n </div>\n </div>\n\n <script>\n // ==========================================\n // 内嵌 Widget 配置 JSON\n // ==========================================\n const widgetConfig = {\n \"type\": \"code\",\n \"language\": \"python\",\n \"description\": \"通过交互式代码练习修改函数参数并观察返回值。\",\n \"starterCode\": \"# 定义一个带参数的函数\\ndef double(x):\\n return x * 2\\n\\n# 调用函数并打印结果\\nprint(double(5))\\nprint(double(10))\\nprint(double(100))\",\n \"testCases\": [\n { \"id\": \"t1\", \"input\": \"5\", \"expected\": \"10\", \"description\": \"double(5) 应该返回 10\" },\n { \"id\": \"t2\", \"input\": \"10\", \"expected\": \"20\", \"description\": \"double(10) 应该返回 20\" },\n { \"id\": \"t3\", \"input\": \"100\", \"expected\": \"200\", \"description\": \"double(100) 应该返回 200\" }\n ],\n \"hints\": [\n \"函数使用 def 关键字定义,括号里的 x 是参数(parameter),它代表了传入的值。\",\n \"return 语句把计算结果返回给调用者。x * 2 就是计算 x 的两倍。\",\n \"修改 print(double(5)) 里的数字,比如改成 double(20) 或 double(50),看看输出有什么变化。\",\n \"试试传入负数或小数,比如 print(double(-3)) 或 print(double(2.5)),Python 会自动处理。\"\n ],\n \"solution\": \"def double(x):\\n return x * 2\\n\\nprint(double(5))\\nprint(double(10))\\nprint(double(100))\"\n };\n\n // ==========================================\n // 全局变量\n // ==========================================\n let pyodide = null;\n let editor = null; // CodeMirror 实例(全局,供 postMessage 使用)\n let currentHintIndex = 0;\n let testsRun = false;\n const hintsList = widgetConfig.hints || [];\n\n // ==========================================\n // 初始化 CodeMirror 编辑器\n // ==========================================\n function initEditor() {\n editor = CodeMirror(document.getElementById('code-input'), {\n mode: 'python',\n theme: 'default',\n lineNumbers: true,\n value: widgetConfig.starterCode,\n indentUnit: 4,\n tabSize: 4,\n indentWithTabs: false,\n lineWrapping: true,\n viewportMargin: Infinity,\n autoCloseBrackets: true,\n matchBrackets: true,\n });\n editor.setSize('100%', '100%');\n editor.refresh();\n // 确保在容器大小变化时正确刷新\n setTimeout(() => editor.refresh(), 100);\n }\n\n // ==========================================\n // 初始化 Pyodide\n // ==========================================\n async function initPyodide() {\n const statusEl = document.getElementById('status');\n const dotEl = document.getElementById('status-dot');\n try {\n pyodide = await loadPyodide();\n // 本练习不需要额外包,但示例展示如何加载\n // await pyodide.loadPackage(['numpy']);\n dotEl.classList.add('ready');\n statusEl.textContent = 'Python 环境已就绪';\n document.getElementById('run-btn').disabled = false;\n } catch (e) {\n dotEl.style.background = '#ef4444';\n dotEl.style.animation = 'none';\n statusEl.textContent = 'Python 环境加载失败: ' + e.message;\n document.getElementById('run-btn').disabled = true;\n }\n }\n\n // ==========================================\n // 运行用户代码 + 执行测试\n // ==========================================\n async function runCode() {\n if (!pyodide) {\n alert('Python 环境尚未准备就绪,请稍候。');\n return;\n }\n\n const code = editor.getValue().trim();\n if (!code) {\n document.getElementById('output').innerHTML =\n '<span class=\"error-text\">错误:代码为空,请输入代码。</span>';\n return;\n }\n\n const outputEl = document.getElementById('output');\n outputEl.textContent = '正在运行...';\n\n try {\n // CRITICAL: 必须同时导入 sys 和 io,然后再重定向 stdout\n await pyodide.runPythonAsync(`\nimport sys\nimport io\nsys.stdout = io.StringIO()\n`);\n\n // 异步执行用户代码\n await pyodide.runPythonAsync(code);\n\n // 获取标准输出内容\n const userOutput = pyodide.runPython('sys.stdout.getvalue()');\n\n if (userOutput && userOutput.trim()) {\n outputEl.textContent = userOutput;\n } else {\n outputEl.innerHTML = '<span class=\"success-text\">(代码执行成功,但没有打印输出。试着用 print() 打印结果吧!)</span>';\n }\n\n // 执行测试用例\n await runTestCases();\n\n } catch (e) {\n // 提取 Python 错误信息\n let errorMsg = e.message;\n // 清理常见前缀\n if (errorMsg.includes('PythonError')) {\n errorMsg = errorMsg.replace(/^PythonError:\\s*/g, '');\n }\n outputEl.innerHTML = '<span class=\"error-text\">❌ 错误: ' +\n escapeHtml(errorMsg) + '</span>';\n markAllTestsFailed('代码有错误');\n }\n }\n\n // ==========================================\n // 运行测试用例\n // ==========================================\n async function runTestCases() {\n const testCases = widgetConfig.testCases || [];\n const resultsContainer = document.getElementById('test-results');\n testsRun = true;\n\n if (testCases.length === 0) {\n resultsContainer.innerHTML =\n '<div class=\"test-item pending\"><span class=\"test-icon\">📋</span><span class=\"test-desc\">本练习没有设置测试用例,请自由探索!</span></div>';\n return;\n }\n\n let passCount = 0;\n let resultsHtml = '';\n\n for (const tc of testCases) {\n const funcName = 'double'; // 从 starterCode 中推断函数名\n const expr = funcName + '(' + tc.input + ')';\n let pass = false;\n let actualValue = '未定义';\n let errorDetail = '';\n\n try {\n // 在 Pyodide 中运行表达式,获取结果\n const resultScript = `\n__test_result = ${expr}\n__test_result_str = str(__test_result)\n`;\n await pyodide.runPythonAsync(resultScript);\n const actual = pyodide.runPython('__test_result_str');\n actualValue = actual;\n // 比较(字符串比较,兼容数字和字符串类型)\n pass = (actual === String(tc.expected));\n } catch (e) {\n errorDetail = e.message.replace(/^PythonError:\\s*/g, '');\n errorDetail = errorDetail.split('\\n')[0]; // 只取第一行\n actualValue = '错误: ' + errorDetail;\n pass = false;\n }\n\n if (pass) passCount++;\n\n resultsHtml += `\n <div class=\"test-item ${pass ? 'pass' : 'fail'}\">\n <span class=\"test-icon\">${pass ? '✅' : '❌'}</span>\n <span class=\"test-desc\">${escapeHtml(tc.description || tc.id)}</span>\n <span class=\"test-detail\">${escapeHtml(expr)} → 期望 ${escapeHtml(String(tc.expected))} / 实际 ${escapeHtml(actualValue)}</span>\n </div>`;\n }\n\n // 添加摘要\n const summaryClass = passCount === testCases.length ? 'pass' : 'fail';\n resultsHtml += `\n <div class=\"test-item ${summaryClass}\" style=\"font-weight:600;margin-top:4px;\">\n <span class=\"test-icon\">${passCount === testCases.length ? '🏆' : '📊'}</span>\n <span class=\"test-desc\">测试结果:${passCount} / ${testCases.length} 通过</span>\n </div>`;\n\n resultsContainer.innerHTML = resultsHtml;\n }\n\n function markAllTestsFailed(reason) {\n const resultsContainer = document.getElementById('test-results');\n const testCases = widgetConfig.testCases || [];\n let html = '';\n for (const tc of testCases) {\n html += `\n <div class=\"test-item fail\">\n <span class=\"test-icon\">❌</span>\n <span class=\"test-desc\">${escapeHtml(tc.description || tc.id)}</span>\n <span class=\"test-detail\">${escapeHtml(reason)}</span>\n </div>`;\n }\n if (testCases.length > 0) {\n html += `\n <div class=\"test-item fail\" style=\"font-weight:600;margin-top:4px;\">\n <span class=\"test-icon\">📊</span>\n <span class=\"test-desc\">测试结果:0 / ${testCases.length} 通过</span>\n </div>`;\n }\n resultsContainer.innerHTML = html;\n testsRun = true;\n }\n\n // ==========================================\n // 提示系统(逐条显示)\n // ==========================================\n function showNextHint() {\n const hintsSection = document.getElementById('hints-section');\n const hintsContainer = document.getElementById('hints-container');\n\n if (!hintsSection.classList.contains('visible')) {\n hintsSection.classList.add('visible');\n }\n\n if (currentHintIndex < hintsList.length) {\n const hint = hintsList[currentHintIndex];\n const hintEl = document.createElement('div');\n hintEl.className = 'hint-item';\n hintEl.id = 'hint-' + (currentHintIndex + 1);\n hintEl.innerHTML = '<strong>提示 ' + (currentHintIndex + 1) + ':</strong> ' + escapeHtml(hint);\n hintsContainer.appendChild(hintEl);\n currentHintIndex++;\n\n if (currentHintIndex >= hintsList.length) {\n document.getElementById('hint-btn').textContent = '💡 提示已全部显示';\n document.getElementById('hint-btn').disabled = true;\n }\n }\n }\n\n // ==========================================\n // 重置代码到初始状态\n // ==========================================\n function resetCode() {\n if (editor) {\n editor.setValue(widgetConfig.starterCode);\n editor.clearHistory();\n document.getElementById('output').textContent = '已重置,点击「运行代码」查看输出。';\n // 重置测试状态\n const resultsContainer = document.getElementById('test-results');\n resultsContainer.innerHTML =\n '<div class=\"test-item pending\"><span class=\"test-icon\">⏳</span><span class=\"test-desc\">运行代码后自动验证</span></div>';\n testsRun = false;\n // 重置提示\n currentHintIndex = 0;\n document.getElementById('hints-container').innerHTML = '';\n document.getElementById('hints-section').classList.remove('visible');\n document.getElementById('hint-btn').textContent = '💡 提示';\n document.getElementById('hint-btn').disabled = false;\n // 隐藏参考解答\n document.getElementById('solution').classList.remove('visible');\n }\n }\n\n // ==========================================\n // 工具函数:HTML 转义\n // ==========================================\n function escapeHtml(str) {\n if (str === null || str === undefined) return '';\n const div = document.createElement('div');\n div.textContent = str;\n return div.innerHTML;\n }\n\n // ==========================================\n // CRITICAL: postMessage 事件监听器 (REQUIRED)\n // ==========================================\n window.addEventListener('message', function(event) {\n const { type, target, state, content } = event.data;\n\n switch (type) {\n case 'SET_WIDGET_STATE':\n // 加载代码到编辑器,可选择性运行\n if (state && typeof state.code === 'string') {\n if (typeof editor !== 'undefined' && editor && typeof editor.setValue ===\n 'function') {\n editor.setValue(state.code);\n } else {\n const ta = document.getElementById('code-input');\n if (ta) ta.value = state.code;\n }\n }\n if (state && state.run && typeof runCode === 'function') {\n runCode();\n }\n break;\n\n case 'HIGHLIGHT_ELEMENT':\n const highlightEl = document.querySelector(target);\n if (highlightEl) {\n highlightEl.style.outline = '3px solid rgba(124, 58, 237, 0.8)';\n highlightEl.style.outlineOffset = '4px';\n highlightEl.style.animation = 'pulse-highlight 2s infinite';\n setTimeout(() => {\n highlightEl.style.outline = '';\n highlightEl.style.animation = '';\n }, 3000);\n }\n break;\n\n case 'ANNOTATE_ELEMENT':\n const annotateEl = document.querySelector(target);\n if (annotateEl && content) {\n const rect = annotateEl.getBoundingClientRect();\n const tooltip = document.createElement('div');\n tooltip.className = 'teacher-annotation';\n tooltip.style.cssText =\n 'position:fixed; top:' + (rect.top - 40) + 'px; left:' + rect.left +\n 'px; background:rgba(124,58,237,0.95); color:white; padding:8px 12px; border-radius:8px; font-size:14px; z-index:1000; animation:fadeIn 0.3s;';\n tooltip.textContent = content;\n document.body.appendChild(tooltip);\n setTimeout(() => tooltip.remove(), 4000);\n }\n break;\n\n case 'REVEAL_ELEMENT':\n const revealEl = document.querySelector(target);\n if (revealEl) {\n revealEl.style.display = '';\n revealEl.style.opacity = '1';\n if (revealEl.id === 'solution' || revealEl.classList.contains(\n 'solution-section')) {\n revealEl.classList.add('visible');\n }\n }\n break;\n\n default:\n // 忽略未知类型\n break;\n }\n });\n\n // 添加 pulse-highlight 和 fadeIn 关键帧动画\n const styleEl = document.createElement('style');\n styleEl.textContent =\n '@keyframes pulse-highlight { 0%, 100% { outline-color: rgba(124, 58, 237, 0.8); } 50% { outline-color: rgba(124, 58, 237, 0.3); } } @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }';\n document.head.appendChild(styleEl);\n\n // ==========================================\n // 事件绑定\n // ==========================================\n document.addEventListener('DOMContentLoaded', function() {\n initEditor();\n initPyodide();\n\n document.getElementById('run-btn').addEventListener('click', runCode);\n document.getElementById('hint-btn').addEventListener('click', showNextHint);\n document.getElementById('reset-btn').addEventListener('click', resetCode);\n\n // 测试用例折叠/展开\n const testToggle = document.getElementById('test-toggle');\n const testResults = document.getElementById('test-results');\n testToggle.addEventListener('click', function() {\n const isCollapsed = testToggle.classList.toggle('collapsed');\n if (isCollapsed) {\n testResults.style.display = 'none';\n } else {\n testResults.style.display = '';\n }\n });\n\n // 在参考解答 section 中加载 solution 内容\n const solutionEl = document.getElementById('solution');\n if (solutionEl && widgetConfig.solution) {\n document.getElementById('solution-code').textContent = widgetConfig.solution;\n }\n\n // 窗口大小变化时刷新编辑器\n window.addEventListener('resize', function() {\n if (editor) editor.refresh();\n });\n });\n\n // 确保在 Pyodide 脚本加载完成后初始化\n // 由于脚本是同步加载的,这里直接初始化\n // 但如果 Pyodide CDN 尚未加载完成,loadPyodide 会在 initPyodide 中处理\n </script>\n</body>\n</html>",
|
||
"widgetType": "code"
|
||
}
|
||
},
|
||
{
|
||
"id": "scene_nyqpYDSNXz",
|
||
"stageId": "DZynj92UuU",
|
||
"title": "课程总结测验",
|
||
"order": 10,
|
||
"actions": [
|
||
{
|
||
"id": "action_Hgob6KaW",
|
||
"type": "speech",
|
||
"text": "现在来做课程总结小测验,看看这节课的内容掌握得怎么样。请独立尝试每一道题,提交后我会和你一起逐题回顾分析。"
|
||
}
|
||
],
|
||
"outlineId": "scene_10",
|
||
"createdAt": 1786856253251,
|
||
"updatedAt": 1786856253251,
|
||
"type": "quiz",
|
||
"content": {
|
||
"type": "quiz",
|
||
"questions": [
|
||
{
|
||
"id": "q1",
|
||
"type": "single",
|
||
"question": "在Python中,以下哪个数据类型最适合用来存储一个学生的姓名和对应的年龄信息?",
|
||
"options": [
|
||
{
|
||
"value": "A",
|
||
"label": "列表(list)"
|
||
},
|
||
{
|
||
"value": "B",
|
||
"label": "元组(tuple)"
|
||
},
|
||
{
|
||
"value": "C",
|
||
"label": "字典(dict)"
|
||
},
|
||
{
|
||
"value": "D",
|
||
"label": "集合(set)"
|
||
}
|
||
],
|
||
"answer": [
|
||
"C"
|
||
],
|
||
"analysis": "字典(dict)以键值对形式存储数据,适合存储姓名和年龄这种一一对应的信息。列表和元组只能按顺序存储多个值,集合不能存储键值对。",
|
||
"points": 10,
|
||
"hasAnswer": true
|
||
},
|
||
{
|
||
"id": "q2",
|
||
"type": "single",
|
||
"question": "运行以下Python代码后,屏幕上会依次显示哪些数字?\nfor i in range(1, 4):\n print(i)",
|
||
"options": [
|
||
{
|
||
"value": "A",
|
||
"label": "1 2 3"
|
||
},
|
||
{
|
||
"value": "B",
|
||
"label": "1 2 3 4"
|
||
},
|
||
{
|
||
"value": "C",
|
||
"label": "0 1 2"
|
||
},
|
||
{
|
||
"value": "D",
|
||
"label": "2 3"
|
||
}
|
||
],
|
||
"answer": [
|
||
"A"
|
||
],
|
||
"analysis": "range(1, 4)从1开始到4之前结束,生成1、2、3。循环依次打印每个数字,所以屏幕上依次显示1、2、3。",
|
||
"points": 10,
|
||
"hasAnswer": true
|
||
},
|
||
{
|
||
"id": "q3",
|
||
"type": "multiple",
|
||
"question": "关于Python中的列表(list)和字典(dict),以下哪些说法是正确的?(多选)",
|
||
"options": [
|
||
{
|
||
"value": "A",
|
||
"label": "列表是有序的,可以通过索引访问元素"
|
||
},
|
||
{
|
||
"value": "B",
|
||
"label": "字典中的键必须是唯一的"
|
||
},
|
||
{
|
||
"value": "C",
|
||
"label": "列表只能存储数字类型的数据"
|
||
},
|
||
{
|
||
"value": "D",
|
||
"label": "字典的元素可以通过append()方法添加"
|
||
}
|
||
],
|
||
"answer": [
|
||
"A",
|
||
"B"
|
||
],
|
||
"analysis": "A正确:列表是有序的,支持索引访问。B正确:字典的键必须唯一,否则后面的会覆盖前面的。C错误:列表可以存储任意类型的数据。D错误:append()是列表的方法,字典添加键值对通常使用赋值或update()方法。",
|
||
"points": 15,
|
||
"hasAnswer": true
|
||
},
|
||
{
|
||
"id": "q4",
|
||
"type": "single",
|
||
"question": "在Python中,定义函数时需要使用哪个关键字?",
|
||
"options": [
|
||
{
|
||
"value": "A",
|
||
"label": "def"
|
||
},
|
||
{
|
||
"value": "B",
|
||
"label": "function"
|
||
},
|
||
{
|
||
"value": "C",
|
||
"label": "define"
|
||
},
|
||
{
|
||
"value": "D",
|
||
"label": "fun"
|
||
}
|
||
],
|
||
"answer": [
|
||
"A"
|
||
],
|
||
"analysis": "Python中定义函数使用def关键字,后面跟函数名和参数列表。function、define、fun都不是Python定义函数的关键字。",
|
||
"points": 10,
|
||
"hasAnswer": true
|
||
},
|
||
{
|
||
"id": "q5",
|
||
"type": "short_answer",
|
||
"question": "请用中文解释什么是函数(function),并说明在程序中使用函数的好处。最后请写出一个简单的Python函数示例(可以只写代码,也可以加上解释)。",
|
||
"commentPrompt": "评分标准:(1)正确解释函数的概念:函数是一段可重复使用的代码块,用于完成特定任务,占40%;(2)至少说出两个使用函数的好处,如复用代码、模块化、易于维护、减少重复等,占30%;(3)给出一个正确的Python函数定义示例(包含def关键字、函数名、括号、冒号、函数体),占30%。如果学生没有写出代码,但前两部分正确,可给至多70%的分。",
|
||
"analysis": "参考答案:函数是一段有名字的、可重复调用的代码块,用于完成特定任务。好处包括:避免代码重复,提高代码复用性;使程序结构清晰,易于理解和维护;便于调试和修改。示例:def greet(name):\n print('你好, ' + name)\n调用greet('小明')会输出'你好, 小明'。",
|
||
"points": 20,
|
||
"hasAnswer": false
|
||
}
|
||
]
|
||
}
|
||
}
|
||
],
|
||
"createdAt": "2026-08-16T04:57:33.260Z"
|
||
}
|