diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..b512c09
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+node_modules
\ No newline at end of file
diff --git a/04-dom-events-async/01-query-selectors/README.md b/04-dom-events-async/01-query-selectors/README.md
new file mode 100644
index 0000000..7ee016a
--- /dev/null
+++ b/04-dom-events-async/01-query-selectors/README.md
@@ -0,0 +1,28 @@
+# 练习 1:获取元素
+
+## 目标
+
+学会用几种常见方式拿到页面元素。
+
+## 你要练什么
+
+- `getElementById`
+- `querySelector`
+- `querySelectorAll`
+- 元素文本读取
+
+## 任务
+
+请基于页面结构完成以下操作:
+
+- 选中主标题
+- 选中“开始学习”按钮
+- 选中全部学习卡片
+- 在控制台输出标题文字、按钮文字和卡片数量
+
+## 文件
+
+- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/01-query-selectors/starter.html)
+- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/01-query-selectors/starter.js)
+- [answer.html](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/01-query-selectors/answer.html)
+- [answer.js](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/01-query-selectors/answer.js)
diff --git a/04-dom-events-async/01-query-selectors/answer.html b/04-dom-events-async/01-query-selectors/answer.html
new file mode 100644
index 0000000..e89f82a
--- /dev/null
+++ b/04-dom-events-async/01-query-selectors/answer.html
@@ -0,0 +1,52 @@
+
+
+
+
+
+ 获取元素
+
+
+
+
+ DOM 获取元素练习
+
+
+
+
+
+
+
+
diff --git a/04-dom-events-async/01-query-selectors/answer.js b/04-dom-events-async/01-query-selectors/answer.js
new file mode 100644
index 0000000..b9ab505
--- /dev/null
+++ b/04-dom-events-async/01-query-selectors/answer.js
@@ -0,0 +1,7 @@
+const title = document.getElementById("page-title");
+const button = document.querySelector(".start-btn");
+const cards = document.querySelectorAll(".card");
+
+console.log("标题:", title.textContent);
+console.log("按钮:", button.textContent);
+console.log("卡片数量:", cards.length);
diff --git a/04-dom-events-async/01-query-selectors/starter.html b/04-dom-events-async/01-query-selectors/starter.html
new file mode 100644
index 0000000..b1eda62
--- /dev/null
+++ b/04-dom-events-async/01-query-selectors/starter.html
@@ -0,0 +1,52 @@
+
+
+
+
+
+ 获取元素
+
+
+
+
+ DOM 获取元素练习
+
+
+
+
+
+
+
+
diff --git a/04-dom-events-async/01-query-selectors/starter.js b/04-dom-events-async/01-query-selectors/starter.js
new file mode 100644
index 0000000..44e5864
--- /dev/null
+++ b/04-dom-events-async/01-query-selectors/starter.js
@@ -0,0 +1,5 @@
+// 任务:
+// 1. 用 getElementById 获取标题
+// 2. 用 querySelector 获取按钮
+// 3. 用 querySelectorAll 获取全部卡片
+// 4. 在控制台输出标题文字、按钮文字和卡片数量
diff --git a/04-dom-events-async/02-text-class-style/README.md b/04-dom-events-async/02-text-class-style/README.md
new file mode 100644
index 0000000..29d17de
--- /dev/null
+++ b/04-dom-events-async/02-text-class-style/README.md
@@ -0,0 +1,25 @@
+# 练习 2:修改文本、类名和样式
+
+## 目标
+
+学会改元素内容、切换类名和设置简单样式。
+
+## 你要练什么
+
+- `textContent`
+- `classList.add`
+- `classList.remove`
+- `style`
+
+## 任务
+
+- 把标题改成“今天已完成 DOM 练习”
+- 给状态标签加上完成样式
+- 修改说明文字颜色
+
+## 文件
+
+- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/02-text-class-style/starter.html)
+- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/02-text-class-style/starter.js)
+- [answer.html](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/02-text-class-style/answer.html)
+- [answer.js](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/02-text-class-style/answer.js)
diff --git a/04-dom-events-async/02-text-class-style/answer.html b/04-dom-events-async/02-text-class-style/answer.html
new file mode 100644
index 0000000..e9a5ec4
--- /dev/null
+++ b/04-dom-events-async/02-text-class-style/answer.html
@@ -0,0 +1,47 @@
+
+
+
+
+
+ 修改文本、类名和样式
+
+
+
+
+ 今天的学习状态
+ 当前还没有更新进度。
+ 进行中
+
+
+
+
+
diff --git a/04-dom-events-async/02-text-class-style/answer.js b/04-dom-events-async/02-text-class-style/answer.js
new file mode 100644
index 0000000..b434195
--- /dev/null
+++ b/04-dom-events-async/02-text-class-style/answer.js
@@ -0,0 +1,9 @@
+const title = document.getElementById("title");
+const description = document.getElementById("description");
+const badge = document.getElementById("badge");
+
+title.textContent = "今天已完成 DOM 练习";
+description.textContent = "已经练习了元素选择、文本修改和样式切换。";
+description.style.color = "#2563eb";
+badge.textContent = "已完成";
+badge.classList.add("done");
diff --git a/04-dom-events-async/02-text-class-style/starter.html b/04-dom-events-async/02-text-class-style/starter.html
new file mode 100644
index 0000000..28af723
--- /dev/null
+++ b/04-dom-events-async/02-text-class-style/starter.html
@@ -0,0 +1,47 @@
+
+
+
+
+
+ 修改文本、类名和样式
+
+
+
+
+ 今天的学习状态
+ 当前还没有更新进度。
+ 进行中
+
+
+
+
+
diff --git a/04-dom-events-async/02-text-class-style/starter.js b/04-dom-events-async/02-text-class-style/starter.js
new file mode 100644
index 0000000..2966eeb
--- /dev/null
+++ b/04-dom-events-async/02-text-class-style/starter.js
@@ -0,0 +1,5 @@
+// 任务:
+// 1. 选中标题、描述、状态标签
+// 2. 修改标题文字
+// 3. 给标签加上 done 类名
+// 4. 修改描述文字颜色
diff --git a/04-dom-events-async/03-create-and-remove/README.md b/04-dom-events-async/03-create-and-remove/README.md
new file mode 100644
index 0000000..bf5e8f9
--- /dev/null
+++ b/04-dom-events-async/03-create-and-remove/README.md
@@ -0,0 +1,24 @@
+# 练习 3:创建和删除节点
+
+## 目标
+
+学会动态新增和删除页面节点。
+
+## 你要练什么
+
+- `createElement`
+- `appendChild`
+- `remove`
+- 事件绑定
+
+## 任务
+
+- 点击“新增任务”时往列表里加一个新项
+- 点击“删除最后一项”时删除最后一个列表项
+
+## 文件
+
+- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/03-create-and-remove/starter.html)
+- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/03-create-and-remove/starter.js)
+- [answer.html](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/03-create-and-remove/answer.html)
+- [answer.js](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/03-create-and-remove/answer.js)
diff --git a/04-dom-events-async/03-create-and-remove/answer.html b/04-dom-events-async/03-create-and-remove/answer.html
new file mode 100644
index 0000000..90a810b
--- /dev/null
+++ b/04-dom-events-async/03-create-and-remove/answer.html
@@ -0,0 +1,47 @@
+
+
+
+
+
+ 创建和删除节点
+
+
+
+
+ 任务列表
+
+
+
+
+ - 学习 querySelector
+ - 学习 classList
+
+
+
+
+
+
diff --git a/04-dom-events-async/03-create-and-remove/answer.js b/04-dom-events-async/03-create-and-remove/answer.js
new file mode 100644
index 0000000..39488ec
--- /dev/null
+++ b/04-dom-events-async/03-create-and-remove/answer.js
@@ -0,0 +1,20 @@
+const addButton = document.getElementById("add-btn");
+const removeButton = document.getElementById("remove-btn");
+const taskList = document.getElementById("task-list");
+
+let taskIndex = 3;
+
+addButton.addEventListener("click", function () {
+ const item = document.createElement("li");
+ item.textContent = `新任务 ${taskIndex}`;
+ taskList.appendChild(item);
+ taskIndex += 1;
+});
+
+removeButton.addEventListener("click", function () {
+ const lastItem = taskList.lastElementChild;
+
+ if (lastItem) {
+ lastItem.remove();
+ }
+});
diff --git a/04-dom-events-async/03-create-and-remove/starter.html b/04-dom-events-async/03-create-and-remove/starter.html
new file mode 100644
index 0000000..2bb0658
--- /dev/null
+++ b/04-dom-events-async/03-create-and-remove/starter.html
@@ -0,0 +1,47 @@
+
+
+
+
+
+ 创建和删除节点
+
+
+
+
+ 任务列表
+
+
+
+
+ - 学习 querySelector
+ - 学习 classList
+
+
+
+
+
+
diff --git a/04-dom-events-async/03-create-and-remove/starter.js b/04-dom-events-async/03-create-and-remove/starter.js
new file mode 100644
index 0000000..d5abcb4
--- /dev/null
+++ b/04-dom-events-async/03-create-and-remove/starter.js
@@ -0,0 +1,4 @@
+// 任务:
+// 1. 获取新增按钮、删除按钮和列表
+// 2. 点击新增按钮时创建一个新的 li 并追加到列表
+// 3. 点击删除按钮时删除最后一个 li
diff --git a/04-dom-events-async/04-click-counter/README.md b/04-dom-events-async/04-click-counter/README.md
new file mode 100644
index 0000000..ecf284a
--- /dev/null
+++ b/04-dom-events-async/04-click-counter/README.md
@@ -0,0 +1,25 @@
+# 练习 4:点击计数器
+
+## 目标
+
+学会给按钮绑定点击事件,并更新页面数据。
+
+## 你要练什么
+
+- `addEventListener`
+- 点击事件
+- 数字状态更新
+- DOM 渲染
+
+## 任务
+
+- 点击加一按钮时计数加 1
+- 点击减一按钮时计数减 1
+- 点击重置按钮时恢复为 0
+
+## 文件
+
+- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/04-click-counter/starter.html)
+- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/04-click-counter/starter.js)
+- [answer.html](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/04-click-counter/answer.html)
+- [answer.js](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/04-click-counter/answer.js)
diff --git a/04-dom-events-async/04-click-counter/answer.html b/04-dom-events-async/04-click-counter/answer.html
new file mode 100644
index 0000000..437bc7d
--- /dev/null
+++ b/04-dom-events-async/04-click-counter/answer.html
@@ -0,0 +1,51 @@
+
+
+
+
+
+ 点击计数器
+
+
+
+
+ 点击计数器
+ 0
+
+
+
+
+
+
+
+
+
+
diff --git a/04-dom-events-async/04-click-counter/answer.js b/04-dom-events-async/04-click-counter/answer.js
new file mode 100644
index 0000000..b2d3af9
--- /dev/null
+++ b/04-dom-events-async/04-click-counter/answer.js
@@ -0,0 +1,25 @@
+let count = 0;
+
+const value = document.getElementById("value");
+const decreaseButton = document.getElementById("decrease-btn");
+const increaseButton = document.getElementById("increase-btn");
+const resetButton = document.getElementById("reset-btn");
+
+function render() {
+ value.textContent = count;
+}
+
+decreaseButton.addEventListener("click", function () {
+ count -= 1;
+ render();
+});
+
+increaseButton.addEventListener("click", function () {
+ count += 1;
+ render();
+});
+
+resetButton.addEventListener("click", function () {
+ count = 0;
+ render();
+});
diff --git a/04-dom-events-async/04-click-counter/starter.html b/04-dom-events-async/04-click-counter/starter.html
new file mode 100644
index 0000000..ee6997c
--- /dev/null
+++ b/04-dom-events-async/04-click-counter/starter.html
@@ -0,0 +1,51 @@
+
+
+
+
+
+ 点击计数器
+
+
+
+
+ 点击计数器
+ 0
+
+
+
+
+
+
+
+
+
+
diff --git a/04-dom-events-async/04-click-counter/starter.js b/04-dom-events-async/04-click-counter/starter.js
new file mode 100644
index 0000000..6a57a2b
--- /dev/null
+++ b/04-dom-events-async/04-click-counter/starter.js
@@ -0,0 +1,6 @@
+let count = 0;
+
+// 任务:
+// 1. 获取数字元素和 3 个按钮
+// 2. 点击按钮时更新 count
+// 3. 每次修改后,把最新 count 渲染到页面
diff --git a/04-dom-events-async/05-form-submit-and-prevent-default/README.md b/04-dom-events-async/05-form-submit-and-prevent-default/README.md
new file mode 100644
index 0000000..32dbcea
--- /dev/null
+++ b/04-dom-events-async/05-form-submit-and-prevent-default/README.md
@@ -0,0 +1,26 @@
+# 练习 5:表单提交和 preventDefault
+
+## 目标
+
+学会拦截表单默认提交,并把输入内容渲染到页面。
+
+## 你要练什么
+
+- `submit` 事件
+- `preventDefault()`
+- 表单值读取
+- 动态追加列表项
+
+## 任务
+
+- 提交表单时阻止页面刷新
+- 读取输入框内容
+- 把内容加入到待办列表
+- 提交后清空输入框
+
+## 文件
+
+- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/05-form-submit-and-prevent-default/starter.html)
+- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/05-form-submit-and-prevent-default/starter.js)
+- [answer.html](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/05-form-submit-and-prevent-default/answer.html)
+- [answer.js](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/05-form-submit-and-prevent-default/answer.js)
diff --git a/04-dom-events-async/05-form-submit-and-prevent-default/answer.html b/04-dom-events-async/05-form-submit-and-prevent-default/answer.html
new file mode 100644
index 0000000..14df416
--- /dev/null
+++ b/04-dom-events-async/05-form-submit-and-prevent-default/answer.html
@@ -0,0 +1,51 @@
+
+
+
+
+
+ 表单提交和 preventDefault
+
+
+
+
+
+
+
+
diff --git a/04-dom-events-async/05-form-submit-and-prevent-default/answer.js b/04-dom-events-async/05-form-submit-and-prevent-default/answer.js
new file mode 100644
index 0000000..afacfc7
--- /dev/null
+++ b/04-dom-events-async/05-form-submit-and-prevent-default/answer.js
@@ -0,0 +1,18 @@
+const form = document.getElementById("todo-form");
+const input = document.getElementById("todo-input");
+const list = document.getElementById("todo-list");
+
+form.addEventListener("submit", function (event) {
+ event.preventDefault();
+
+ const value = input.value.trim();
+
+ if (!value) {
+ return;
+ }
+
+ const item = document.createElement("li");
+ item.textContent = value;
+ list.appendChild(item);
+ input.value = "";
+});
diff --git a/04-dom-events-async/05-form-submit-and-prevent-default/starter.html b/04-dom-events-async/05-form-submit-and-prevent-default/starter.html
new file mode 100644
index 0000000..1eb5213
--- /dev/null
+++ b/04-dom-events-async/05-form-submit-and-prevent-default/starter.html
@@ -0,0 +1,51 @@
+
+
+
+
+
+ 表单提交和 preventDefault
+
+
+
+
+
+
+
+
diff --git a/04-dom-events-async/05-form-submit-and-prevent-default/starter.js b/04-dom-events-async/05-form-submit-and-prevent-default/starter.js
new file mode 100644
index 0000000..1e1d825
--- /dev/null
+++ b/04-dom-events-async/05-form-submit-and-prevent-default/starter.js
@@ -0,0 +1,6 @@
+// 任务:
+// 1. 获取表单、输入框和列表
+// 2. 监听 submit 事件
+// 3. 用 preventDefault() 阻止默认提交
+// 4. 读取输入框内容,创建新 li,追加到列表
+// 5. 清空输入框
diff --git a/04-dom-events-async/06-bubbling-and-delegation/README.md b/04-dom-events-async/06-bubbling-and-delegation/README.md
new file mode 100644
index 0000000..a29ad63
--- /dev/null
+++ b/04-dom-events-async/06-bubbling-and-delegation/README.md
@@ -0,0 +1,25 @@
+# 练习 6:冒泡、委托和 stopPropagation
+
+## 目标
+
+理解事件会冒泡,并学会在列表里使用事件委托。
+
+## 你要练什么
+
+- 事件冒泡
+- `event.target`
+- 事件委托
+- `stopPropagation()`
+
+## 任务
+
+- 点击外层面板时输出一条日志
+- 点击列表项时,通过事件委托切换激活状态
+- 点击列表项里的删除按钮时,阻止冒泡并删除当前项
+
+## 文件
+
+- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/06-bubbling-and-delegation/starter.html)
+- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/06-bubbling-and-delegation/starter.js)
+- [answer.html](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/06-bubbling-and-delegation/answer.html)
+- [answer.js](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/06-bubbling-and-delegation/answer.js)
diff --git a/04-dom-events-async/06-bubbling-and-delegation/answer.html b/04-dom-events-async/06-bubbling-and-delegation/answer.html
new file mode 100644
index 0000000..4c3e581
--- /dev/null
+++ b/04-dom-events-async/06-bubbling-and-delegation/answer.html
@@ -0,0 +1,58 @@
+
+
+
+
+
+ 冒泡、委托和 stopPropagation
+
+
+
+
+ 事件委托练习
+ 点击列表项可以切换高亮,点击删除按钮可以移除当前项。
+
+
+ -
+ 事件冒泡
+
+
+ -
+ 事件委托
+
+
+
+
+
+
+
+
diff --git a/04-dom-events-async/06-bubbling-and-delegation/answer.js b/04-dom-events-async/06-bubbling-and-delegation/answer.js
new file mode 100644
index 0000000..5d4c457
--- /dev/null
+++ b/04-dom-events-async/06-bubbling-and-delegation/answer.js
@@ -0,0 +1,27 @@
+const panel = document.getElementById("panel");
+const lessonList = document.getElementById("lesson-list");
+
+panel.addEventListener("click", function () {
+ console.log("点击到了外层面板");
+});
+
+lessonList.addEventListener("click", function (event) {
+ const removeButton = event.target.closest(".remove-btn");
+
+ if (removeButton) {
+ event.stopPropagation();
+ const currentItem = removeButton.closest(".lesson-item");
+
+ if (currentItem) {
+ currentItem.remove();
+ }
+
+ return;
+ }
+
+ const currentItem = event.target.closest(".lesson-item");
+
+ if (currentItem) {
+ currentItem.classList.toggle("active");
+ }
+});
diff --git a/04-dom-events-async/06-bubbling-and-delegation/starter.html b/04-dom-events-async/06-bubbling-and-delegation/starter.html
new file mode 100644
index 0000000..8a9c3f0
--- /dev/null
+++ b/04-dom-events-async/06-bubbling-and-delegation/starter.html
@@ -0,0 +1,58 @@
+
+
+
+
+
+ 冒泡、委托和 stopPropagation
+
+
+
+
+ 事件委托练习
+ 点击列表项可以切换高亮,点击删除按钮可以移除当前项。
+
+
+ -
+ 事件冒泡
+
+
+ -
+ 事件委托
+
+
+
+
+
+
+
+
diff --git a/04-dom-events-async/06-bubbling-and-delegation/starter.js b/04-dom-events-async/06-bubbling-and-delegation/starter.js
new file mode 100644
index 0000000..fcdadae
--- /dev/null
+++ b/04-dom-events-async/06-bubbling-and-delegation/starter.js
@@ -0,0 +1,5 @@
+// 任务:
+// 1. 给 panel 绑定点击事件,输出一条日志
+// 2. 给 lesson-list 绑定点击事件,使用事件委托
+// 3. 点击 li 时切换 active 类名
+// 4. 点击删除按钮时,阻止冒泡并删除当前 li
diff --git a/04-dom-events-async/07-timers-and-async-order/README.md b/04-dom-events-async/07-timers-and-async-order/README.md
new file mode 100644
index 0000000..626ff25
--- /dev/null
+++ b/04-dom-events-async/07-timers-and-async-order/README.md
@@ -0,0 +1,25 @@
+# 练习 7:setTimeout 和异步顺序
+
+## 目标
+
+理解同步代码和异步回调的执行先后顺序。
+
+## 你要练什么
+
+- `setTimeout`
+- 同步顺序
+- 异步回调
+- DOM 日志输出
+
+## 任务
+
+- 点击按钮后先输出“开始执行”
+- 再立刻输出“同步代码结束”
+- 然后延迟输出“异步回调完成”
+
+## 文件
+
+- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/07-timers-and-async-order/starter.html)
+- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/07-timers-and-async-order/starter.js)
+- [answer.html](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/07-timers-and-async-order/answer.html)
+- [answer.js](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/07-timers-and-async-order/answer.js)
diff --git a/04-dom-events-async/07-timers-and-async-order/answer.html b/04-dom-events-async/07-timers-and-async-order/answer.html
new file mode 100644
index 0000000..0b4117d
--- /dev/null
+++ b/04-dom-events-async/07-timers-and-async-order/answer.html
@@ -0,0 +1,38 @@
+
+
+
+
+
+ setTimeout 和异步顺序
+
+
+
+
+
+
+
+
diff --git a/04-dom-events-async/07-timers-and-async-order/answer.js b/04-dom-events-async/07-timers-and-async-order/answer.js
new file mode 100644
index 0000000..ef2ec3b
--- /dev/null
+++ b/04-dom-events-async/07-timers-and-async-order/answer.js
@@ -0,0 +1,20 @@
+const runButton = document.getElementById("run-btn");
+const logList = document.getElementById("log-list");
+
+function appendLog(text) {
+ const item = document.createElement("li");
+ item.textContent = text;
+ logList.appendChild(item);
+}
+
+runButton.addEventListener("click", function () {
+ logList.innerHTML = "";
+
+ appendLog("开始执行");
+
+ setTimeout(function () {
+ appendLog("异步回调完成");
+ }, 600);
+
+ appendLog("同步代码结束");
+});
diff --git a/04-dom-events-async/07-timers-and-async-order/starter.html b/04-dom-events-async/07-timers-and-async-order/starter.html
new file mode 100644
index 0000000..a92d8cd
--- /dev/null
+++ b/04-dom-events-async/07-timers-and-async-order/starter.html
@@ -0,0 +1,38 @@
+
+
+
+
+
+ setTimeout 和异步顺序
+
+
+
+
+
+
+
+
diff --git a/04-dom-events-async/07-timers-and-async-order/starter.js b/04-dom-events-async/07-timers-and-async-order/starter.js
new file mode 100644
index 0000000..4a10696
--- /dev/null
+++ b/04-dom-events-async/07-timers-and-async-order/starter.js
@@ -0,0 +1,6 @@
+// 任务:
+// 1. 获取按钮和日志列表
+// 2. 点击按钮后清空旧日志
+// 3. 先追加“开始执行”
+// 4. 用 setTimeout 延迟追加“异步回调完成”
+// 5. 再立刻追加“同步代码结束”
diff --git a/04-dom-events-async/08-promise-and-render/README.md b/04-dom-events-async/08-promise-and-render/README.md
new file mode 100644
index 0000000..d328c7a
--- /dev/null
+++ b/04-dom-events-async/08-promise-and-render/README.md
@@ -0,0 +1,26 @@
+# 练习 8:Promise 和渲染
+
+## 目标
+
+学会在 Promise 完成后把结果渲染到页面。
+
+## 你要练什么
+
+- Promise
+- `.then()`
+- `catch()`
+- loading 状态
+
+## 任务
+
+- 点击按钮后显示“加载中”
+- 等待 Promise 返回数据
+- 把课程名称渲染到列表
+- 如果失败,显示错误信息
+
+## 文件
+
+- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/08-promise-and-render/starter.html)
+- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/08-promise-and-render/starter.js)
+- [answer.html](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/08-promise-and-render/answer.html)
+- [answer.js](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/08-promise-and-render/answer.js)
diff --git a/04-dom-events-async/08-promise-and-render/answer.html b/04-dom-events-async/08-promise-and-render/answer.html
new file mode 100644
index 0000000..15f6fcc
--- /dev/null
+++ b/04-dom-events-async/08-promise-and-render/answer.html
@@ -0,0 +1,35 @@
+
+
+
+
+
+ Promise 和渲染
+
+
+
+
+ Promise 数据渲染
+
+ 等待加载
+
+
+
+
+
+
diff --git a/04-dom-events-async/08-promise-and-render/answer.js b/04-dom-events-async/08-promise-and-render/answer.js
new file mode 100644
index 0000000..5b08732
--- /dev/null
+++ b/04-dom-events-async/08-promise-and-render/answer.js
@@ -0,0 +1,30 @@
+function fakeFetchCourses() {
+ return new Promise(function (resolve) {
+ setTimeout(function () {
+ resolve(["DOM 获取元素", "事件监听", "异步基础"]);
+ }, 800);
+ });
+}
+
+const loadButton = document.getElementById("load-btn");
+const statusText = document.getElementById("status-text");
+const courseList = document.getElementById("course-list");
+
+loadButton.addEventListener("click", function () {
+ statusText.textContent = "加载中...";
+ courseList.innerHTML = "";
+
+ fakeFetchCourses()
+ .then(function (courses) {
+ courses.forEach(function (course) {
+ const item = document.createElement("li");
+ item.textContent = course;
+ courseList.appendChild(item);
+ });
+
+ statusText.textContent = "加载完成";
+ })
+ .catch(function () {
+ statusText.textContent = "加载失败";
+ });
+});
diff --git a/04-dom-events-async/08-promise-and-render/starter.html b/04-dom-events-async/08-promise-and-render/starter.html
new file mode 100644
index 0000000..f6b33b2
--- /dev/null
+++ b/04-dom-events-async/08-promise-and-render/starter.html
@@ -0,0 +1,35 @@
+
+
+
+
+
+ Promise 和渲染
+
+
+
+
+ Promise 数据渲染
+
+ 等待加载
+
+
+
+
+
+
diff --git a/04-dom-events-async/08-promise-and-render/starter.js b/04-dom-events-async/08-promise-and-render/starter.js
new file mode 100644
index 0000000..9a0df22
--- /dev/null
+++ b/04-dom-events-async/08-promise-and-render/starter.js
@@ -0,0 +1,14 @@
+function fakeFetchCourses() {
+ return new Promise(function (resolve) {
+ setTimeout(function () {
+ resolve(["DOM 获取元素", "事件监听", "异步基础"]);
+ }, 800);
+ });
+}
+
+// 任务:
+// 1. 获取按钮、状态文字、列表
+// 2. 点击按钮后显示“加载中”
+// 3. 调用 fakeFetchCourses()
+// 4. 用 then 渲染课程列表
+// 5. 用 catch 处理错误
diff --git a/04-dom-events-async/09-async-await-panel/README.md b/04-dom-events-async/09-async-await-panel/README.md
new file mode 100644
index 0000000..ecc806a
--- /dev/null
+++ b/04-dom-events-async/09-async-await-panel/README.md
@@ -0,0 +1,26 @@
+# 练习 9:async / await 面板
+
+## 目标
+
+学会用 `async` / `await` 写一个更直观的异步流程。
+
+## 你要练什么
+
+- `async`
+- `await`
+- `try...catch`
+- DOM 更新
+
+## 任务
+
+- 点击按钮后进入加载状态
+- 等待异步函数返回用户信息
+- 把结果渲染到卡片里
+- 如果失败,显示错误信息
+
+## 文件
+
+- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/09-async-await-panel/starter.html)
+- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/09-async-await-panel/starter.js)
+- [answer.html](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/09-async-await-panel/answer.html)
+- [answer.js](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/09-async-await-panel/answer.js)
diff --git a/04-dom-events-async/09-async-await-panel/answer.html b/04-dom-events-async/09-async-await-panel/answer.html
new file mode 100644
index 0000000..437d0b0
--- /dev/null
+++ b/04-dom-events-async/09-async-await-panel/answer.html
@@ -0,0 +1,35 @@
+
+
+
+
+
+ async / await 面板
+
+
+
+
+ 用户信息面板
+
+ 等待加载
+
+
+
+
+
+
diff --git a/04-dom-events-async/09-async-await-panel/answer.js b/04-dom-events-async/09-async-await-panel/answer.js
new file mode 100644
index 0000000..d688245
--- /dev/null
+++ b/04-dom-events-async/09-async-await-panel/answer.js
@@ -0,0 +1,35 @@
+function fakeFetchUser() {
+ return new Promise(function (resolve) {
+ setTimeout(function () {
+ resolve({
+ name: "林晨",
+ role: "前端学习者",
+ focus: "DOM + 事件 + 异步",
+ });
+ }, 900);
+ });
+}
+
+const loadButton = document.getElementById("load-user-btn");
+const statusText = document.getElementById("user-status");
+const userCard = document.getElementById("user-card");
+
+async function loadUser() {
+ statusText.textContent = "加载中...";
+ userCard.innerHTML = "";
+
+ try {
+ const user = await fakeFetchUser();
+
+ userCard.innerHTML = `
+ 姓名:${user.name}
+ 身份:${user.role}
+ 当前重点:${user.focus}
+ `;
+ statusText.textContent = "加载完成";
+ } catch (error) {
+ statusText.textContent = "加载失败";
+ }
+}
+
+loadButton.addEventListener("click", loadUser);
diff --git a/04-dom-events-async/09-async-await-panel/starter.html b/04-dom-events-async/09-async-await-panel/starter.html
new file mode 100644
index 0000000..21f511c
--- /dev/null
+++ b/04-dom-events-async/09-async-await-panel/starter.html
@@ -0,0 +1,35 @@
+
+
+
+
+
+ async / await 面板
+
+
+
+
+ 用户信息面板
+
+ 等待加载
+
+
+
+
+
+
diff --git a/04-dom-events-async/09-async-await-panel/starter.js b/04-dom-events-async/09-async-await-panel/starter.js
new file mode 100644
index 0000000..26538b6
--- /dev/null
+++ b/04-dom-events-async/09-async-await-panel/starter.js
@@ -0,0 +1,18 @@
+function fakeFetchUser() {
+ return new Promise(function (resolve) {
+ setTimeout(function () {
+ resolve({
+ name: "林晨",
+ role: "前端学习者",
+ focus: "DOM + 事件 + 异步",
+ });
+ }, 900);
+ });
+}
+
+// 任务:
+// 1. 获取按钮、状态文字、信息面板
+// 2. 写一个 async 函数
+// 3. 用 await 等待 fakeFetchUser()
+// 4. 渲染用户信息
+// 5. 用 try...catch 处理异常
diff --git a/04-dom-events-async/10-final-dashboard/README.md b/04-dom-events-async/10-final-dashboard/README.md
new file mode 100644
index 0000000..5f77577
--- /dev/null
+++ b/04-dom-events-async/10-final-dashboard/README.md
@@ -0,0 +1,34 @@
+# 练习 10:综合页面
+
+## 目标
+
+把 DOM、事件和异步知识拼起来,做一个可以真实互动的小页面。
+
+## 项目名称
+
+学习面板
+
+## 任务
+
+请完成一个控制台之外可见的页面交互,要求至少包含:
+
+- 一个课程列表区域
+- 一个表单区域
+- 一个添加课程的交互
+- 一个点击课程切换完成状态的交互
+- 一个异步加载提示或远程数据模拟
+- 清晰的状态文案更新
+
+## 建议顺序
+
+1. 先看 HTML 结构
+2. 先写元素获取和渲染函数
+3. 再写表单提交和点击事件
+4. 最后补异步加载逻辑
+
+## 文件
+
+- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/10-final-dashboard/starter.html)
+- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/10-final-dashboard/starter.js)
+- [answer.html](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/10-final-dashboard/answer.html)
+- [answer.js](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/10-final-dashboard/answer.js)
diff --git a/04-dom-events-async/10-final-dashboard/answer.html b/04-dom-events-async/10-final-dashboard/answer.html
new file mode 100644
index 0000000..a32e647
--- /dev/null
+++ b/04-dom-events-async/10-final-dashboard/answer.html
@@ -0,0 +1,82 @@
+
+
+
+
+
+ 学习面板
+
+
+
+
+
+ DOM + 事件 + 异步学习面板
+ 正在初始化页面...
+
+
+
+
+
+
+
+
+
+
diff --git a/04-dom-events-async/10-final-dashboard/answer.js b/04-dom-events-async/10-final-dashboard/answer.js
new file mode 100644
index 0000000..40623cf
--- /dev/null
+++ b/04-dom-events-async/10-final-dashboard/answer.js
@@ -0,0 +1,84 @@
+const lessons = [
+ { title: "获取元素", done: false },
+ { title: "修改 DOM", done: true },
+];
+
+function fakeLoadExtraLessons() {
+ return new Promise(function (resolve) {
+ setTimeout(function () {
+ resolve([
+ { title: "事件委托", done: false },
+ { title: "异步渲染", done: false },
+ ]);
+ }, 900);
+ });
+}
+
+const statusText = document.getElementById("status-text");
+const loadButton = document.getElementById("load-btn");
+const lessonList = document.getElementById("lesson-list");
+const lessonForm = document.getElementById("lesson-form");
+const lessonInput = document.getElementById("lesson-input");
+
+function renderLessons() {
+ lessonList.innerHTML = "";
+
+ lessons.forEach(function (lesson, index) {
+ const item = document.createElement("li");
+ item.className = "lesson-item";
+
+ if (lesson.done) {
+ item.classList.add("done");
+ }
+
+ item.dataset.index = index;
+ item.textContent = lesson.title;
+ lessonList.appendChild(item);
+ });
+}
+
+lessonList.addEventListener("click", function (event) {
+ const currentItem = event.target.closest(".lesson-item");
+
+ if (!currentItem) {
+ return;
+ }
+
+ const index = Number(currentItem.dataset.index);
+ lessons[index].done = !lessons[index].done;
+ renderLessons();
+});
+
+lessonForm.addEventListener("submit", function (event) {
+ event.preventDefault();
+
+ const value = lessonInput.value.trim();
+
+ if (!value) {
+ return;
+ }
+
+ lessons.push({
+ title: value,
+ done: false,
+ });
+
+ lessonInput.value = "";
+ statusText.textContent = "已新增一门课程";
+ renderLessons();
+});
+
+loadButton.addEventListener("click", async function () {
+ statusText.textContent = "正在异步加载课程...";
+
+ const newLessons = await fakeLoadExtraLessons();
+ newLessons.forEach(function (lesson) {
+ lessons.push(lesson);
+ });
+
+ statusText.textContent = "额外课程加载完成";
+ renderLessons();
+});
+
+statusText.textContent = "页面初始化完成";
+renderLessons();
diff --git a/04-dom-events-async/10-final-dashboard/starter.html b/04-dom-events-async/10-final-dashboard/starter.html
new file mode 100644
index 0000000..6406afd
--- /dev/null
+++ b/04-dom-events-async/10-final-dashboard/starter.html
@@ -0,0 +1,82 @@
+
+
+
+
+
+ 学习面板
+
+
+
+
+
+ DOM + 事件 + 异步学习面板
+ 正在初始化页面...
+
+
+
+
+
+
+
+
+
+
diff --git a/04-dom-events-async/10-final-dashboard/starter.js b/04-dom-events-async/10-final-dashboard/starter.js
new file mode 100644
index 0000000..b0cdcfc
--- /dev/null
+++ b/04-dom-events-async/10-final-dashboard/starter.js
@@ -0,0 +1,22 @@
+const lessons = [
+ { title: "获取元素", done: false },
+ { title: "修改 DOM", done: true },
+];
+
+function fakeLoadExtraLessons() {
+ return new Promise(function (resolve) {
+ setTimeout(function () {
+ resolve([
+ { title: "事件委托", done: false },
+ { title: "异步渲染", done: false },
+ ]);
+ }, 900);
+ });
+}
+
+// 任务:
+// 1. 获取页面里的关键元素
+// 2. 写一个 renderLessons 函数,把 lessons 渲染到列表
+// 3. 点击列表项时切换 done 状态
+// 4. 提交表单时阻止默认提交,并新增课程
+// 5. 点击加载按钮时显示加载状态,并把异步返回的课程追加到列表
diff --git a/04-dom-events-async/11-input-live-preview/README.md b/04-dom-events-async/11-input-live-preview/README.md
new file mode 100644
index 0000000..9f0a312
--- /dev/null
+++ b/04-dom-events-async/11-input-live-preview/README.md
@@ -0,0 +1,25 @@
+# 练习 11:input 实时预览
+
+## 目标
+
+学会处理输入类事件,让页面随着用户输入实时变化。
+
+## 你要练什么
+
+- `input` 事件
+- `change` 事件
+- 表单值读取
+- 实时 DOM 更新
+
+## 任务
+
+- 在输入框输入昵称时,实时更新预览标题
+- 在文本域输入学习目标时,实时更新预览内容
+- 切换学习阶段下拉框时,更新阶段标签
+
+## 文件
+
+- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/11-input-live-preview/starter.html)
+- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/11-input-live-preview/starter.js)
+- [answer.html](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/11-input-live-preview/answer.html)
+- [answer.js](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/11-input-live-preview/answer.js)
diff --git a/04-dom-events-async/11-input-live-preview/answer.html b/04-dom-events-async/11-input-live-preview/answer.html
new file mode 100644
index 0000000..df61394
--- /dev/null
+++ b/04-dom-events-async/11-input-live-preview/answer.html
@@ -0,0 +1,86 @@
+
+
+
+
+
+ input 实时预览
+
+
+
+
+
+
+
+ 入门阶段
+ 未填写昵称
+ 这里会显示你的学习目标。
+
+
+
+
+
+
diff --git a/04-dom-events-async/11-input-live-preview/answer.js b/04-dom-events-async/11-input-live-preview/answer.js
new file mode 100644
index 0000000..6ce5f3b
--- /dev/null
+++ b/04-dom-events-async/11-input-live-preview/answer.js
@@ -0,0 +1,21 @@
+const nicknameInput = document.getElementById("nickname-input");
+const goalInput = document.getElementById("goal-input");
+const stageSelect = document.getElementById("stage-select");
+
+const previewStage = document.getElementById("preview-stage");
+const previewName = document.getElementById("preview-name");
+const previewGoal = document.getElementById("preview-goal");
+
+nicknameInput.addEventListener("input", function () {
+ const value = nicknameInput.value.trim();
+ previewName.textContent = value || "未填写昵称";
+});
+
+goalInput.addEventListener("input", function () {
+ const value = goalInput.value.trim();
+ previewGoal.textContent = value || "这里会显示你的学习目标。";
+});
+
+stageSelect.addEventListener("change", function () {
+ previewStage.textContent = stageSelect.value;
+});
diff --git a/04-dom-events-async/11-input-live-preview/starter.html b/04-dom-events-async/11-input-live-preview/starter.html
new file mode 100644
index 0000000..550dfb8
--- /dev/null
+++ b/04-dom-events-async/11-input-live-preview/starter.html
@@ -0,0 +1,86 @@
+
+
+
+
+
+ input 实时预览
+
+
+
+
+
+
+
+ 入门阶段
+ 未填写昵称
+ 这里会显示你的学习目标。
+
+
+
+
+
+
diff --git a/04-dom-events-async/11-input-live-preview/starter.js b/04-dom-events-async/11-input-live-preview/starter.js
new file mode 100644
index 0000000..9ee6472
--- /dev/null
+++ b/04-dom-events-async/11-input-live-preview/starter.js
@@ -0,0 +1,6 @@
+// 任务:
+// 1. 获取昵称输入框、目标文本域、阶段下拉框
+// 2. 监听昵称和目标的 input 事件
+// 3. 把输入内容实时渲染到右侧预览
+// 4. 监听阶段下拉框的 change 事件
+// 5. 更新阶段标签文字
diff --git a/04-dom-events-async/12-prepend-remove-and-link-default/README.md b/04-dom-events-async/12-prepend-remove-and-link-default/README.md
new file mode 100644
index 0000000..c2f1bc5
--- /dev/null
+++ b/04-dom-events-async/12-prepend-remove-and-link-default/README.md
@@ -0,0 +1,25 @@
+# 练习 12:prepend、classList.remove 和链接默认行为
+
+## 目标
+
+补齐几个常见但容易漏掉的 DOM 细节操作。
+
+## 你要练什么
+
+- `prepend()`
+- `classList.remove()`
+- 链接点击事件
+- `preventDefault()`
+
+## 任务
+
+- 点击“插入到最前面”时,把一条新消息插入列表顶部
+- 点击“清除高亮”时,移除全部高亮类名
+- 点击帮助链接时,阻止默认跳转,并在页面显示提示信息
+
+## 文件
+
+- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/12-prepend-remove-and-link-default/starter.html)
+- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/12-prepend-remove-and-link-default/starter.js)
+- [answer.html](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/12-prepend-remove-and-link-default/answer.html)
+- [answer.js](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/12-prepend-remove-and-link-default/answer.js)
diff --git a/04-dom-events-async/12-prepend-remove-and-link-default/answer.html b/04-dom-events-async/12-prepend-remove-and-link-default/answer.html
new file mode 100644
index 0000000..b6b4aa3
--- /dev/null
+++ b/04-dom-events-async/12-prepend-remove-and-link-default/answer.html
@@ -0,0 +1,62 @@
+
+
+
+
+
+ prepend、classList.remove 和链接默认行为
+
+
+
+
+ 补漏练习
+
+
+ 这里会显示操作提示。
+
+
+ - 先学事件监听
+ - 再学异步顺序
+ - 最后做综合页面
+
+
+
+
+
+
diff --git a/04-dom-events-async/12-prepend-remove-and-link-default/answer.js b/04-dom-events-async/12-prepend-remove-and-link-default/answer.js
new file mode 100644
index 0000000..cd259b6
--- /dev/null
+++ b/04-dom-events-async/12-prepend-remove-and-link-default/answer.js
@@ -0,0 +1,31 @@
+const prependButton = document.getElementById("prepend-btn");
+const clearActiveButton = document.getElementById("clear-active-btn");
+const helpLink = document.getElementById("help-link");
+const hintText = document.getElementById("hint-text");
+const messageList = document.getElementById("message-list");
+
+let messageIndex = 1;
+
+prependButton.addEventListener("click", function () {
+ const item = document.createElement("li");
+ item.className = "item active";
+ item.textContent = `新插入的提醒 ${messageIndex}`;
+ messageList.prepend(item);
+ hintText.textContent = "已把一条消息插入到最前面";
+ messageIndex += 1;
+});
+
+clearActiveButton.addEventListener("click", function () {
+ const items = document.querySelectorAll(".item");
+
+ items.forEach(function (item) {
+ item.classList.remove("active");
+ });
+
+ hintText.textContent = "已移除全部高亮状态";
+});
+
+helpLink.addEventListener("click", function (event) {
+ event.preventDefault();
+ hintText.textContent = "已阻止默认跳转";
+});
diff --git a/04-dom-events-async/12-prepend-remove-and-link-default/starter.html b/04-dom-events-async/12-prepend-remove-and-link-default/starter.html
new file mode 100644
index 0000000..f8f3b56
--- /dev/null
+++ b/04-dom-events-async/12-prepend-remove-and-link-default/starter.html
@@ -0,0 +1,62 @@
+
+
+
+
+
+ prepend、classList.remove 和链接默认行为
+
+
+
+
+ 补漏练习
+
+
+ 这里会显示操作提示。
+
+
+ - 先学事件监听
+ - 再学异步顺序
+ - 最后做综合页面
+
+
+
+
+
+
diff --git a/04-dom-events-async/12-prepend-remove-and-link-default/starter.js b/04-dom-events-async/12-prepend-remove-and-link-default/starter.js
new file mode 100644
index 0000000..37879aa
--- /dev/null
+++ b/04-dom-events-async/12-prepend-remove-and-link-default/starter.js
@@ -0,0 +1,6 @@
+// 任务:
+// 1. 获取两个按钮、帮助链接、提示文字和列表
+// 2. 点击 prepend-btn 时创建新 li,并插入到列表最前面
+// 3. 点击 clear-active-btn 时移除所有 item 的 active 类名
+// 4. 点击帮助链接时,用 preventDefault() 阻止跳转
+// 5. 在提示文字里输出“已阻止默认跳转”
diff --git a/04-dom-events-async/README.md b/04-dom-events-async/README.md
new file mode 100644
index 0000000..c83ae34
--- /dev/null
+++ b/04-dom-events-async/README.md
@@ -0,0 +1,176 @@
+# DOM + 事件 + 异步
+
+这部分只解决一个问题:你能不能让页面里的元素被 JavaScript 真正控制起来,并对用户操作和异步结果做出响应。
+
+## 学完后你应该掌握
+
+- 如何获取页面元素
+- 如何修改文本、样式和类名
+- 如何创建、插入和删除节点
+- 如何监听点击、输入、提交等事件
+- `preventDefault()` 和 `stopPropagation()` 的常见场景
+- 事件委托的基本写法
+- `prepend()`、`classList.remove()` 的基础使用
+- `setTimeout` 的异步顺序
+- Promise 的基础用法
+- `async` / `await` 的基础写法
+- 如何把 DOM、事件、异步组合成一个小页面
+
+## 这一章在解决什么
+
+前面三章分别解决了:
+
+- 页面有什么结构
+- 页面长什么样
+- 代码逻辑怎么写
+
+这一章开始,JavaScript 不再只是打印到控制台,而是要真正操作页面。
+
+它回答的是:
+
+- 我怎么拿到某个按钮或输入框
+- 用户点击后页面怎么变
+- 表单提交后怎么处理
+- 数据晚一点回来时页面怎么更新
+
+## 必须建立的 5 个核心意识
+
+### 1. 先选中元素,再操作元素
+
+DOM 操作第一步不是“改”,而是“找到”。
+
+常见方式:
+
+- `getElementById`
+- `querySelector`
+- `querySelectorAll`
+
+### 2. 事件是页面和用户的连接点
+
+页面本身不会“自动响应”。
+
+是你通过事件监听告诉浏览器:
+
+- 点了按钮要做什么
+- 输入框变化后要做什么
+- 提交表单要做什么
+
+### 3. 改页面就是改 DOM
+
+常见 DOM 改动包括:
+
+- 改文字:`textContent`
+- 改类名:`classList`
+- 改样式:`style`
+- 增删节点:`appendChild`、`remove`
+
+### 4. 异步不是“慢一点执行”,而是“先不阻塞主流程”
+
+比如:
+
+- `setTimeout`
+- Promise
+- `async` / `await`
+
+这些都意味着:当前代码先继续往下走,结果稍后回来再处理。
+
+### 5. 一个完整交互通常是“事件 + DOM + 状态 + 异步”
+
+例如点击“加载数据”按钮时:
+
+1. 监听点击事件
+2. 更新 loading 状态
+3. 等待异步结果
+4. 把结果渲染到页面
+
+## 高频 API 速记
+
+### 获取元素
+
+- `document.getElementById()`
+- `document.querySelector()`
+- `document.querySelectorAll()`
+
+### 修改元素
+
+- `textContent`
+- `innerHTML`
+- `style`
+- `classList.add()`
+- `classList.remove()`
+- `classList.toggle()`
+
+### 节点操作
+
+- `document.createElement()`
+- `appendChild()`
+- `prepend()`
+- `remove()`
+
+### 事件
+
+- `addEventListener()`
+- `event.target`
+- `preventDefault()`
+- `stopPropagation()`
+
+### 异步
+
+- `setTimeout()`
+- `Promise`
+- `.then()`
+- `async`
+- `await`
+
+## 学习顺序
+
+1. 获取元素
+2. 修改文本、类名、样式
+3. 创建和删除节点
+4. 点击事件
+5. 表单提交和 `preventDefault`
+6. 冒泡、委托和 `stopPropagation`
+7. `setTimeout` 和异步顺序
+8. Promise 渲染
+9. `async` / `await`
+10. 综合小页面
+11. `input` / `change` 事件
+12. `prepend`、`classList.remove` 和链接默认行为
+
+## 练习目录
+
+- [01-query-selectors/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/01-query-selectors/README.md)
+- [02-text-class-style/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/02-text-class-style/README.md)
+- [03-create-and-remove/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/03-create-and-remove/README.md)
+- [04-click-counter/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/04-click-counter/README.md)
+- [05-form-submit-and-prevent-default/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/05-form-submit-and-prevent-default/README.md)
+- [06-bubbling-and-delegation/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/06-bubbling-and-delegation/README.md)
+- [07-timers-and-async-order/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/07-timers-and-async-order/README.md)
+- [08-promise-and-render/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/08-promise-and-render/README.md)
+- [09-async-await-panel/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/09-async-await-panel/README.md)
+- [10-final-dashboard/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/10-final-dashboard/README.md)
+- [11-input-live-preview/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/11-input-live-preview/README.md)
+- [12-prepend-remove-and-link-default/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/12-prepend-remove-and-link-default/README.md)
+
+## 过关标准
+
+如果你能独立做到下面这些,就说明这一章已经基本过关:
+
+- 能正确选中页面里的元素
+- 能改文字、类名和内联样式
+- 能动态创建和删除列表项
+- 能给按钮、表单绑定事件
+- 能理解事件冒泡和事件委托
+- 能在需要时使用 `preventDefault()` 和 `stopPropagation()`
+- 能处理 `input` / `change` 这类常见表单事件
+- 能使用 `prepend()`、`classList.remove()` 完成简单 DOM 调整
+- 能理解 `setTimeout` 的异步顺序
+- 能用 Promise 和 `async` / `await` 处理一个简单异步流程
+- 能做出一个有真实交互的小页面
+
+## 学习建议
+
+- 每个练习都用浏览器打开 `starter.html`
+- 打开开发者工具,边看页面边看控制台
+- 一个交互没反应时,先确认元素有没有选中
+- 一个异步结果不对时,先打印中间状态
diff --git a/05-es6-plus/01-template-and-destructuring/README.md b/05-es6-plus/01-template-and-destructuring/README.md
new file mode 100644
index 0000000..0b052a2
--- /dev/null
+++ b/05-es6-plus/01-template-and-destructuring/README.md
@@ -0,0 +1,24 @@
+# 练习 1:模板字符串和解构
+
+## 目标
+
+学会用模板字符串拼接信息,并用解构快速读取对象和数组数据。
+
+## 你要练什么
+
+- 模板字符串
+- 对象解构
+- 数组解构
+
+## 任务
+
+- 从 `student` 对象里解构出姓名和阶段
+- 从 `scores` 数组里解构出前两个分数
+- 用模板字符串把这些信息渲染到页面
+
+## 文件
+
+- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/01-template-and-destructuring/starter.html)
+- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/01-template-and-destructuring/starter.js)
+- [answer.html](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/01-template-and-destructuring/answer.html)
+- [answer.js](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/01-template-and-destructuring/answer.js)
diff --git a/05-es6-plus/01-template-and-destructuring/answer.html b/05-es6-plus/01-template-and-destructuring/answer.html
new file mode 100644
index 0000000..00fc6a2
--- /dev/null
+++ b/05-es6-plus/01-template-and-destructuring/answer.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+ 模板字符串和解构
+
+
+
+
+
+
+
diff --git a/05-es6-plus/01-template-and-destructuring/answer.js b/05-es6-plus/01-template-and-destructuring/answer.js
new file mode 100644
index 0000000..abf190f
--- /dev/null
+++ b/05-es6-plus/01-template-and-destructuring/answer.js
@@ -0,0 +1,11 @@
+const student = {
+ name: "林晨",
+ stage: "ES6+",
+};
+
+const scores = [88, 92, 95];
+
+const { name, stage } = student;
+const [firstScore, secondScore] = scores;
+
+document.getElementById("output").textContent = `${name} 正在学习 ${stage},前两次练习分数分别是 ${firstScore} 和 ${secondScore}。`;
diff --git a/05-es6-plus/01-template-and-destructuring/starter.html b/05-es6-plus/01-template-and-destructuring/starter.html
new file mode 100644
index 0000000..876c16e
--- /dev/null
+++ b/05-es6-plus/01-template-and-destructuring/starter.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+ 模板字符串和解构
+
+
+
+
+
+
+
diff --git a/05-es6-plus/01-template-and-destructuring/starter.js b/05-es6-plus/01-template-and-destructuring/starter.js
new file mode 100644
index 0000000..a2445dc
--- /dev/null
+++ b/05-es6-plus/01-template-and-destructuring/starter.js
@@ -0,0 +1,11 @@
+const student = {
+ name: "林晨",
+ stage: "ES6+",
+};
+
+const scores = [88, 92, 95];
+
+// 任务:
+// 1. 解构出 name、stage
+// 2. 解构出前两个分数
+// 3. 用模板字符串把信息写入 #output
diff --git a/05-es6-plus/02-spread-and-rest/README.md b/05-es6-plus/02-spread-and-rest/README.md
new file mode 100644
index 0000000..ff3982e
--- /dev/null
+++ b/05-es6-plus/02-spread-and-rest/README.md
@@ -0,0 +1,24 @@
+# 练习 2:展开运算符和剩余参数
+
+## 目标
+
+学会复制数组、合并对象,以及用剩余参数接收不定数量的参数。
+
+## 你要练什么
+
+- 数组展开
+- 对象展开
+- 剩余参数
+
+## 任务
+
+- 复制一份课程数组并新增一项
+- 基于用户对象生成一个更新后的对象
+- 写一个 `sumScores` 函数,用剩余参数计算总分
+
+## 文件
+
+- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/02-spread-and-rest/starter.html)
+- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/02-spread-and-rest/starter.js)
+- [answer.html](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/02-spread-and-rest/answer.html)
+- [answer.js](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/02-spread-and-rest/answer.js)
diff --git a/05-es6-plus/02-spread-and-rest/answer.html b/05-es6-plus/02-spread-and-rest/answer.html
new file mode 100644
index 0000000..55f5883
--- /dev/null
+++ b/05-es6-plus/02-spread-and-rest/answer.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ 展开运算符和剩余参数
+
+
+
+
+
+
diff --git a/05-es6-plus/02-spread-and-rest/answer.js b/05-es6-plus/02-spread-and-rest/answer.js
new file mode 100644
index 0000000..1ece5b1
--- /dev/null
+++ b/05-es6-plus/02-spread-and-rest/answer.js
@@ -0,0 +1,24 @@
+const tracks = ["HTML", "CSS", "JavaScript"];
+
+const user = {
+ name: "林晨",
+ stage: "基础阶段",
+};
+
+function sumScores(...scores) {
+ return scores.reduce((total, score) => total + score, 0);
+}
+
+const newTracks = [...tracks, "ES6+"];
+const nextUser = { ...user, stage: "现代 JS" };
+const totalScore = sumScores(88, 91, 95);
+
+document.getElementById("output").textContent = JSON.stringify(
+ {
+ newTracks,
+ nextUser,
+ totalScore,
+ },
+ null,
+ 2
+);
diff --git a/05-es6-plus/02-spread-and-rest/starter.html b/05-es6-plus/02-spread-and-rest/starter.html
new file mode 100644
index 0000000..52799ff
--- /dev/null
+++ b/05-es6-plus/02-spread-and-rest/starter.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ 展开运算符和剩余参数
+
+
+
+
+
+
diff --git a/05-es6-plus/02-spread-and-rest/starter.js b/05-es6-plus/02-spread-and-rest/starter.js
new file mode 100644
index 0000000..86fbe35
--- /dev/null
+++ b/05-es6-plus/02-spread-and-rest/starter.js
@@ -0,0 +1,16 @@
+const tracks = ["HTML", "CSS", "JavaScript"];
+
+const user = {
+ name: "林晨",
+ stage: "基础阶段",
+};
+
+function sumScores(...scores) {
+ // 返回总分
+}
+
+// 任务:
+// 1. 复制 tracks 并新增 "ES6+"
+// 2. 基于 user 生成一个 stage 为 "现代 JS" 的新对象
+// 3. 调用 sumScores
+// 4. 输出结果到 #output
diff --git a/05-es6-plus/03-arrow-functions/README.md b/05-es6-plus/03-arrow-functions/README.md
new file mode 100644
index 0000000..cf4631a
--- /dev/null
+++ b/05-es6-plus/03-arrow-functions/README.md
@@ -0,0 +1,23 @@
+# 练习 3:箭头函数
+
+## 目标
+
+学会把简单函数改写成箭头函数。
+
+## 你要练什么
+
+- 箭头函数
+- 简写返回值
+- 数组映射
+
+## 任务
+
+- 把两个普通函数改成箭头函数
+- 用 `map` 和箭头函数生成课程标签
+
+## 文件
+
+- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/03-arrow-functions/starter.html)
+- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/03-arrow-functions/starter.js)
+- [answer.html](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/03-arrow-functions/answer.html)
+- [answer.js](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/03-arrow-functions/answer.js)
diff --git a/05-es6-plus/03-arrow-functions/answer.html b/05-es6-plus/03-arrow-functions/answer.html
new file mode 100644
index 0000000..492e3d7
--- /dev/null
+++ b/05-es6-plus/03-arrow-functions/answer.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ 箭头函数
+
+
+
+
+
+
diff --git a/05-es6-plus/03-arrow-functions/answer.js b/05-es6-plus/03-arrow-functions/answer.js
new file mode 100644
index 0000000..3ded488
--- /dev/null
+++ b/05-es6-plus/03-arrow-functions/answer.js
@@ -0,0 +1,15 @@
+const getLevel = (score) => (score >= 80 ? "达标" : "继续练习");
+const add = (a, b) => a + b;
+
+const tracks = ["DOM", "异步", "模块化"];
+const labels = tracks.map((track) => `[${track}]`);
+
+document.getElementById("output").textContent = JSON.stringify(
+ {
+ level: getLevel(86),
+ sum: add(12, 8),
+ labels,
+ },
+ null,
+ 2
+);
diff --git a/05-es6-plus/03-arrow-functions/starter.html b/05-es6-plus/03-arrow-functions/starter.html
new file mode 100644
index 0000000..1a35351
--- /dev/null
+++ b/05-es6-plus/03-arrow-functions/starter.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ 箭头函数
+
+
+
+
+
+
diff --git a/05-es6-plus/03-arrow-functions/starter.js b/05-es6-plus/03-arrow-functions/starter.js
new file mode 100644
index 0000000..29de476
--- /dev/null
+++ b/05-es6-plus/03-arrow-functions/starter.js
@@ -0,0 +1,14 @@
+function getLevel(score) {
+ return score >= 80 ? "达标" : "继续练习";
+}
+
+function add(a, b) {
+ return a + b;
+}
+
+const tracks = ["DOM", "异步", "模块化"];
+
+// 任务:
+// 1. 把 getLevel 改成箭头函数
+// 2. 把 add 改成箭头函数
+// 3. 用 map + 箭头函数生成 ["[DOM]", ...]
diff --git a/05-es6-plus/04-modules-basic/README.md b/05-es6-plus/04-modules-basic/README.md
new file mode 100644
index 0000000..52c8632
--- /dev/null
+++ b/05-es6-plus/04-modules-basic/README.md
@@ -0,0 +1,24 @@
+# 练习 4:模块基础
+
+## 目标
+
+学会用 `export` 和 `import` 把不同文件连起来。
+
+## 你要练什么
+
+- `export`
+- `import`
+- `type="module"`
+
+## 任务
+
+- 从模块文件里导入课程数据和格式化函数
+- 把结果渲染到页面
+
+## 文件
+
+- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/04-modules-basic/starter.html)
+- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/04-modules-basic/starter.js)
+- [course-data.js](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/04-modules-basic/course-data.js)
+- [answer.html](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/04-modules-basic/answer.html)
+- [answer.js](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/04-modules-basic/answer.js)
diff --git a/05-es6-plus/04-modules-basic/answer.html b/05-es6-plus/04-modules-basic/answer.html
new file mode 100644
index 0000000..146ebbd
--- /dev/null
+++ b/05-es6-plus/04-modules-basic/answer.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ 模块基础
+
+
+
+
+
+
diff --git a/05-es6-plus/04-modules-basic/answer.js b/05-es6-plus/04-modules-basic/answer.js
new file mode 100644
index 0000000..3356d57
--- /dev/null
+++ b/05-es6-plus/04-modules-basic/answer.js
@@ -0,0 +1,4 @@
+import { courses, formatCourse } from "./course-data.js";
+
+const lines = courses.map((course) => formatCourse(course));
+document.getElementById("output").textContent = lines.join("\n");
diff --git a/05-es6-plus/04-modules-basic/course-data.js b/05-es6-plus/04-modules-basic/course-data.js
new file mode 100644
index 0000000..9036db4
--- /dev/null
+++ b/05-es6-plus/04-modules-basic/course-data.js
@@ -0,0 +1,3 @@
+export const courses = ["解构", "展开运算符", "Promise"];
+
+export const formatCourse = (course) => `正在学习:${course}`;
diff --git a/05-es6-plus/04-modules-basic/starter.html b/05-es6-plus/04-modules-basic/starter.html
new file mode 100644
index 0000000..06ece18
--- /dev/null
+++ b/05-es6-plus/04-modules-basic/starter.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ 模块基础
+
+
+
+
+
+
diff --git a/05-es6-plus/04-modules-basic/starter.js b/05-es6-plus/04-modules-basic/starter.js
new file mode 100644
index 0000000..e5cd937
--- /dev/null
+++ b/05-es6-plus/04-modules-basic/starter.js
@@ -0,0 +1,3 @@
+// 任务:
+// 1. 从 ./course-data.js 导入 courses 和 formatCourse
+// 2. 把格式化后的结果写入 #output
diff --git a/05-es6-plus/05-promise-basics/README.md b/05-es6-plus/05-promise-basics/README.md
new file mode 100644
index 0000000..87389ed
--- /dev/null
+++ b/05-es6-plus/05-promise-basics/README.md
@@ -0,0 +1,24 @@
+# 练习 5:Promise 基础
+
+## 目标
+
+学会用 Promise 表达异步结果。
+
+## 你要练什么
+
+- Promise
+- `.then()`
+- `.catch()`
+
+## 任务
+
+- 调用模拟请求函数
+- 成功时渲染课程数据
+- 失败时渲染错误状态
+
+## 文件
+
+- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/05-promise-basics/starter.html)
+- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/05-promise-basics/starter.js)
+- [answer.html](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/05-promise-basics/answer.html)
+- [answer.js](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/05-promise-basics/answer.js)
diff --git a/05-es6-plus/05-promise-basics/answer.html b/05-es6-plus/05-promise-basics/answer.html
new file mode 100644
index 0000000..14a7966
--- /dev/null
+++ b/05-es6-plus/05-promise-basics/answer.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+ Promise 基础
+
+
+ 等待加载
+
+
+
+
diff --git a/05-es6-plus/05-promise-basics/answer.js b/05-es6-plus/05-promise-basics/answer.js
new file mode 100644
index 0000000..9e75840
--- /dev/null
+++ b/05-es6-plus/05-promise-basics/answer.js
@@ -0,0 +1,21 @@
+function loadTracks() {
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ resolve(["模板字符串", "模块化", "async/await"]);
+ }, 700);
+ });
+}
+
+const status = document.getElementById("status");
+const output = document.getElementById("output");
+
+status.textContent = "加载中...";
+
+loadTracks()
+ .then((tracks) => {
+ status.textContent = "加载完成";
+ output.textContent = tracks.join("\n");
+ })
+ .catch(() => {
+ status.textContent = "加载失败";
+ });
diff --git a/05-es6-plus/05-promise-basics/starter.html b/05-es6-plus/05-promise-basics/starter.html
new file mode 100644
index 0000000..9d38c2f
--- /dev/null
+++ b/05-es6-plus/05-promise-basics/starter.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+ Promise 基础
+
+
+ 等待加载
+
+
+
+
diff --git a/05-es6-plus/05-promise-basics/starter.js b/05-es6-plus/05-promise-basics/starter.js
new file mode 100644
index 0000000..5f85a80
--- /dev/null
+++ b/05-es6-plus/05-promise-basics/starter.js
@@ -0,0 +1,12 @@
+function loadTracks() {
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ resolve(["模板字符串", "模块化", "async/await"]);
+ }, 700);
+ });
+}
+
+// 任务:
+// 1. 调用 loadTracks()
+// 2. 成功时更新状态和输出内容
+// 3. 失败时更新状态
diff --git a/05-es6-plus/06-async-await/README.md b/05-es6-plus/06-async-await/README.md
new file mode 100644
index 0000000..c1f4554
--- /dev/null
+++ b/05-es6-plus/06-async-await/README.md
@@ -0,0 +1,24 @@
+# 练习 6:async / await
+
+## 目标
+
+学会用 `async` / `await` 改写一个 Promise 流程。
+
+## 你要练什么
+
+- `async`
+- `await`
+- `try...catch`
+
+## 任务
+
+- 写一个 `async` 函数等待课程配置
+- 成功时渲染结果
+- 失败时更新状态
+
+## 文件
+
+- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/06-async-await/starter.html)
+- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/06-async-await/starter.js)
+- [answer.html](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/06-async-await/answer.html)
+- [answer.js](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/06-async-await/answer.js)
diff --git a/05-es6-plus/06-async-await/answer.html b/05-es6-plus/06-async-await/answer.html
new file mode 100644
index 0000000..5901a63
--- /dev/null
+++ b/05-es6-plus/06-async-await/answer.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+ async / await
+
+
+ 等待加载
+
+
+
+
diff --git a/05-es6-plus/06-async-await/answer.js b/05-es6-plus/06-async-await/answer.js
new file mode 100644
index 0000000..2b620fa
--- /dev/null
+++ b/05-es6-plus/06-async-await/answer.js
@@ -0,0 +1,27 @@
+function loadConfig() {
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ resolve({
+ title: "现代 JS 面板",
+ level: "进阶",
+ });
+ }, 800);
+ });
+}
+
+const status = document.getElementById("status");
+const output = document.getElementById("output");
+
+async function renderConfig() {
+ status.textContent = "加载中...";
+
+ try {
+ const config = await loadConfig();
+ status.textContent = "加载完成";
+ output.textContent = `${config.title} - ${config.level}`;
+ } catch (error) {
+ status.textContent = "加载失败";
+ }
+}
+
+renderConfig();
diff --git a/05-es6-plus/06-async-await/starter.html b/05-es6-plus/06-async-await/starter.html
new file mode 100644
index 0000000..8bad475
--- /dev/null
+++ b/05-es6-plus/06-async-await/starter.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+ async / await
+
+
+ 等待加载
+
+
+
+
diff --git a/05-es6-plus/06-async-await/starter.js b/05-es6-plus/06-async-await/starter.js
new file mode 100644
index 0000000..3b027f2
--- /dev/null
+++ b/05-es6-plus/06-async-await/starter.js
@@ -0,0 +1,15 @@
+function loadConfig() {
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ resolve({
+ title: "现代 JS 面板",
+ level: "进阶",
+ });
+ }, 800);
+ });
+}
+
+// 任务:
+// 1. 写一个 async 函数
+// 2. 用 await 等待 loadConfig()
+// 3. 用 try...catch 处理流程
diff --git a/05-es6-plus/07-arrow-this/README.md b/05-es6-plus/07-arrow-this/README.md
new file mode 100644
index 0000000..7ddb194
--- /dev/null
+++ b/05-es6-plus/07-arrow-this/README.md
@@ -0,0 +1,23 @@
+# 练习 7:箭头函数里的 this
+
+## 目标
+
+理解箭头函数不会创建自己的 `this`。
+
+## 你要练什么
+
+- 箭头函数
+- `this`
+- 定时器回调
+
+## 任务
+
+- 在对象方法里用箭头函数处理 `setTimeout`
+- 让延迟输出仍然拿到当前对象的名称
+
+## 文件
+
+- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/07-arrow-this/starter.html)
+- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/07-arrow-this/starter.js)
+- [answer.html](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/07-arrow-this/answer.html)
+- [answer.js](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/07-arrow-this/answer.js)
diff --git a/05-es6-plus/07-arrow-this/answer.html b/05-es6-plus/07-arrow-this/answer.html
new file mode 100644
index 0000000..cffba6e
--- /dev/null
+++ b/05-es6-plus/07-arrow-this/answer.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ 箭头函数里的 this
+
+
+
+
+
+
diff --git a/05-es6-plus/07-arrow-this/answer.js b/05-es6-plus/07-arrow-this/answer.js
new file mode 100644
index 0000000..d37fd7b
--- /dev/null
+++ b/05-es6-plus/07-arrow-this/answer.js
@@ -0,0 +1,10 @@
+const trainer = {
+ name: "现代 JS 训练营",
+ report() {
+ setTimeout(() => {
+ document.getElementById("output").textContent = `当前模块:${this.name}`;
+ }, 500);
+ },
+};
+
+trainer.report();
diff --git a/05-es6-plus/07-arrow-this/starter.html b/05-es6-plus/07-arrow-this/starter.html
new file mode 100644
index 0000000..4d2be44
--- /dev/null
+++ b/05-es6-plus/07-arrow-this/starter.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ 箭头函数里的 this
+
+
+
+
+
+
diff --git a/05-es6-plus/07-arrow-this/starter.js b/05-es6-plus/07-arrow-this/starter.js
new file mode 100644
index 0000000..1d60c79
--- /dev/null
+++ b/05-es6-plus/07-arrow-this/starter.js
@@ -0,0 +1,11 @@
+const trainer = {
+ name: "现代 JS 训练营",
+ report() {
+ // 任务:
+ // 1. 用 setTimeout
+ // 2. 在回调里用箭头函数读取 this.name
+ // 3. 把结果写入 #output
+ },
+};
+
+trainer.report();
diff --git a/05-es6-plus/08-fetch-and-json/README.md b/05-es6-plus/08-fetch-and-json/README.md
new file mode 100644
index 0000000..8a326b8
--- /dev/null
+++ b/05-es6-plus/08-fetch-and-json/README.md
@@ -0,0 +1,26 @@
+# 练习 8:fetch 和 JSON
+
+## 目标
+
+补齐 `fetch()` 和 `res.json()` 这一层最常见的现代异步写法。
+
+## 你要练什么
+
+- `fetch()`
+- `await`
+- `res.json()`
+- 接口数据渲染
+
+## 任务
+
+- 点击按钮后发起一次 `fetch`
+- 等待返回的响应对象
+- 调用 `res.json()` 解析 JSON
+- 把课程标题和内容渲染到页面
+
+## 文件
+
+- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/08-fetch-and-json/starter.html)
+- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/08-fetch-and-json/starter.js)
+- [answer.html](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/08-fetch-and-json/answer.html)
+- [answer.js](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/08-fetch-and-json/answer.js)
diff --git a/05-es6-plus/08-fetch-and-json/answer.html b/05-es6-plus/08-fetch-and-json/answer.html
new file mode 100644
index 0000000..a732f51
--- /dev/null
+++ b/05-es6-plus/08-fetch-and-json/answer.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+ fetch 和 JSON
+
+
+ fetch 和 JSON
+
+ 等待加载
+
+
+
+
+
diff --git a/05-es6-plus/08-fetch-and-json/answer.js b/05-es6-plus/08-fetch-and-json/answer.js
new file mode 100644
index 0000000..e591e84
--- /dev/null
+++ b/05-es6-plus/08-fetch-and-json/answer.js
@@ -0,0 +1,19 @@
+const loadButton = document.getElementById("load-btn");
+const status = document.getElementById("status");
+const output = document.getElementById("output");
+
+async function loadPost() {
+ status.textContent = "加载中...";
+
+ try {
+ const res = await fetch("https://jsonplaceholder.typicode.com/posts/1");
+ const data = await res.json();
+
+ status.textContent = "加载完成";
+ output.textContent = `标题:${data.title}\n\n内容:${data.body}`;
+ } catch (error) {
+ status.textContent = "加载失败";
+ }
+}
+
+loadButton.addEventListener("click", loadPost);
diff --git a/05-es6-plus/08-fetch-and-json/starter.html b/05-es6-plus/08-fetch-and-json/starter.html
new file mode 100644
index 0000000..032046a
--- /dev/null
+++ b/05-es6-plus/08-fetch-and-json/starter.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+ fetch 和 JSON
+
+
+ fetch 和 JSON
+
+ 等待加载
+
+
+
+
+
diff --git a/05-es6-plus/08-fetch-and-json/starter.js b/05-es6-plus/08-fetch-and-json/starter.js
new file mode 100644
index 0000000..3a22d55
--- /dev/null
+++ b/05-es6-plus/08-fetch-and-json/starter.js
@@ -0,0 +1,11 @@
+const loadButton = document.getElementById("load-btn");
+const status = document.getElementById("status");
+const output = document.getElementById("output");
+
+// 任务:
+// 1. 点击按钮后把状态改成“加载中...”
+// 2. 用 fetch 请求 https://jsonplaceholder.typicode.com/posts/1
+// 3. 用 await 等待响应对象
+// 4. 调用 res.json() 解析数据
+// 5. 把 title 和 body 渲染到页面
+// 6. 失败时显示“加载失败”
diff --git a/05-es6-plus/09-final-modern-js/README.md b/05-es6-plus/09-final-modern-js/README.md
new file mode 100644
index 0000000..76fb986
--- /dev/null
+++ b/05-es6-plus/09-final-modern-js/README.md
@@ -0,0 +1,28 @@
+# 练习 9:综合页面
+
+## 目标
+
+把 ES6+ 的关键语法拼起来,做一个现代 JS 小页面。
+
+## 项目名称
+
+现代 JS 学习摘要页
+
+## 任务
+
+请完成一个小页面,要求至少包含:
+
+- 模块导入
+- 解构
+- 展开运算符
+- 模板字符串
+- 箭头函数
+- Promise 或 `async` / `await`
+
+## 文件
+
+- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/09-final-modern-js/starter.html)
+- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/09-final-modern-js/starter.js)
+- [summary-service.js](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/09-final-modern-js/summary-service.js)
+- [answer.html](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/09-final-modern-js/answer.html)
+- [answer.js](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/09-final-modern-js/answer.js)
diff --git a/05-es6-plus/09-final-modern-js/answer.html b/05-es6-plus/09-final-modern-js/answer.html
new file mode 100644
index 0000000..11bdebb
--- /dev/null
+++ b/05-es6-plus/09-final-modern-js/answer.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+ 现代 JS 学习摘要页
+
+
+ 等待加载
+ 等待渲染
+
+
+
+
+
diff --git a/05-es6-plus/09-final-modern-js/answer.js b/05-es6-plus/09-final-modern-js/answer.js
new file mode 100644
index 0000000..b92015b
--- /dev/null
+++ b/05-es6-plus/09-final-modern-js/answer.js
@@ -0,0 +1,22 @@
+import { baseSummary, loadExtraSkills } from "./summary-service.js";
+
+const title = document.getElementById("title");
+const intro = document.getElementById("intro");
+const skillList = document.getElementById("skill-list");
+
+async function renderPage() {
+ const extraSkills = await loadExtraSkills();
+ const { name, stage, skills } = baseSummary;
+ const allSkills = [...skills, ...extraSkills];
+
+ title.textContent = `${name} 的 ${stage} 学习摘要`;
+ intro.textContent = `当前已覆盖 ${allSkills.length} 个现代 JS 关键点。`;
+
+ allSkills.forEach((skill) => {
+ const item = document.createElement("li");
+ item.textContent = `已掌握:${skill}`;
+ skillList.appendChild(item);
+ });
+}
+
+renderPage();
diff --git a/05-es6-plus/09-final-modern-js/starter.html b/05-es6-plus/09-final-modern-js/starter.html
new file mode 100644
index 0000000..6a94ed0
--- /dev/null
+++ b/05-es6-plus/09-final-modern-js/starter.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+ 现代 JS 学习摘要页
+
+
+ 等待加载
+ 等待渲染
+
+
+
+
+
diff --git a/05-es6-plus/09-final-modern-js/starter.js b/05-es6-plus/09-final-modern-js/starter.js
new file mode 100644
index 0000000..d99d101
--- /dev/null
+++ b/05-es6-plus/09-final-modern-js/starter.js
@@ -0,0 +1,7 @@
+// 任务:
+// 1. 从 ./summary-service.js 导入数据
+// 2. 用 async / await 获取额外技能
+// 3. 解构 name、stage、skills
+// 4. 用展开运算符合并技能
+// 5. 用模板字符串渲染标题和说明
+// 6. 用 forEach 或 map + 箭头函数渲染列表
diff --git a/05-es6-plus/09-final-modern-js/summary-service.js b/05-es6-plus/09-final-modern-js/summary-service.js
new file mode 100644
index 0000000..8765d41
--- /dev/null
+++ b/05-es6-plus/09-final-modern-js/summary-service.js
@@ -0,0 +1,12 @@
+export const baseSummary = {
+ name: "林晨",
+ stage: "ES6+",
+ skills: ["模板字符串", "解构", "展开运算符"],
+};
+
+export const loadExtraSkills = () =>
+ new Promise((resolve) => {
+ setTimeout(() => {
+ resolve(["模块化", "Promise", "async/await"]);
+ }, 700);
+ });
diff --git a/05-es6-plus/README.md b/05-es6-plus/README.md
new file mode 100644
index 0000000..dc476af
--- /dev/null
+++ b/05-es6-plus/README.md
@@ -0,0 +1,151 @@
+# ES6+(现代 JS)
+
+这部分只解决一个问题:你能不能用现代 JavaScript 的语法和模块能力,把代码写得更清晰、更工程化。
+
+## 学完后你应该掌握
+
+- 模板字符串
+- 对象和数组解构
+- 展开运算符和剩余参数
+- 箭头函数的基础写法
+- 箭头函数和 `this` 的常见差异
+- `import` / `export` 的基础用法
+- `fetch()` 和 `res.json()` 的基础用法
+- Promise 的基础链式写法
+- `async` / `await` 的基础写法
+- 如何把这些能力组合成一个现代 JS 小页面
+
+## 这一章在解决什么
+
+JavaScript 本体解决的是逻辑表达。
+
+ES6+ 这一章解决的是:
+
+- 代码怎么写得更短、更清楚
+- 多文件之间怎么建立依赖
+- 异步代码怎么写得更自然
+
+它回答的是:
+
+- 对象里的值怎么快速取出来
+- 数组和对象怎么复制或合并
+- 回调函数怎么写得更紧凑
+- 多个 JS 文件怎么互相导入
+- Promise 和 `async` / `await` 怎么替代层层回调
+
+## 必须建立的 5 个核心意识
+
+### 1. ES6+ 不是“炫技语法”,而是为了减少样板代码
+
+比如:
+
+- 模板字符串比字符串拼接更直观
+- 解构让取值更直接
+- 展开运算符让复制和合并更清楚
+
+### 2. 语法糖不改变逻辑本质
+
+例如:
+
+- 箭头函数还是函数
+- `async` / `await` 本质上仍然建立在 Promise 上
+
+### 3. 模块化是现代前端代码的基础
+
+一个文件只做一件事,然后通过:
+
+- `export`
+- `import`
+
+把能力组合起来。
+
+### 4. Promise 和 `async` / `await` 是为了解决异步可读性
+
+不是让异步消失,而是让异步流程更好读、更好维护。
+
+### 5. 优先理解“什么时候用”,再记语法
+
+比如:
+
+- 需要字符串插值时用模板字符串
+- 需要快速取对象字段时用解构
+- 需要复制对象和数组时用展开运算符
+- 需要跨文件协作时用模块
+
+## 高频语法速记
+
+### 模板字符串
+
+- `` `Hello ${name}` ``
+
+### 解构
+
+- `const { name } = user`
+- `const [first, second] = list`
+
+### 展开和剩余
+
+- `const newArr = [...arr]`
+- `const newUser = { ...user, age: 21 }`
+- `function sum(...numbers) {}`
+
+### 箭头函数
+
+- `const add = (a, b) => a + b`
+
+### 模块
+
+- `export`
+- `import`
+
+### 异步
+
+- `Promise`
+- `.then()`
+- `async`
+- `await`
+
+## 学习顺序
+
+1. 模板字符串和解构
+2. 展开运算符和剩余参数
+3. 箭头函数
+4. 模块基础
+5. Promise
+6. `async` / `await`
+7. 箭头函数里的 `this`
+8. `fetch` 和 JSON
+9. 综合练习
+
+## 练习目录
+
+- [01-template-and-destructuring/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/01-template-and-destructuring/README.md)
+- [02-spread-and-rest/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/02-spread-and-rest/README.md)
+- [03-arrow-functions/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/03-arrow-functions/README.md)
+- [04-modules-basic/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/04-modules-basic/README.md)
+- [05-promise-basics/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/05-promise-basics/README.md)
+- [06-async-await/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/06-async-await/README.md)
+- [07-arrow-this/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/07-arrow-this/README.md)
+- [08-fetch-and-json/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/08-fetch-and-json/README.md)
+- [09-final-modern-js/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/09-final-modern-js/README.md)
+
+## 过关标准
+
+如果你能独立做到下面这些,就说明这一章已经基本过关:
+
+- 能用模板字符串代替拼接
+- 能用解构快速读取对象和数组数据
+- 能用展开运算符合并对象、复制数组
+- 能写简单的箭头函数
+- 能理解箭头函数的 `this` 和普通函数的差异
+- 能写出基础的 `import` / `export`
+- 能读懂 `fetch()` 返回值,并通过 `res.json()` 取到数据
+- 能用 Promise 和 `async` / `await` 写一个简单异步流程
+- 能把这些语法组合进一个小页面
+
+## 学习建议
+
+- 先把语法写顺,再去追求更短
+- 一次只替换一类旧写法,避免混乱
+- 学模块时一定要看清“谁导出、谁导入”
+- 学异步时先看执行顺序,再看语法形式
diff --git a/06-typescript/01-js-vs-ts/README.md b/06-typescript/01-js-vs-ts/README.md
new file mode 100644
index 0000000..44eeaa7
--- /dev/null
+++ b/06-typescript/01-js-vs-ts/README.md
@@ -0,0 +1,21 @@
+# 练习 1:JavaScript 和 TypeScript 的差别
+
+## 目标
+
+理解 TypeScript 为什么能在写代码时提前发现类型问题。
+
+## 你要练什么
+
+- 参数类型
+- 返回值类型
+- 类型报错的意义
+
+## 任务
+
+- 观察 `add` 函数的类型标注
+- 看懂为什么 `add(1, "2")` 会在 TypeScript 里报错
+
+## 文件
+
+- [starter.ts](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/01-js-vs-ts/starter.ts)
+- [answer.ts](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/01-js-vs-ts/answer.ts)
diff --git a/06-typescript/01-js-vs-ts/answer.ts b/06-typescript/01-js-vs-ts/answer.ts
new file mode 100644
index 0000000..050b4e3
--- /dev/null
+++ b/06-typescript/01-js-vs-ts/answer.ts
@@ -0,0 +1,10 @@
+function add(a: number, b: number): number {
+ return a + b;
+}
+
+const result = add(1, 2);
+console.log(result);
+
+// const wrongResult = add(1, "2");
+// TypeScript 会在写代码阶段直接提示:
+// 第二个参数应该是 number,但这里传入了 string。
diff --git a/06-typescript/01-js-vs-ts/starter.ts b/06-typescript/01-js-vs-ts/starter.ts
new file mode 100644
index 0000000..7f7a1a4
--- /dev/null
+++ b/06-typescript/01-js-vs-ts/starter.ts
@@ -0,0 +1,11 @@
+function add(a: number, b: number): number {
+ return a + b;
+}
+
+const result = add(1, 2);
+console.log(result);
+
+// 任务:
+// 1. 观察 add 的参数和返回值类型
+// 2. 尝试把下面这行取消注释,看看 TypeScript 为什么会报错
+// const wrongResult = add(1, "2");
diff --git a/06-typescript/02-basic-type-annotations/README.md b/06-typescript/02-basic-type-annotations/README.md
new file mode 100644
index 0000000..64de115
--- /dev/null
+++ b/06-typescript/02-basic-type-annotations/README.md
@@ -0,0 +1,21 @@
+# 练习 2:基本类型标注
+
+## 目标
+
+学会给最常见的值加上类型标注。
+
+## 你要练什么
+
+- `number`
+- `string`
+- `boolean`
+
+## 任务
+
+- 给年龄、姓名、付费状态加类型
+- 再输出一段清晰的学习信息
+
+## 文件
+
+- [starter.ts](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/02-basic-type-annotations/starter.ts)
+- [answer.ts](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/02-basic-type-annotations/answer.ts)
diff --git a/06-typescript/02-basic-type-annotations/answer.ts b/06-typescript/02-basic-type-annotations/answer.ts
new file mode 100644
index 0000000..b200bab
--- /dev/null
+++ b/06-typescript/02-basic-type-annotations/answer.ts
@@ -0,0 +1,5 @@
+let age: number = 21;
+let userName: string = "林晨";
+let isPaid: boolean = true;
+
+console.log(`${userName} 今年 ${age} 岁,课程付费状态:${isPaid}`);
diff --git a/06-typescript/02-basic-type-annotations/starter.ts b/06-typescript/02-basic-type-annotations/starter.ts
new file mode 100644
index 0000000..2267730
--- /dev/null
+++ b/06-typescript/02-basic-type-annotations/starter.ts
@@ -0,0 +1,7 @@
+let age: number = 18;
+let userName: string = "Tom";
+let isPaid: boolean = true;
+
+// 任务:
+// 1. 修改上面的值,让它们更像一个学习者资料
+// 2. 输出一句完整信息
diff --git a/06-typescript/03-array-and-function-types/README.md b/06-typescript/03-array-and-function-types/README.md
new file mode 100644
index 0000000..3c32e37
--- /dev/null
+++ b/06-typescript/03-array-and-function-types/README.md
@@ -0,0 +1,22 @@
+# 练习 3:数组和函数类型
+
+## 目标
+
+学会给数组和函数写类型。
+
+## 你要练什么
+
+- `number[]`
+- 参数类型
+- 返回值类型
+
+## 任务
+
+- 定义一个数字数组
+- 写一个 `sum` 函数
+- 让它接收两个数字并返回数字结果
+
+## 文件
+
+- [starter.ts](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/03-array-and-function-types/starter.ts)
+- [answer.ts](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/03-array-and-function-types/answer.ts)
diff --git a/06-typescript/03-array-and-function-types/answer.ts b/06-typescript/03-array-and-function-types/answer.ts
new file mode 100644
index 0000000..979ec0b
--- /dev/null
+++ b/06-typescript/03-array-and-function-types/answer.ts
@@ -0,0 +1,10 @@
+const scoreList: number[] = [82, 90, 95];
+
+function sum(a: number, b: number): number {
+ return a + b;
+}
+
+const total = sum(88, 12);
+
+console.log(scoreList);
+console.log(total);
diff --git a/06-typescript/03-array-and-function-types/starter.ts b/06-typescript/03-array-and-function-types/starter.ts
new file mode 100644
index 0000000..f17d2a9
--- /dev/null
+++ b/06-typescript/03-array-and-function-types/starter.ts
@@ -0,0 +1,11 @@
+const scoreList: number[] = [82, 90, 95];
+
+function sum(a: number, b: number): number {
+ // 返回两个数字之和
+ return 0;
+}
+
+// 任务:
+// 1. 改写 sum 的返回值
+// 2. 调用 sum
+// 3. 输出 scoreList 和 sum 结果
diff --git a/06-typescript/04-interface-object-shape/README.md b/06-typescript/04-interface-object-shape/README.md
new file mode 100644
index 0000000..2bdf4ed
--- /dev/null
+++ b/06-typescript/04-interface-object-shape/README.md
@@ -0,0 +1,22 @@
+# 练习 4:interface 描述对象结构
+
+## 目标
+
+学会用 `interface` 给对象建立结构约束。
+
+## 你要练什么
+
+- `interface`
+- 对象结构
+- 类型约束
+
+## 任务
+
+- 写一个 `User` 接口
+- 用它约束一个学习者对象
+- 输出对象中的关键信息
+
+## 文件
+
+- [starter.ts](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/04-interface-object-shape/starter.ts)
+- [answer.ts](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/04-interface-object-shape/answer.ts)
diff --git a/06-typescript/04-interface-object-shape/answer.ts b/06-typescript/04-interface-object-shape/answer.ts
new file mode 100644
index 0000000..1a2ade1
--- /dev/null
+++ b/06-typescript/04-interface-object-shape/answer.ts
@@ -0,0 +1,14 @@
+interface User {
+ id: number;
+ name: string;
+ age: number;
+}
+
+const user: User = {
+ id: 1,
+ name: "林晨",
+ age: 21,
+};
+
+console.log(user.name);
+console.log(user.age);
diff --git a/06-typescript/04-interface-object-shape/starter.ts b/06-typescript/04-interface-object-shape/starter.ts
new file mode 100644
index 0000000..fa5177d
--- /dev/null
+++ b/06-typescript/04-interface-object-shape/starter.ts
@@ -0,0 +1,15 @@
+interface User {
+ id: number;
+ name: string;
+ age: number;
+}
+
+const user: User = {
+ id: 1,
+ name: "Tom",
+ age: 20,
+};
+
+// 任务:
+// 1. 把对象内容改成学习者资料
+// 2. 输出 name 和 age
diff --git a/06-typescript/05-generic-functions/README.md b/06-typescript/05-generic-functions/README.md
new file mode 100644
index 0000000..c49fdd8
--- /dev/null
+++ b/06-typescript/05-generic-functions/README.md
@@ -0,0 +1,21 @@
+# 练习 5:泛型函数
+
+## 目标
+
+理解泛型是在保持输入输出类型一致。
+
+## 你要练什么
+
+- 泛型 ``
+- 输入输出同类型
+
+## 任务
+
+- 写一个 `getData` 泛型函数
+- 传入什么类型,就返回什么类型
+- 分别用数字和字符串调用
+
+## 文件
+
+- [starter.ts](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/05-generic-functions/starter.ts)
+- [answer.ts](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/05-generic-functions/answer.ts)
diff --git a/06-typescript/05-generic-functions/answer.ts b/06-typescript/05-generic-functions/answer.ts
new file mode 100644
index 0000000..760cc0a
--- /dev/null
+++ b/06-typescript/05-generic-functions/answer.ts
@@ -0,0 +1,9 @@
+function getData(data: T): T {
+ return data;
+}
+
+const numberResult = getData(1);
+const stringResult = getData("abc");
+
+console.log(numberResult);
+console.log(stringResult);
diff --git a/06-typescript/05-generic-functions/starter.ts b/06-typescript/05-generic-functions/starter.ts
new file mode 100644
index 0000000..461ae75
--- /dev/null
+++ b/06-typescript/05-generic-functions/starter.ts
@@ -0,0 +1,9 @@
+function getData(data: T): T {
+ // 返回 data
+ return data;
+}
+
+// 任务:
+// 1. 用 number 调用
+// 2. 用 string 调用
+// 3. 输出结果
diff --git a/06-typescript/06-union-and-optional-props/README.md b/06-typescript/06-union-and-optional-props/README.md
new file mode 100644
index 0000000..e9eeca8
--- /dev/null
+++ b/06-typescript/06-union-and-optional-props/README.md
@@ -0,0 +1,22 @@
+# 练习 6:联合类型和可选属性
+
+## 目标
+
+学会处理不固定的数据结构。
+
+## 你要练什么
+
+- 联合类型
+- 可选属性
+- 接口扩展场景
+
+## 任务
+
+- 定义一个带可选年龄的 `User` 接口
+- 声明一个 `id`,它可以是数字或字符串
+- 分别创建有年龄和没有年龄的对象
+
+## 文件
+
+- [starter.ts](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/06-union-and-optional-props/starter.ts)
+- [answer.ts](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/06-union-and-optional-props/answer.ts)
diff --git a/06-typescript/06-union-and-optional-props/answer.ts b/06-typescript/06-union-and-optional-props/answer.ts
new file mode 100644
index 0000000..b391c7c
--- /dev/null
+++ b/06-typescript/06-union-and-optional-props/answer.ts
@@ -0,0 +1,19 @@
+let id: number | string = "user-1";
+
+interface User {
+ name: string;
+ age?: number;
+}
+
+const userA: User = {
+ name: "林晨",
+ age: 21,
+};
+
+const userB: User = {
+ name: "小周",
+};
+
+console.log(id);
+console.log(userA);
+console.log(userB);
diff --git a/06-typescript/06-union-and-optional-props/starter.ts b/06-typescript/06-union-and-optional-props/starter.ts
new file mode 100644
index 0000000..e88ff3b
--- /dev/null
+++ b/06-typescript/06-union-and-optional-props/starter.ts
@@ -0,0 +1,12 @@
+let id: number | string = 1;
+
+interface User {
+ name: string;
+ age?: number;
+}
+
+// 任务:
+// 1. 把 id 改成字符串也试一次
+// 2. 创建一个带 age 的 userA
+// 3. 创建一个不带 age 的 userB
+// 4. 输出它们
diff --git a/06-typescript/07-type-safe-renderer/README.md b/06-typescript/07-type-safe-renderer/README.md
new file mode 100644
index 0000000..0e12046
--- /dev/null
+++ b/06-typescript/07-type-safe-renderer/README.md
@@ -0,0 +1,22 @@
+# 练习 7:类型安全渲染
+
+## 目标
+
+把接口、数组和函数类型组合起来,处理一组结构化数据。
+
+## 你要练什么
+
+- `interface`
+- 数组类型
+- 函数返回值类型
+
+## 任务
+
+- 定义课程接口
+- 创建课程数组
+- 写一个渲染函数,返回字符串数组
+
+## 文件
+
+- [starter.ts](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/07-type-safe-renderer/starter.ts)
+- [answer.ts](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/07-type-safe-renderer/answer.ts)
diff --git a/06-typescript/07-type-safe-renderer/answer.ts b/06-typescript/07-type-safe-renderer/answer.ts
new file mode 100644
index 0000000..8aff99e
--- /dev/null
+++ b/06-typescript/07-type-safe-renderer/answer.ts
@@ -0,0 +1,19 @@
+interface Course {
+ title: string;
+ lessons: number;
+ finished: boolean;
+}
+
+const courses: Course[] = [
+ { title: "TypeScript 基础", lessons: 8, finished: true },
+ { title: "接口和泛型", lessons: 6, finished: false },
+];
+
+function renderCourseLines(list: Course[]): string[] {
+ return list.map((course) => {
+ const status = course.finished ? "已完成" : "学习中";
+ return `${course.title} - ${course.lessons} 节 - ${status}`;
+ });
+}
+
+console.log(renderCourseLines(courses));
diff --git a/06-typescript/07-type-safe-renderer/starter.ts b/06-typescript/07-type-safe-renderer/starter.ts
new file mode 100644
index 0000000..cae82dc
--- /dev/null
+++ b/06-typescript/07-type-safe-renderer/starter.ts
@@ -0,0 +1,19 @@
+interface Course {
+ title: string;
+ lessons: number;
+ finished: boolean;
+}
+
+const courses: Course[] = [
+ { title: "TypeScript 基础", lessons: 8, finished: true },
+ { title: "接口和泛型", lessons: 6, finished: false },
+];
+
+function renderCourseLines(list: Course[]): string[] {
+ // 返回渲染后的字符串数组
+ return [];
+}
+
+// 任务:
+// 1. 实现 renderCourseLines
+// 2. 输出结果
diff --git a/06-typescript/08-final-mini-app/README.md b/06-typescript/08-final-mini-app/README.md
new file mode 100644
index 0000000..8f2559e
--- /dev/null
+++ b/06-typescript/08-final-mini-app/README.md
@@ -0,0 +1,24 @@
+# 练习 8:综合小练习
+
+## 目标
+
+把基础类型、接口、泛型和联合类型组合起来,完成一个小型 TypeScript 数据模型。
+
+## 项目名称
+
+类型安全学习摘要
+
+## 任务
+
+请完成一个小程序,要求至少包含:
+
+- 一个 `Student` 接口
+- 一个课程数组类型
+- 一个联合类型字段
+- 一个泛型函数
+- 一个格式化输出函数
+
+## 文件
+
+- [starter.ts](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/08-final-mini-app/starter.ts)
+- [answer.ts](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/08-final-mini-app/answer.ts)
diff --git a/06-typescript/08-final-mini-app/answer.ts b/06-typescript/08-final-mini-app/answer.ts
new file mode 100644
index 0000000..00acd85
--- /dev/null
+++ b/06-typescript/08-final-mini-app/answer.ts
@@ -0,0 +1,36 @@
+type StudentId = number | string;
+
+interface Course {
+ title: string;
+ finished: boolean;
+}
+
+interface Student {
+ id: StudentId;
+ name: string;
+ age?: number;
+ courses: Course[];
+}
+
+function pickFirst(list: T[]): T {
+ return list[0];
+}
+
+function formatStudent(student: Student): string {
+ const courseCount = student.courses.length;
+ return `${student.name} 当前有 ${courseCount} 门课程,编号是 ${student.id}`;
+}
+
+const student: Student = {
+ id: "stu-1",
+ name: "林晨",
+ courses: [
+ { title: "基本类型", finished: true },
+ { title: "接口", finished: false },
+ ],
+};
+
+const firstCourse = pickFirst(student.courses);
+
+console.log(formatStudent(student));
+console.log(firstCourse);
diff --git a/06-typescript/08-final-mini-app/starter.ts b/06-typescript/08-final-mini-app/starter.ts
new file mode 100644
index 0000000..1216ff1
--- /dev/null
+++ b/06-typescript/08-final-mini-app/starter.ts
@@ -0,0 +1,36 @@
+type StudentId = number | string;
+
+interface Course {
+ title: string;
+ finished: boolean;
+}
+
+interface Student {
+ id: StudentId;
+ name: string;
+ age?: number;
+ courses: Course[];
+}
+
+function pickFirst(list: T[]): T {
+ return list[0];
+}
+
+function formatStudent(student: Student): string {
+ // 返回一段摘要文字
+ return "";
+}
+
+const student: Student = {
+ id: "stu-1",
+ name: "林晨",
+ courses: [
+ { title: "基本类型", finished: true },
+ { title: "接口", finished: false },
+ ],
+};
+
+// 任务:
+// 1. 实现 formatStudent
+// 2. 用 pickFirst 取第一门课程
+// 3. 输出摘要和第一门课程
diff --git a/06-typescript/README.md b/06-typescript/README.md
new file mode 100644
index 0000000..5af8792
--- /dev/null
+++ b/06-typescript/README.md
@@ -0,0 +1,170 @@
+# TypeScript(类型系统)
+
+这部分只解决一个问题:你能不能在写代码阶段就让错误尽量提前暴露,而不是把问题留到运行时。
+
+## 学完后你应该掌握
+
+- TypeScript 和 JavaScript 的差别
+- 基本类型标注
+- 数组类型和函数参数/返回值类型
+- `interface` 描述对象结构
+- 泛型函数的基本写法
+- 联合类型和可选属性
+- 如何用类型约束一组业务数据
+- 如何把这些类型能力组合成一个小程序
+
+## TypeScript 是什么
+
+TypeScript 不是让程序“更能运行”,而是让程序“更不容易写错”。
+
+它回答的是:
+
+- 这个值应该是什么类型
+- 这个函数该接收什么参数
+- 这个对象应该有哪些字段
+- 一组数据之间的结构是否一致
+
+如果 JavaScript 更关注“能不能表达逻辑”,那么 TypeScript 更关注“表达出来的逻辑有没有类型保障”。
+
+## 必须建立的 5 个核心意识
+
+### 1. 类型信息是写给人和工具看的约束
+
+```ts
+function add(a: number, b: number): number {
+ return a + b;
+}
+```
+
+这段代码的重点不是语法更复杂,而是把“这个函数只能接收数字”说清楚了。
+
+### 2. TypeScript 的价值主要发生在运行前
+
+TypeScript 很多时候不是修复 bug,而是阻止 bug 被写进去。
+
+### 3. 对象结构要先约定,再使用
+
+当数据开始变复杂时,优先考虑用 `interface` 或类型别名描述结构。
+
+### 4. 泛型是在复用“结构能力”
+
+泛型不是高难技巧,它本质上是在说:
+
+- 这个函数可以处理很多类型
+- 但输入和输出之间的类型关系要保持一致
+
+### 5. 类型要服务于业务,不要为了类型而类型
+
+目标不是把代码写得最花,而是让代码更清晰、更稳。
+
+## 高频概念速记
+
+### 基本类型
+
+- `number`
+- `string`
+- `boolean`
+
+### 容器类型
+
+- `number[]`
+- `string[]`
+- `Array`
+
+### 函数类型
+
+- 参数类型
+- 返回值类型
+
+### 对象结构
+
+- `interface`
+
+### 进阶类型
+
+- 泛型 ``
+- 联合类型 `|`
+- 可选属性 `?`
+
+## 学习顺序
+
+1. JavaScript 和 TypeScript 的差别
+2. 基本类型标注
+3. 数组和函数类型
+4. `interface`
+5. 泛型
+6. 联合类型和可选属性
+7. 类型安全渲染
+8. 综合小练习
+
+## 练习目录
+
+- [01-js-vs-ts/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/01-js-vs-ts/README.md)
+- [02-basic-type-annotations/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/02-basic-type-annotations/README.md)
+- [03-array-and-function-types/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/03-array-and-function-types/README.md)
+- [04-interface-object-shape/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/04-interface-object-shape/README.md)
+- [05-generic-functions/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/05-generic-functions/README.md)
+- [06-union-and-optional-props/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/06-union-and-optional-props/README.md)
+- [07-type-safe-renderer/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/07-type-safe-renderer/README.md)
+- [08-final-mini-app/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/08-final-mini-app/README.md)
+
+## 过关标准
+
+如果你能独立做到下面这些,就说明这一章已经基本过关:
+
+- 能给基本值加类型标注
+- 能给数组和函数写正确类型
+- 能用 `interface` 描述对象结构
+- 能写一个简单泛型函数
+- 能理解联合类型和可选属性的使用场景
+- 能看懂 TypeScript 在写代码阶段提示的问题
+- 能把类型约束用到一个小型数据模型里
+
+## 学习建议
+
+- 先保证类型写对,再考虑写得更少
+- 报错时先看“期望类型”和“实际类型”分别是什么
+- 不确定对象结构时,先写接口再写数据
+- 遇到泛型不要先背定义,先看输入输出是不是保持同类关系
+
+## 运行调试
+
+这一章现在已经做成了 `Vite + TypeScript` 工程。
+
+它保留了原来的 8 个练习目录,同时新增了一个统一的学习面板,你可以在浏览器里切换章节、查看 `starter.ts`、查看 `answer.ts`,并直接看到示例输出。
+
+### 启动方式
+
+在 [06-typescript](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript) 目录执行:
+
+```bash
+npm install
+npm run dev
+```
+
+### 常用命令
+
+```bash
+npm run dev
+npm run build
+npm run preview
+npm run check
+npm run check:app
+npm run check:lessons
+```
+
+### 工程入口
+
+- 入口页面:[index.html](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/index.html)
+- 应用入口:[src/main.ts](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/src/main.ts)
+- 章节数据:[src/lessons.ts](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/src/lessons.ts)
+- 页面样式:[src/style.css](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/src/style.css)
+- 包管理配置:[package.json](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/package.json)
+- TypeScript 配置:[tsconfig.json](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/tsconfig.json)
+
+### 说明
+
+- `npm run check` 会先检查 `src/` 下的 Vite 入口代码,再逐个检查每个练习目录里的 `starter.ts` 和 `answer.ts`
+- `npm run check:app` 只检查 Vite 应用入口
+- `npm run check:lessons` 会按文件逐个检查 8 组练习,避免不同练习之间的全局变量互相污染
+- 原来的练习目录仍然保留,继续作为题目和答案素材使用
diff --git a/06-typescript/index.html b/06-typescript/index.html
new file mode 100644
index 0000000..ad9e758
--- /dev/null
+++ b/06-typescript/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+ TypeScript Learning Lab
+
+
+
+
+
+
diff --git a/06-typescript/package-lock.json b/06-typescript/package-lock.json
new file mode 100644
index 0000000..131a095
--- /dev/null
+++ b/06-typescript/package-lock.json
@@ -0,0 +1,913 @@
+{
+ "name": "06-typescript",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "06-typescript",
+ "version": "1.0.0",
+ "dependencies": {
+ "pnpm": "^10.32.1"
+ },
+ "devDependencies": {
+ "typescript": "latest",
+ "vite": "latest"
+ }
+ },
+ "node_modules/@emnapi/core": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.0.tgz",
+ "integrity": "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.2.0",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz",
+ "integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/wasi-threads": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz",
+ "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@napi-rs/wasm-runtime": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz",
+ "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "^1.7.1",
+ "@emnapi/runtime": "^1.7.1",
+ "@tybys/wasm-util": "^0.10.1"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@oxc-project/runtime": {
+ "version": "0.115.0",
+ "resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.115.0.tgz",
+ "integrity": "sha512-Rg8Wlt5dCbXhQnsXPrkOjL1DTSvXLgb2R/KYfnf1/K+R0k6UMLEmbQXPM+kwrWqSmWA2t0B1EtHy2/3zikQpvQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxc-project/types": {
+ "version": "0.115.0",
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.115.0.tgz",
+ "integrity": "sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/Boshen"
+ }
+ },
+ "node_modules/@rolldown/binding-android-arm64": {
+ "version": "1.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.9.tgz",
+ "integrity": "sha512-lcJL0bN5hpgJfSIz/8PIf02irmyL43P+j1pTCfbD1DbLkmGRuFIA4DD3B3ZOvGqG0XiVvRznbKtN0COQVaKUTg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-arm64": {
+ "version": "1.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.9.tgz",
+ "integrity": "sha512-J7Zk3kLYFsLtuH6U+F4pS2sYVzac0qkjcO5QxHS7OS7yZu2LRs+IXo+uvJ/mvpyUljDJ3LROZPoQfgBIpCMhdQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-x64": {
+ "version": "1.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.9.tgz",
+ "integrity": "sha512-iwtmmghy8nhfRGeNAIltcNXzD0QMNaaA5U/NyZc1Ia4bxrzFByNMDoppoC+hl7cDiUq5/1CnFthpT9n+UtfFyg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-freebsd-x64": {
+ "version": "1.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.9.tgz",
+ "integrity": "sha512-DLFYI78SCiZr5VvdEplsVC2Vx53lnA4/Ga5C65iyldMVaErr86aiqCoNBLl92PXPfDtUYjUh+xFFor40ueNs4Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
+ "version": "1.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.9.tgz",
+ "integrity": "sha512-CsjTmTwd0Hri6iTw/DRMK7kOZ7FwAkrO4h8YWKoX/kcj833e4coqo2wzIFywtch/8Eb5enQ/lwLM7w6JX1W5RQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-gnu": {
+ "version": "1.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.9.tgz",
+ "integrity": "sha512-2x9O2JbSPxpxMDhP9Z74mahAStibTlrBMW0520+epJH5sac7/LwZW5Bmg/E6CXuEF53JJFW509uP+lSedaUNxg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-musl": {
+ "version": "1.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.9.tgz",
+ "integrity": "sha512-JA1QRW31ogheAIRhIg9tjMfsYbglXXYGNPLdPEYrwFxdbkQCAzvpSCSHCDWNl4hTtrol8WeboCSEpjdZK8qrCg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-ppc64-gnu": {
+ "version": "1.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.9.tgz",
+ "integrity": "sha512-aOKU9dJheda8Kj8Y3w9gnt9QFOO+qKPAl8SWd7JPHP+Cu0EuDAE5wokQubLzIDQWg2myXq2XhTpOVS07qqvT+w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-s390x-gnu": {
+ "version": "1.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.9.tgz",
+ "integrity": "sha512-OalO94fqj7IWRn3VdXWty75jC5dk4C197AWEuMhIpvVv2lw9fiPhud0+bW2ctCxb3YoBZor71QHbY+9/WToadA==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-gnu": {
+ "version": "1.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.9.tgz",
+ "integrity": "sha512-cVEl1vZtBsBZna3YMjGXNvnYYrOJ7RzuWvZU0ffvJUexWkukMaDuGhUXn0rjnV0ptzGVkvc+vW9Yqy6h8YX4pg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-musl": {
+ "version": "1.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.9.tgz",
+ "integrity": "sha512-UzYnKCIIc4heAKgI4PZ3dfBGUZefGCJ1TPDuLHoCzgrMYPb5Rv6TLFuYtyM4rWyHM7hymNdsg5ik2C+UD9VDbA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-openharmony-arm64": {
+ "version": "1.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.9.tgz",
+ "integrity": "sha512-+6zoiF+RRyf5cdlFQP7nm58mq7+/2PFaY2DNQeD4B87N36JzfF/l9mdBkkmTvSYcYPE8tMh/o3cRlsx1ldLfog==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-wasm32-wasi": {
+ "version": "1.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.9.tgz",
+ "integrity": "sha512-rgFN6sA/dyebil3YTlL2evvi/M+ivhfnyxec7AccTpRPccno/rPoNlqybEZQBkcbZu8Hy+eqNJCqfBR8P7Pg8g==",
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@napi-rs/wasm-runtime": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-arm64-msvc": {
+ "version": "1.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.9.tgz",
+ "integrity": "sha512-lHVNUG/8nlF1IQk1C0Ci574qKYyty2goMiPlRqkC5R+3LkXDkL5Dhx8ytbxq35m+pkHVIvIxviD+TWLdfeuadA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-x64-msvc": {
+ "version": "1.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.9.tgz",
+ "integrity": "sha512-G0oA4+w1iY5AGi5HcDTxWsoxF509hrFIPB2rduV5aDqS9FtDg1CAfa7V34qImbjfhIcA8C+RekocJZA96EarwQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.9.tgz",
+ "integrity": "sha512-w6oiRWgEBl04QkFZgmW+jnU1EC9b57Oihi2ot3HNWIQRqgHp5PnYDia5iZ5FF7rpa4EQdiqMDXjlqKGXBhsoXw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tybys/wasm-util": {
+ "version": "0.10.1",
+ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
+ "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/lightningcss": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
+ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.32.0",
+ "lightningcss-darwin-arm64": "1.32.0",
+ "lightningcss-darwin-x64": "1.32.0",
+ "lightningcss-freebsd-x64": "1.32.0",
+ "lightningcss-linux-arm-gnueabihf": "1.32.0",
+ "lightningcss-linux-arm64-gnu": "1.32.0",
+ "lightningcss-linux-arm64-musl": "1.32.0",
+ "lightningcss-linux-x64-gnu": "1.32.0",
+ "lightningcss-linux-x64-musl": "1.32.0",
+ "lightningcss-win32-arm64-msvc": "1.32.0",
+ "lightningcss-win32-x64-msvc": "1.32.0"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
+ "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
+ "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
+ "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
+ "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
+ "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
+ "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
+ "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
+ "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
+ "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
+ "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
+ "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pnpm": {
+ "version": "10.32.1",
+ "resolved": "https://registry.npmjs.org/pnpm/-/pnpm-10.32.1.tgz",
+ "integrity": "sha512-pwaTjw6JrBRWtlY+q07fHR+vM2jRGR/FxZeQ6W3JGORFarLmfWE94QQ9LoyB+HMD5rQNT/7KnfFe8a1Wc0jyvg==",
+ "license": "MIT",
+ "bin": {
+ "pnpm": "bin/pnpm.cjs",
+ "pnpx": "bin/pnpx.cjs"
+ },
+ "engines": {
+ "node": ">=18.12"
+ },
+ "funding": {
+ "url": "https://opencollective.com/pnpm"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.8",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
+ "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/rolldown": {
+ "version": "1.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.9.tgz",
+ "integrity": "sha512-9EbgWge7ZH+yqb4d2EnELAntgPTWbfL8ajiTW+SyhJEC4qhBbkCKbqFV4Ge4zmu5ziQuVbWxb/XwLZ+RIO7E8Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@oxc-project/types": "=0.115.0",
+ "@rolldown/pluginutils": "1.0.0-rc.9"
+ },
+ "bin": {
+ "rolldown": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "optionalDependencies": {
+ "@rolldown/binding-android-arm64": "1.0.0-rc.9",
+ "@rolldown/binding-darwin-arm64": "1.0.0-rc.9",
+ "@rolldown/binding-darwin-x64": "1.0.0-rc.9",
+ "@rolldown/binding-freebsd-x64": "1.0.0-rc.9",
+ "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.9",
+ "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.9",
+ "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.9",
+ "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.9",
+ "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.9",
+ "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.9",
+ "@rolldown/binding-linux-x64-musl": "1.0.0-rc.9",
+ "@rolldown/binding-openharmony-arm64": "1.0.0-rc.9",
+ "@rolldown/binding-wasm32-wasi": "1.0.0-rc.9",
+ "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.9",
+ "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.9"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "dev": true,
+ "license": "0BSD",
+ "optional": true
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/vite": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.0.tgz",
+ "integrity": "sha512-fPGaRNj9Zytaf8LEiBhY7Z6ijnFKdzU/+mL8EFBaKr7Vw1/FWcTBAMW0wLPJAGMPX38ZPVCVgLceWiEqeoqL2Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@oxc-project/runtime": "0.115.0",
+ "lightningcss": "^1.32.0",
+ "picomatch": "^4.0.3",
+ "postcss": "^8.5.8",
+ "rolldown": "1.0.0-rc.9",
+ "tinyglobby": "^0.2.15"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "@vitejs/devtools": "^0.0.0-alpha.31",
+ "esbuild": "^0.27.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "@vitejs/devtools": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ }
+ }
+}
diff --git a/06-typescript/package.json b/06-typescript/package.json
new file mode 100644
index 0000000..1c11691
--- /dev/null
+++ b/06-typescript/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "06-typescript",
+ "private": true,
+ "version": "1.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview",
+ "check": "tsc --noEmit -p ./tsconfig.json && node ./scripts/check-lessons.mjs",
+ "check:app": "tsc --noEmit -p ./tsconfig.json",
+ "check:lessons": "node ./scripts/check-lessons.mjs"
+ },
+ "devDependencies": {
+ "typescript": "latest",
+ "vite": "latest"
+ },
+ "dependencies": {
+ "pnpm": "^10.32.1"
+ }
+}
diff --git a/06-typescript/pnpm-lock.yaml b/06-typescript/pnpm-lock.yaml
new file mode 100644
index 0000000..7ac9d3a
--- /dev/null
+++ b/06-typescript/pnpm-lock.yaml
@@ -0,0 +1,518 @@
+lockfileVersion: '9.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+importers:
+
+ .:
+ dependencies:
+ pnpm:
+ specifier: ^10.32.1
+ version: 10.32.1
+ devDependencies:
+ typescript:
+ specifier: latest
+ version: 5.9.3
+ vite:
+ specifier: latest
+ version: 8.0.0
+
+packages:
+
+ '@emnapi/core@1.9.0':
+ resolution: {integrity: sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==}
+
+ '@emnapi/runtime@1.9.0':
+ resolution: {integrity: sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==}
+
+ '@emnapi/wasi-threads@1.2.0':
+ resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==}
+
+ '@napi-rs/wasm-runtime@1.1.1':
+ resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==}
+
+ '@oxc-project/runtime@0.115.0':
+ resolution: {integrity: sha512-Rg8Wlt5dCbXhQnsXPrkOjL1DTSvXLgb2R/KYfnf1/K+R0k6UMLEmbQXPM+kwrWqSmWA2t0B1EtHy2/3zikQpvQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+
+ '@oxc-project/types@0.115.0':
+ resolution: {integrity: sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==}
+
+ '@rolldown/binding-android-arm64@1.0.0-rc.9':
+ resolution: {integrity: sha512-lcJL0bN5hpgJfSIz/8PIf02irmyL43P+j1pTCfbD1DbLkmGRuFIA4DD3B3ZOvGqG0XiVvRznbKtN0COQVaKUTg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [android]
+
+ '@rolldown/binding-darwin-arm64@1.0.0-rc.9':
+ resolution: {integrity: sha512-J7Zk3kLYFsLtuH6U+F4pS2sYVzac0qkjcO5QxHS7OS7yZu2LRs+IXo+uvJ/mvpyUljDJ3LROZPoQfgBIpCMhdQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@rolldown/binding-darwin-x64@1.0.0-rc.9':
+ resolution: {integrity: sha512-iwtmmghy8nhfRGeNAIltcNXzD0QMNaaA5U/NyZc1Ia4bxrzFByNMDoppoC+hl7cDiUq5/1CnFthpT9n+UtfFyg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [darwin]
+
+ '@rolldown/binding-freebsd-x64@1.0.0-rc.9':
+ resolution: {integrity: sha512-DLFYI78SCiZr5VvdEplsVC2Vx53lnA4/Ga5C65iyldMVaErr86aiqCoNBLl92PXPfDtUYjUh+xFFor40ueNs4Q==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.9':
+ resolution: {integrity: sha512-CsjTmTwd0Hri6iTw/DRMK7kOZ7FwAkrO4h8YWKoX/kcj833e4coqo2wzIFywtch/8Eb5enQ/lwLM7w6JX1W5RQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm]
+ os: [linux]
+
+ '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.9':
+ resolution: {integrity: sha512-2x9O2JbSPxpxMDhP9Z74mahAStibTlrBMW0520+epJH5sac7/LwZW5Bmg/E6CXuEF53JJFW509uP+lSedaUNxg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rolldown/binding-linux-arm64-musl@1.0.0-rc.9':
+ resolution: {integrity: sha512-JA1QRW31ogheAIRhIg9tjMfsYbglXXYGNPLdPEYrwFxdbkQCAzvpSCSHCDWNl4hTtrol8WeboCSEpjdZK8qrCg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.9':
+ resolution: {integrity: sha512-aOKU9dJheda8Kj8Y3w9gnt9QFOO+qKPAl8SWd7JPHP+Cu0EuDAE5wokQubLzIDQWg2myXq2XhTpOVS07qqvT+w==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [ppc64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.9':
+ resolution: {integrity: sha512-OalO94fqj7IWRn3VdXWty75jC5dk4C197AWEuMhIpvVv2lw9fiPhud0+bW2ctCxb3YoBZor71QHbY+9/WToadA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [s390x]
+ os: [linux]
+ libc: [glibc]
+
+ '@rolldown/binding-linux-x64-gnu@1.0.0-rc.9':
+ resolution: {integrity: sha512-cVEl1vZtBsBZna3YMjGXNvnYYrOJ7RzuWvZU0ffvJUexWkukMaDuGhUXn0rjnV0ptzGVkvc+vW9Yqy6h8YX4pg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rolldown/binding-linux-x64-musl@1.0.0-rc.9':
+ resolution: {integrity: sha512-UzYnKCIIc4heAKgI4PZ3dfBGUZefGCJ1TPDuLHoCzgrMYPb5Rv6TLFuYtyM4rWyHM7hymNdsg5ik2C+UD9VDbA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@rolldown/binding-openharmony-arm64@1.0.0-rc.9':
+ resolution: {integrity: sha512-+6zoiF+RRyf5cdlFQP7nm58mq7+/2PFaY2DNQeD4B87N36JzfF/l9mdBkkmTvSYcYPE8tMh/o3cRlsx1ldLfog==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@rolldown/binding-wasm32-wasi@1.0.0-rc.9':
+ resolution: {integrity: sha512-rgFN6sA/dyebil3YTlL2evvi/M+ivhfnyxec7AccTpRPccno/rPoNlqybEZQBkcbZu8Hy+eqNJCqfBR8P7Pg8g==}
+ engines: {node: '>=14.0.0'}
+ cpu: [wasm32]
+
+ '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.9':
+ resolution: {integrity: sha512-lHVNUG/8nlF1IQk1C0Ci574qKYyty2goMiPlRqkC5R+3LkXDkL5Dhx8ytbxq35m+pkHVIvIxviD+TWLdfeuadA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [win32]
+
+ '@rolldown/binding-win32-x64-msvc@1.0.0-rc.9':
+ resolution: {integrity: sha512-G0oA4+w1iY5AGi5HcDTxWsoxF509hrFIPB2rduV5aDqS9FtDg1CAfa7V34qImbjfhIcA8C+RekocJZA96EarwQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [win32]
+
+ '@rolldown/pluginutils@1.0.0-rc.9':
+ resolution: {integrity: sha512-w6oiRWgEBl04QkFZgmW+jnU1EC9b57Oihi2ot3HNWIQRqgHp5PnYDia5iZ5FF7rpa4EQdiqMDXjlqKGXBhsoXw==}
+
+ '@tybys/wasm-util@0.10.1':
+ resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
+
+ detect-libc@2.1.2:
+ resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
+ engines: {node: '>=8'}
+
+ fdir@6.5.0:
+ resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
+ engines: {node: '>=12.0.0'}
+ peerDependencies:
+ picomatch: ^3 || ^4
+ peerDependenciesMeta:
+ picomatch:
+ optional: true
+
+ fsevents@2.3.3:
+ resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
+ lightningcss-android-arm64@1.32.0:
+ resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [android]
+
+ lightningcss-darwin-arm64@1.32.0:
+ resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [darwin]
+
+ lightningcss-darwin-x64@1.32.0:
+ resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [darwin]
+
+ lightningcss-freebsd-x64@1.32.0:
+ resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [freebsd]
+
+ lightningcss-linux-arm-gnueabihf@1.32.0:
+ resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm]
+ os: [linux]
+
+ lightningcss-linux-arm64-gnu@1.32.0:
+ resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ lightningcss-linux-arm64-musl@1.32.0:
+ resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ lightningcss-linux-x64-gnu@1.32.0:
+ resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ lightningcss-linux-x64-musl@1.32.0:
+ resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ lightningcss-win32-arm64-msvc@1.32.0:
+ resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [win32]
+
+ lightningcss-win32-x64-msvc@1.32.0:
+ resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [win32]
+
+ lightningcss@1.32.0:
+ resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
+ engines: {node: '>= 12.0.0'}
+
+ nanoid@3.3.11:
+ resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+ hasBin: true
+
+ picocolors@1.1.1:
+ resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
+
+ picomatch@4.0.3:
+ resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
+ engines: {node: '>=12'}
+
+ pnpm@10.32.1:
+ resolution: {integrity: sha512-pwaTjw6JrBRWtlY+q07fHR+vM2jRGR/FxZeQ6W3JGORFarLmfWE94QQ9LoyB+HMD5rQNT/7KnfFe8a1Wc0jyvg==}
+ engines: {node: '>=18.12'}
+ hasBin: true
+
+ postcss@8.5.8:
+ resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==}
+ engines: {node: ^10 || ^12 || >=14}
+
+ rolldown@1.0.0-rc.9:
+ resolution: {integrity: sha512-9EbgWge7ZH+yqb4d2EnELAntgPTWbfL8ajiTW+SyhJEC4qhBbkCKbqFV4Ge4zmu5ziQuVbWxb/XwLZ+RIO7E8Q==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+
+ source-map-js@1.2.1:
+ resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+ engines: {node: '>=0.10.0'}
+
+ tinyglobby@0.2.15:
+ resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
+ engines: {node: '>=12.0.0'}
+
+ tslib@2.8.1:
+ resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
+
+ typescript@5.9.3:
+ resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
+ engines: {node: '>=14.17'}
+ hasBin: true
+
+ vite@8.0.0:
+ resolution: {integrity: sha512-fPGaRNj9Zytaf8LEiBhY7Z6ijnFKdzU/+mL8EFBaKr7Vw1/FWcTBAMW0wLPJAGMPX38ZPVCVgLceWiEqeoqL2Q==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+ peerDependencies:
+ '@types/node': ^20.19.0 || >=22.12.0
+ '@vitejs/devtools': ^0.0.0-alpha.31
+ esbuild: ^0.27.0
+ jiti: '>=1.21.0'
+ less: ^4.0.0
+ sass: ^1.70.0
+ sass-embedded: ^1.70.0
+ stylus: '>=0.54.8'
+ sugarss: ^5.0.0
+ terser: ^5.16.0
+ tsx: ^4.8.1
+ yaml: ^2.4.2
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+ '@vitejs/devtools':
+ optional: true
+ esbuild:
+ optional: true
+ jiti:
+ optional: true
+ less:
+ optional: true
+ sass:
+ optional: true
+ sass-embedded:
+ optional: true
+ stylus:
+ optional: true
+ sugarss:
+ optional: true
+ terser:
+ optional: true
+ tsx:
+ optional: true
+ yaml:
+ optional: true
+
+snapshots:
+
+ '@emnapi/core@1.9.0':
+ dependencies:
+ '@emnapi/wasi-threads': 1.2.0
+ tslib: 2.8.1
+ optional: true
+
+ '@emnapi/runtime@1.9.0':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@emnapi/wasi-threads@1.2.0':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@napi-rs/wasm-runtime@1.1.1':
+ dependencies:
+ '@emnapi/core': 1.9.0
+ '@emnapi/runtime': 1.9.0
+ '@tybys/wasm-util': 0.10.1
+ optional: true
+
+ '@oxc-project/runtime@0.115.0': {}
+
+ '@oxc-project/types@0.115.0': {}
+
+ '@rolldown/binding-android-arm64@1.0.0-rc.9':
+ optional: true
+
+ '@rolldown/binding-darwin-arm64@1.0.0-rc.9':
+ optional: true
+
+ '@rolldown/binding-darwin-x64@1.0.0-rc.9':
+ optional: true
+
+ '@rolldown/binding-freebsd-x64@1.0.0-rc.9':
+ optional: true
+
+ '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.9':
+ optional: true
+
+ '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.9':
+ optional: true
+
+ '@rolldown/binding-linux-arm64-musl@1.0.0-rc.9':
+ optional: true
+
+ '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.9':
+ optional: true
+
+ '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.9':
+ optional: true
+
+ '@rolldown/binding-linux-x64-gnu@1.0.0-rc.9':
+ optional: true
+
+ '@rolldown/binding-linux-x64-musl@1.0.0-rc.9':
+ optional: true
+
+ '@rolldown/binding-openharmony-arm64@1.0.0-rc.9':
+ optional: true
+
+ '@rolldown/binding-wasm32-wasi@1.0.0-rc.9':
+ dependencies:
+ '@napi-rs/wasm-runtime': 1.1.1
+ optional: true
+
+ '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.9':
+ optional: true
+
+ '@rolldown/binding-win32-x64-msvc@1.0.0-rc.9':
+ optional: true
+
+ '@rolldown/pluginutils@1.0.0-rc.9': {}
+
+ '@tybys/wasm-util@0.10.1':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ detect-libc@2.1.2: {}
+
+ fdir@6.5.0(picomatch@4.0.3):
+ optionalDependencies:
+ picomatch: 4.0.3
+
+ fsevents@2.3.3:
+ optional: true
+
+ lightningcss-android-arm64@1.32.0:
+ optional: true
+
+ lightningcss-darwin-arm64@1.32.0:
+ optional: true
+
+ lightningcss-darwin-x64@1.32.0:
+ optional: true
+
+ lightningcss-freebsd-x64@1.32.0:
+ optional: true
+
+ lightningcss-linux-arm-gnueabihf@1.32.0:
+ optional: true
+
+ lightningcss-linux-arm64-gnu@1.32.0:
+ optional: true
+
+ lightningcss-linux-arm64-musl@1.32.0:
+ optional: true
+
+ lightningcss-linux-x64-gnu@1.32.0:
+ optional: true
+
+ lightningcss-linux-x64-musl@1.32.0:
+ optional: true
+
+ lightningcss-win32-arm64-msvc@1.32.0:
+ optional: true
+
+ lightningcss-win32-x64-msvc@1.32.0:
+ optional: true
+
+ lightningcss@1.32.0:
+ dependencies:
+ detect-libc: 2.1.2
+ optionalDependencies:
+ lightningcss-android-arm64: 1.32.0
+ lightningcss-darwin-arm64: 1.32.0
+ lightningcss-darwin-x64: 1.32.0
+ lightningcss-freebsd-x64: 1.32.0
+ lightningcss-linux-arm-gnueabihf: 1.32.0
+ lightningcss-linux-arm64-gnu: 1.32.0
+ lightningcss-linux-arm64-musl: 1.32.0
+ lightningcss-linux-x64-gnu: 1.32.0
+ lightningcss-linux-x64-musl: 1.32.0
+ lightningcss-win32-arm64-msvc: 1.32.0
+ lightningcss-win32-x64-msvc: 1.32.0
+
+ nanoid@3.3.11: {}
+
+ picocolors@1.1.1: {}
+
+ picomatch@4.0.3: {}
+
+ pnpm@10.32.1: {}
+
+ postcss@8.5.8:
+ dependencies:
+ nanoid: 3.3.11
+ picocolors: 1.1.1
+ source-map-js: 1.2.1
+
+ rolldown@1.0.0-rc.9:
+ dependencies:
+ '@oxc-project/types': 0.115.0
+ '@rolldown/pluginutils': 1.0.0-rc.9
+ optionalDependencies:
+ '@rolldown/binding-android-arm64': 1.0.0-rc.9
+ '@rolldown/binding-darwin-arm64': 1.0.0-rc.9
+ '@rolldown/binding-darwin-x64': 1.0.0-rc.9
+ '@rolldown/binding-freebsd-x64': 1.0.0-rc.9
+ '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.9
+ '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.9
+ '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.9
+ '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.9
+ '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.9
+ '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.9
+ '@rolldown/binding-linux-x64-musl': 1.0.0-rc.9
+ '@rolldown/binding-openharmony-arm64': 1.0.0-rc.9
+ '@rolldown/binding-wasm32-wasi': 1.0.0-rc.9
+ '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.9
+ '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.9
+
+ source-map-js@1.2.1: {}
+
+ tinyglobby@0.2.15:
+ dependencies:
+ fdir: 6.5.0(picomatch@4.0.3)
+ picomatch: 4.0.3
+
+ tslib@2.8.1:
+ optional: true
+
+ typescript@5.9.3: {}
+
+ vite@8.0.0:
+ dependencies:
+ '@oxc-project/runtime': 0.115.0
+ lightningcss: 1.32.0
+ picomatch: 4.0.3
+ postcss: 8.5.8
+ rolldown: 1.0.0-rc.9
+ tinyglobby: 0.2.15
+ optionalDependencies:
+ fsevents: 2.3.3
diff --git a/06-typescript/scripts/check-lessons.mjs b/06-typescript/scripts/check-lessons.mjs
new file mode 100644
index 0000000..c1e06a0
--- /dev/null
+++ b/06-typescript/scripts/check-lessons.mjs
@@ -0,0 +1,58 @@
+import { spawnSync } from "node:child_process";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+const rootDir = path.resolve(__dirname, "..");
+const tscPath = path.join(rootDir, "node_modules", "typescript", "bin", "tsc");
+
+const lessonFiles = [
+ "01-js-vs-ts/starter.ts",
+ "01-js-vs-ts/answer.ts",
+ "02-basic-type-annotations/starter.ts",
+ "02-basic-type-annotations/answer.ts",
+ "03-array-and-function-types/starter.ts",
+ "03-array-and-function-types/answer.ts",
+ "04-interface-object-shape/starter.ts",
+ "04-interface-object-shape/answer.ts",
+ "05-generic-functions/starter.ts",
+ "05-generic-functions/answer.ts",
+ "06-union-and-optional-props/starter.ts",
+ "06-union-and-optional-props/answer.ts",
+ "07-type-safe-renderer/starter.ts",
+ "07-type-safe-renderer/answer.ts",
+ "08-final-mini-app/starter.ts",
+ "08-final-mini-app/answer.ts",
+];
+
+const sharedArgs = [
+ "--noEmit",
+ "--pretty",
+ "false",
+ "--target",
+ "ES2020",
+ "--module",
+ "ESNext",
+ "--moduleResolution",
+ "Bundler",
+ "--strict",
+ "--skipLibCheck",
+];
+
+for (const lessonFile of lessonFiles) {
+ const filePath = path.join(rootDir, lessonFile);
+
+ console.log(`Checking ${lessonFile}`);
+
+ const result = spawnSync(process.execPath, [tscPath, ...sharedArgs, filePath], {
+ cwd: rootDir,
+ stdio: "inherit",
+ });
+
+ if (result.status !== 0) {
+ process.exit(result.status ?? 1);
+ }
+}
+
+console.log("All lesson files passed TypeScript checks.");
diff --git a/06-typescript/src/lessons.ts b/06-typescript/src/lessons.ts
new file mode 100644
index 0000000..450bfa9
--- /dev/null
+++ b/06-typescript/src/lessons.ts
@@ -0,0 +1,308 @@
+import lesson01Starter from "../01-js-vs-ts/starter.ts?raw";
+import lesson01Answer from "../01-js-vs-ts/answer.ts?raw";
+import lesson02Starter from "../02-basic-type-annotations/starter.ts?raw";
+import lesson02Answer from "../02-basic-type-annotations/answer.ts?raw";
+import lesson03Starter from "../03-array-and-function-types/starter.ts?raw";
+import lesson03Answer from "../03-array-and-function-types/answer.ts?raw";
+import lesson04Starter from "../04-interface-object-shape/starter.ts?raw";
+import lesson04Answer from "../04-interface-object-shape/answer.ts?raw";
+import lesson05Starter from "../05-generic-functions/starter.ts?raw";
+import lesson05Answer from "../05-generic-functions/answer.ts?raw";
+import lesson06Starter from "../06-union-and-optional-props/starter.ts?raw";
+import lesson06Answer from "../06-union-and-optional-props/answer.ts?raw";
+import lesson07Starter from "../07-type-safe-renderer/starter.ts?raw";
+import lesson07Answer from "../07-type-safe-renderer/answer.ts?raw";
+import lesson08Starter from "../08-final-mini-app/starter.ts?raw";
+import lesson08Answer from "../08-final-mini-app/answer.ts?raw";
+
+export interface Lesson {
+ id: string;
+ title: string;
+ focus: string;
+ summary: string;
+ starterCode: string;
+ answerCode: string;
+ keyPoints: string[];
+ runDemo: () => string[];
+}
+
+function runLesson01(): string[] {
+ function add(a: number, b: number): number {
+ return a + b;
+ }
+
+ const result = add(1, 2);
+
+ return [
+ `add(1, 2) => ${result}`,
+ '如果写成 add(1, "2"),TypeScript 会在编码阶段报错。',
+ ];
+}
+
+function runLesson02(): string[] {
+ const age: number = 25;
+ const userName: string = "李青";
+ const isLearning: boolean = true;
+
+ return [
+ `age 的类型是 number,值为 ${age}`,
+ `userName 的类型是 string,值为 ${userName}`,
+ `isLearning 的类型是 boolean,值为 ${isLearning}`,
+ ];
+}
+
+function runLesson03(): string[] {
+ const scoreList: number[] = [82, 90, 95];
+
+ function sum(a: number, b: number): number {
+ return a + b;
+ }
+
+ const total = sum(88, 12);
+
+ return [
+ `scoreList => [${scoreList.join(", ")}]`,
+ `sum(88, 12) => ${total}`,
+ ];
+}
+
+function runLesson04(): string[] {
+ interface User {
+ id: number;
+ name: string;
+ age: number;
+ }
+
+ const user: User = {
+ id: 1,
+ name: "小王",
+ age: 22,
+ };
+
+ return [
+ `User.name => ${user.name}`,
+ `User.age => ${user.age}`,
+ "interface 让对象结构在写代码时就固定下来。",
+ ];
+}
+
+function runLesson05(): string[] {
+ function getData(data: T): T {
+ return data;
+ }
+
+ const text = getData("hello");
+ const count = getData(100);
+
+ return [
+ `getData("hello") => ${text}`,
+ `getData(100) => ${count}`,
+ "泛型可以在复用函数时保留输入和输出的类型关系。",
+ ];
+}
+
+function runLesson06(): string[] {
+ type UserId = number | string;
+
+ interface Profile {
+ id: UserId;
+ name: string;
+ age?: number;
+ }
+
+ const profileA: Profile = {
+ id: 1,
+ name: "周周",
+ };
+
+ const profileB: Profile = {
+ id: "u-2",
+ name: "小林",
+ age: 24,
+ };
+
+ return [
+ `profileA.id => ${profileA.id}`,
+ `profileB.id => ${profileB.id}`,
+ `profileB.age => ${profileB.age}`,
+ ];
+}
+
+function runLesson07(): string[] {
+ interface Course {
+ title: string;
+ lessons: number;
+ finished: boolean;
+ }
+
+ const courses: Course[] = [
+ { title: "TypeScript 基础", lessons: 8, finished: true },
+ { title: "接口和泛型", lessons: 6, finished: false },
+ ];
+
+ function renderCourseLines(list: Course[]): string[] {
+ return list.map((course) => {
+ const status = course.finished ? "已完成" : "学习中";
+ return `${course.title} - ${course.lessons} 节 - ${status}`;
+ });
+ }
+
+ return renderCourseLines(courses);
+}
+
+function runLesson08(): string[] {
+ type StudentId = number | string;
+
+ interface Course {
+ title: string;
+ finished: boolean;
+ }
+
+ interface Student {
+ id: StudentId;
+ name: string;
+ age?: number;
+ courses: Course[];
+ }
+
+ function pickFirst(list: T[]): T {
+ return list[0];
+ }
+
+ function formatStudent(student: Student): string {
+ const courseCount = student.courses.length;
+ return `${student.name} 当前有 ${courseCount} 门课程,编号是 ${student.id}`;
+ }
+
+ const student: Student = {
+ id: "stu-1",
+ name: "林晨",
+ courses: [
+ { title: "基本类型", finished: true },
+ { title: "接口", finished: false },
+ ],
+ };
+
+ const firstCourse = pickFirst(student.courses);
+
+ return [
+ formatStudent(student),
+ `第一门课程:${firstCourse.title}`,
+ `完成状态:${firstCourse.finished ? "已完成" : "学习中"}`,
+ ];
+}
+
+export const lessons: Lesson[] = [
+ {
+ id: "01",
+ title: "JavaScript vs TypeScript",
+ focus: "理解为什么要引入类型系统",
+ summary: "从一个最简单的加法函数开始,看 TypeScript 如何把错误提前到编码阶段。",
+ starterCode: lesson01Starter,
+ answerCode: lesson01Answer,
+ keyPoints: [
+ "TypeScript 在写代码阶段检查参数类型",
+ "运行前发现问题,比运行后排查问题更省成本",
+ "最常见的第一步就是给函数参数和返回值加类型",
+ ],
+ runDemo: runLesson01,
+ },
+ {
+ id: "02",
+ title: "基本类型标注",
+ focus: "number / string / boolean",
+ summary: "把最常见的值类型写清楚,建立最基础的类型意识。",
+ starterCode: lesson02Starter,
+ answerCode: lesson02Answer,
+ keyPoints: [
+ "基础值先学会标注类型",
+ "看到报错时先分清期望类型和实际类型",
+ "能写对基本类型后再往对象和函数走",
+ ],
+ runDemo: runLesson02,
+ },
+ {
+ id: "03",
+ title: "数组和函数类型",
+ focus: "number[] 与函数参数/返回值",
+ summary: "把类型从单个值扩展到数组和函数,是进入业务代码前必须过的一关。",
+ starterCode: lesson03Starter,
+ answerCode: lesson03Answer,
+ keyPoints: [
+ "数组里的元素类型要保持一致",
+ "函数参数和返回值都应该尽量明确",
+ "函数签名是 TypeScript 最高频的使用场景之一",
+ ],
+ runDemo: runLesson03,
+ },
+ {
+ id: "04",
+ title: "interface 对象结构",
+ focus: "用接口约束对象字段",
+ summary: "当数据结构开始变复杂时,先定义 shape,再写数据和逻辑。",
+ starterCode: lesson04Starter,
+ answerCode: lesson04Answer,
+ keyPoints: [
+ "对象字段越多,越应该先定义 interface",
+ "接口让团队协作时更容易知道一个对象该长什么样",
+ "字段缺失、字段类型不匹配都会更早暴露",
+ ],
+ runDemo: runLesson04,
+ },
+ {
+ id: "05",
+ title: "泛型函数",
+ focus: "保持输入输出的类型关系",
+ summary: "泛型的重点不是复杂,而是让复用函数在不同类型下依然安全。",
+ starterCode: lesson05Starter,
+ answerCode: lesson05Answer,
+ keyPoints: [
+ "泛型适合处理同类输入输出关系",
+ "输入是什么类型,输出就跟着保持什么类型",
+ "先理解例子,再去记住 这个写法",
+ ],
+ runDemo: runLesson05,
+ },
+ {
+ id: "06",
+ title: "联合类型和可选属性",
+ focus: "number | string 与 age?",
+ summary: "业务数据并不总是完全整齐,联合类型和可选属性就是为这种情况准备的。",
+ starterCode: lesson06Starter,
+ answerCode: lesson06Answer,
+ keyPoints: [
+ "联合类型描述多个合法输入分支",
+ "可选属性适合那些可能缺失的字段",
+ "真实业务里常常需要同时使用这两类能力",
+ ],
+ runDemo: runLesson06,
+ },
+ {
+ id: "07",
+ title: "类型安全渲染",
+ focus: "把接口用到列表渲染里",
+ summary: "进入接近真实业务的场景,用类型约束一组列表数据和渲染函数。",
+ starterCode: lesson07Starter,
+ answerCode: lesson07Answer,
+ keyPoints: [
+ "先定义 Course,再定义 Course[]",
+ "渲染函数应该接收明确的数据结构",
+ "数据结构一旦变化,相关代码会一起被提醒",
+ ],
+ runDemo: runLesson07,
+ },
+ {
+ id: "08",
+ title: "综合小练习",
+ focus: "把常见类型能力组合起来",
+ summary: "把联合类型、接口、可选属性和泛型组合起来,做一个完整的小示例。",
+ starterCode: lesson08Starter,
+ answerCode: lesson08Answer,
+ keyPoints: [
+ "综合练习的重点是把前面知识点串起来",
+ "能读懂数据模型,通常就能更稳地写业务代码",
+ "类型不是目的,减少错误和提升可维护性才是目的",
+ ],
+ runDemo: runLesson08,
+ },
+];
diff --git a/06-typescript/src/main.ts b/06-typescript/src/main.ts
new file mode 100644
index 0000000..19e5168
--- /dev/null
+++ b/06-typescript/src/main.ts
@@ -0,0 +1,151 @@
+import "./style.css";
+import { lessons } from "./lessons";
+
+function getRequiredElement(
+ selector: string,
+ parent: ParentNode = document,
+): T {
+ const element = parent.querySelector(selector);
+
+ if (!element) {
+ throw new Error(`Required element was not found: ${selector}`);
+ }
+
+ return element;
+}
+
+const app = getRequiredElement("#app");
+
+app.innerHTML = `
+
+
+
+
+
+
+
+
知识点
+
+
+
+
+
+
运行结果
+
+
+
+
+
+
+
+
+
Starter
+
+
+
+
+
+
Answer
+
+
+
+
+
+
+`;
+
+const nav = getRequiredElement(".lesson-nav");
+const title = getRequiredElement("#lesson-title");
+const focus = getRequiredElement("#lesson-focus");
+const summary = getRequiredElement("#lesson-summary");
+const pointList = getRequiredElement("#lesson-points");
+const output = getRequiredElement("#lesson-output");
+const starterCode = getRequiredElement("#starter-code");
+const answerCode = getRequiredElement("#answer-code");
+const runButton = getRequiredElement("#run-demo");
+
+let activeLessonId = lessons[0]?.id ?? "";
+
+function renderLessonList(): void {
+ nav.innerHTML = lessons
+ .map((lesson) => {
+ const isActive = lesson.id === activeLessonId;
+ return `
+
+ `;
+ })
+ .join("");
+}
+
+function renderOutput(lines: string[]): void {
+ output.innerHTML = lines
+ .map((line) => `${line}
`)
+ .join("");
+}
+
+function renderActiveLesson(): void {
+ const lesson = lessons.find((item) => item.id === activeLessonId);
+
+ if (!lesson) {
+ return;
+ }
+
+ title.textContent = `${lesson.id}. ${lesson.title}`;
+ focus.textContent = lesson.focus;
+ summary.textContent = lesson.summary;
+ pointList.innerHTML = lesson.keyPoints
+ .map((item) => `${item}`)
+ .join("");
+ starterCode.textContent = lesson.starterCode;
+ answerCode.textContent = lesson.answerCode;
+ renderOutput(lesson.runDemo());
+ renderLessonList();
+}
+
+nav.addEventListener("click", (event) => {
+ const target = event.target as HTMLElement;
+ const button = target.closest("[data-id]");
+
+ if (!button) {
+ return;
+ }
+
+ activeLessonId = button.dataset.id ?? activeLessonId;
+ renderActiveLesson();
+});
+
+runButton.addEventListener("click", () => {
+ const lesson = lessons.find((item) => item.id === activeLessonId);
+
+ if (!lesson) {
+ return;
+ }
+
+ renderOutput(lesson.runDemo());
+});
+
+renderActiveLesson();
diff --git a/06-typescript/src/style.css b/06-typescript/src/style.css
new file mode 100644
index 0000000..3d9b9e0
--- /dev/null
+++ b/06-typescript/src/style.css
@@ -0,0 +1,237 @@
+:root {
+ font-family: "IBM Plex Sans", "PingFang SC", "Hiragino Sans GB", sans-serif;
+ line-height: 1.5;
+ font-weight: 400;
+ color: #f7f4ec;
+ background:
+ radial-gradient(circle at top left, rgba(255, 191, 73, 0.22), transparent 24rem),
+ radial-gradient(circle at bottom right, rgba(86, 154, 255, 0.16), transparent 26rem),
+ #11131a;
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0;
+ min-width: 320px;
+ min-height: 100vh;
+}
+
+button,
+code,
+pre {
+ font: inherit;
+}
+
+#app {
+ min-height: 100vh;
+}
+
+.shell {
+ display: grid;
+ grid-template-columns: 320px minmax(0, 1fr);
+ min-height: 100vh;
+}
+
+.sidebar {
+ padding: 28px 20px;
+ border-right: 1px solid rgba(255, 255, 255, 0.08);
+ background: rgba(9, 10, 14, 0.62);
+ backdrop-filter: blur(18px);
+}
+
+.brand {
+ margin-bottom: 24px;
+}
+
+.eyebrow,
+.badge {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ margin: 0 0 12px;
+ padding: 6px 10px;
+ border-radius: 999px;
+ background: rgba(255, 191, 73, 0.14);
+ color: #ffcc6a;
+ font-size: 12px;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+}
+
+.brand h1 {
+ margin: 0 0 10px;
+ font-size: 28px;
+ line-height: 1.1;
+}
+
+.intro,
+.summary {
+ margin: 0;
+ color: rgba(247, 244, 236, 0.72);
+}
+
+.lesson-nav {
+ display: grid;
+ gap: 10px;
+}
+
+.lesson-link {
+ display: flex;
+ align-items: flex-start;
+ gap: 12px;
+ width: 100%;
+ padding: 14px 12px;
+ border: 1px solid rgba(255, 255, 255, 0.08);
+ border-radius: 18px;
+ background: rgba(255, 255, 255, 0.03);
+ color: inherit;
+ text-align: left;
+ cursor: pointer;
+ transition:
+ transform 180ms ease,
+ border-color 180ms ease,
+ background 180ms ease;
+}
+
+.lesson-link:hover,
+.lesson-link.is-active {
+ transform: translateY(-1px);
+ border-color: rgba(255, 191, 73, 0.42);
+ background: rgba(255, 191, 73, 0.1);
+}
+
+.lesson-index {
+ min-width: 40px;
+ padding: 8px 0;
+ border-radius: 12px;
+ background: rgba(255, 255, 255, 0.06);
+ text-align: center;
+ font-weight: 700;
+}
+
+.lesson-meta {
+ display: grid;
+ gap: 4px;
+}
+
+.lesson-meta small {
+ color: rgba(247, 244, 236, 0.64);
+}
+
+.content {
+ padding: 28px;
+}
+
+.hero {
+ margin-bottom: 20px;
+}
+
+.focus {
+ margin: 0 0 10px;
+ color: #ffcc6a;
+ font-weight: 600;
+}
+
+.grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 20px;
+ margin-bottom: 20px;
+}
+
+.code-grid {
+ align-items: start;
+}
+
+.card {
+ padding: 20px;
+ border: 1px solid rgba(255, 255, 255, 0.08);
+ border-radius: 28px;
+ background: rgba(15, 17, 24, 0.72);
+ box-shadow: 0 18px 48px rgba(0, 0, 0, 0.22);
+}
+
+.card-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ margin-bottom: 14px;
+}
+
+.card-head h3 {
+ margin: 0;
+ font-size: 18px;
+}
+
+.point-list {
+ margin: 0;
+ padding-left: 18px;
+ color: rgba(247, 244, 236, 0.84);
+}
+
+.point-list li + li {
+ margin-top: 10px;
+}
+
+.output-panel {
+ display: grid;
+ gap: 10px;
+}
+
+.output-line {
+ padding: 12px 14px;
+ border-radius: 16px;
+ background: rgba(86, 154, 255, 0.12);
+ color: #d8e9ff;
+}
+
+.run-button {
+ padding: 10px 14px;
+ border: 0;
+ border-radius: 999px;
+ background: linear-gradient(135deg, #ffbf49, #ff8f48);
+ color: #1d1302;
+ font-weight: 700;
+ cursor: pointer;
+}
+
+.code-card pre {
+ overflow: auto;
+ margin: 0;
+ padding: 18px;
+ border-radius: 20px;
+ background: #0b0d12;
+}
+
+.code-card code {
+ font-family: "JetBrains Mono", "SFMono-Regular", monospace;
+ font-size: 13px;
+ white-space: pre;
+}
+
+@media (max-width: 960px) {
+ .shell {
+ grid-template-columns: 1fr;
+ }
+
+ .sidebar {
+ border-right: 0;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.08);
+ }
+
+ .grid {
+ grid-template-columns: 1fr;
+ }
+
+ .content {
+ padding: 20px;
+ }
+}
diff --git a/06-typescript/src/vite-env.d.ts b/06-typescript/src/vite-env.d.ts
new file mode 100644
index 0000000..11f02fe
--- /dev/null
+++ b/06-typescript/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/06-typescript/tsconfig.json b/06-typescript/tsconfig.json
new file mode 100644
index 0000000..d894758
--- /dev/null
+++ b/06-typescript/tsconfig.json
@@ -0,0 +1,16 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "module": "ESNext",
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "moduleResolution": "Bundler",
+ "types": ["vite/client"],
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "strict": true,
+ "noEmit": true,
+ "skipLibCheck": true
+ },
+ "include": ["./src/**/*.ts", "./src/**/*.d.ts"]
+}
diff --git a/README.md b/README.md
index f9de029..9ba6c92 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,8 @@
- `02-css-layout`:预留给 CSS
- `03-javascript-core`:预留给 JavaScript
- `04-dom-events-async`:预留给 DOM + 事件 + 异步
-- `05-typescript`:预留给 TypeScript
+- `05-es6-plus`:预留给 ES6+(现代 JS)
+- `06-typescript`:预留给 TypeScript
## 当前可学内容
@@ -33,15 +34,41 @@
- `starter.js` 起始代码
- `answer.js` 参考答案
-前三部分现在都已经补充到“核心主线 + 常见细分知识点”。
+现在也已经整理好 `04-dom-events-async`,里面包含:
+
+- DOM + 事件 + 异步讲义
+- 分阶段练习
+- `starter.html` / `starter.js` 起始代码
+- `answer.html` / `answer.js` 参考答案
+
+现在也已经整理好 `05-es6-plus`,里面包含:
+
+- ES6+(现代 JS)讲义
+- 分阶段练习
+- `starter.html` / `starter.js` 起始代码
+- `answer.html` / `answer.js` 参考答案
+
+现在也已经整理好 `06-typescript`,里面包含:
+
+- TypeScript(类型系统)讲义
+- 分阶段练习
+- `starter.ts` 起始代码
+- `answer.ts` 参考答案
+- `Vite + TypeScript` 学习面板
+
+前六部分现在都已经补充到“核心主线 + 常见细分知识点”。
## 使用方式
-1. 先阅读 [01-html-structure/README.md](/Volumes/Macintosh HD 1/home/front-end-example/01-html-structure/README.md)
-2. 再阅读 [02-css-layout/README.md](/Volumes/Macintosh HD 1/home/front-end-example/02-css-layout/README.md)
-3. 再阅读 [03-javascript-core/README.md](/Volumes/Macintosh HD 1/home/front-end-example/03-javascript-core/README.md)
-4. 按顺序完成每个练习目录
-5. 先写 `starter.html`、`starter.css` 或 `starter.js`
-6. 写完后再对照答案文件
+1. 先阅读 [01-html-structure/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/01-html-structure/README.md)
+2. 再阅读 [02-css-layout/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/02-css-layout/README.md)
+3. 再阅读 [03-javascript-core/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/03-javascript-core/README.md)
+4. 再阅读 [04-dom-events-async/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/README.md)
+5. 再阅读 [05-es6-plus/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/README.md)
+6. 再阅读 [06-typescript/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/README.md)
+7. 按顺序完成每个练习目录
+8. 先写 `starter.html`、`starter.css`、`starter.js` 或 `starter.ts`
+9. 写完后再对照答案文件
+10. 学 `06-typescript` 时,也可以进入 [06-typescript](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript) 后执行 `npm install` 和 `npm run dev`
-如果你后面要继续学其他知识点,我可以按同样结构继续给你补 `04-dom-events-async`、`05-typescript` 等目录。
+如果你后面要继续学其他知识点,我可以按同样结构继续给你补更多工程化目录。