Compare commits
8 Commits
3435848495
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a65ba8c6a | ||
|
|
7c0cbe1320 | ||
|
|
acb1445575 | ||
|
|
3850ce7399 | ||
|
|
d0d8be443b | ||
|
|
d5ff59ac76 | ||
|
|
8b83f63235 | ||
|
|
1071f4db05 |
@@ -5,6 +5,20 @@ const students = [
|
|||||||
{ name: "小陈", finished: false },
|
{ name: "小陈", finished: false },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const scores1 = scores.map(item => item = item + '分')
|
||||||
|
const scores2 = scores.filter(item => item > 60)
|
||||||
|
const scores3 = scores.reduce((acc, item) => { return item + acc }, 0)
|
||||||
|
const scores4 = scores.find(item => item >= 90)
|
||||||
|
const scores5 = scores.some(item => item >= 90)
|
||||||
|
const scores6 = students.every(item => item.finished === true)
|
||||||
|
|
||||||
|
console.log("分数:" + scores1);
|
||||||
|
console.log("及格分:" + scores2);
|
||||||
|
console.log("总分:" + scores3);
|
||||||
|
console.log("第一个 >= 90 的分数:" + scores4);
|
||||||
|
console.log("是否存在不及格:" + scores5);
|
||||||
|
console.log("是否都及格:" + scores6);
|
||||||
|
|
||||||
// 任务:
|
// 任务:
|
||||||
// 1. 用 map 生成 ["58分", ...]
|
// 1. 用 map 生成 ["58分", ...]
|
||||||
// 2. 用 filter 筛出及格分
|
// 2. 用 filter 筛出及格分
|
||||||
@@ -12,3 +26,9 @@ const students = [
|
|||||||
// 4. 用 find 找到第一个 >= 90 的分数
|
// 4. 用 find 找到第一个 >= 90 的分数
|
||||||
// 5. 用 some 判断是否存在不及格
|
// 5. 用 some 判断是否存在不及格
|
||||||
// 6. 用 every 判断 students 是否都 finished 为 true
|
// 6. 用 every 判断 students 是否都 finished 为 true
|
||||||
|
/* 用 `map` 生成带单位的新数组
|
||||||
|
- 用 `filter` 找出及格成绩
|
||||||
|
- 用 `reduce` 计算总分
|
||||||
|
- 用 `find` 找出第一条大于等于 90 的成绩
|
||||||
|
- 用 `some` 判断是否有人不及格
|
||||||
|
- 用 `every` 判断是否全部完成考试*/
|
||||||
@@ -19,7 +19,16 @@ function printStepTwo() {
|
|||||||
function updateUser(user) {
|
function updateUser(user) {
|
||||||
// 任务:
|
// 任务:
|
||||||
// 1. 修改 user.city
|
// 1. 修改 user.city
|
||||||
|
user.city = '北京'
|
||||||
}
|
}
|
||||||
|
scoreB = 90
|
||||||
|
console.log('scoreA' + scoreA + ',scoreB' + scoreB);
|
||||||
|
userB.city = '秦皇岛'
|
||||||
|
console.log(userA.city, userB.city);
|
||||||
|
printStepOne()
|
||||||
|
printStepTwo()
|
||||||
|
updateUser(userA)
|
||||||
|
console.log(userA);
|
||||||
|
|
||||||
// 任务:
|
// 任务:
|
||||||
// 2. 修改 scoreB,观察 scoreA 是否变化
|
// 2. 修改 scoreB,观察 scoreA 是否变化
|
||||||
|
|||||||
@@ -6,9 +6,43 @@ const finalComment = null;
|
|||||||
let statusText = "";
|
let statusText = "";
|
||||||
const finishedRecords = [];
|
const finishedRecords = [];
|
||||||
|
|
||||||
|
switch (learningStatus) {
|
||||||
|
case 'review':
|
||||||
|
console.log('已学习')
|
||||||
|
break
|
||||||
|
case 'ing':
|
||||||
|
console.log('学习中');
|
||||||
|
break
|
||||||
|
case 'prepare':
|
||||||
|
console.log('未学习');
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
console.log('状态未知');
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(optionalNote, finalComment);
|
||||||
|
|
||||||
|
for (let i = 0; i < records.length; i++) {
|
||||||
|
if (records[i] === null || records[i] === undefined) {
|
||||||
|
console.log(`第${i + 1}项为空值,停止读取`)
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
finishedRecords.push(records[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(finishedRecords)
|
||||||
|
|
||||||
|
|
||||||
// 任务:
|
// 任务:
|
||||||
// 1. 用 switch 给 learningStatus 生成说明文字
|
// 1. 用 switch 给 learningStatus 生成说明文字
|
||||||
// 2. 输出 optionalNote 和 finalComment 分别是什么
|
// 2. 输出 optionalNote 和 finalComment 分别是什么
|
||||||
// 3. 用 for 循环读取 records
|
// 3. 用 for 循环读取 records
|
||||||
// 4. 如果遇到 undefined 或 null,就 break
|
// 4. 如果遇到 undefined 或 null,就 break
|
||||||
// 5. 输出 finishedRecords
|
// 5. 输出 finishedRecords
|
||||||
|
/*用 `switch` 根据学习状态输出不同说明
|
||||||
|
- 观察 `undefined` 和 `null` 的区别
|
||||||
|
- 用循环读取学习记录
|
||||||
|
- 如果读到 `undefined` 或 `null`,立即用 `break` 停止循环
|
||||||
|
- 输出停止前已经读取到的内容*/
|
||||||
@@ -2,6 +2,7 @@ const reviewer = {
|
|||||||
name: "林晨",
|
name: "林晨",
|
||||||
stage: "final-review",
|
stage: "final-review",
|
||||||
showTitle() {
|
showTitle() {
|
||||||
|
console.log(`学习营总复盘器`);
|
||||||
// 输出总复盘标题
|
// 输出总复盘标题
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -14,12 +15,47 @@ const lessons = [
|
|||||||
{ title: "综合回顾", score: 95, finished: true },
|
{ title: "综合回顾", score: 95, finished: true },
|
||||||
];
|
];
|
||||||
|
|
||||||
let mentorNote;
|
let mentorNote;//undefined
|
||||||
const reviewComment = null;
|
const reviewComment = null;//null
|
||||||
|
|
||||||
function getStageText(stage) {
|
function getStageText(stage) {
|
||||||
// 用 switch 返回阶段说明
|
// 用 switch 返回阶段说明
|
||||||
|
switch (stage) {
|
||||||
|
case 'final-review':
|
||||||
|
console.log('已学习');
|
||||||
|
break
|
||||||
|
case 'ing-review':
|
||||||
|
console.log('学习中');
|
||||||
|
break
|
||||||
|
case 'start-review':
|
||||||
|
console.log('未学习');
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
console.log('状态未知');
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
reviewer.showTitle()
|
||||||
|
|
||||||
|
console.log(mentorNote, reviewComment);
|
||||||
|
|
||||||
|
const lessons1 = []
|
||||||
|
for (let i = 0; i < lessons.length; i++) {
|
||||||
|
if (lessons[i].score != null) {
|
||||||
|
lessons1.push(lessons[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sum = lessons.reduce((acc, item) => {
|
||||||
|
return acc + item.score
|
||||||
|
}, 0)
|
||||||
|
console.log("总分:" + sum);
|
||||||
|
console.log(`平均分:${sum / lessons.length}`);
|
||||||
|
|
||||||
|
|
||||||
|
const reviewerAlias = reviewer
|
||||||
|
reviewerAlias.stage = 'ing-review'
|
||||||
|
console.log(reviewer.stage, reviewerAlias.stage);
|
||||||
|
|
||||||
// 任务:
|
// 任务:
|
||||||
// 1. 调用 reviewer.showTitle()
|
// 1. 调用 reviewer.showTitle()
|
||||||
@@ -28,3 +64,11 @@ function getStageText(stage) {
|
|||||||
// 4. 用高阶函数统计课程标题、完成状态和平均分
|
// 4. 用高阶函数统计课程标题、完成状态和平均分
|
||||||
// 5. 创建 reviewerAlias 指向 reviewer,修改 stage,观察原对象是否变化
|
// 5. 创建 reviewerAlias 指向 reviewer,修改 stage,观察原对象是否变化
|
||||||
// 6. 输出最终结果
|
// 6. 输出最终结果
|
||||||
|
/* 一个带方法的 `reviewer` 对象
|
||||||
|
- 一个 `lessons` 数组
|
||||||
|
- 一个用 `switch` 输出阶段说明的函数
|
||||||
|
- 一个用 `for + break` 清洗有效数据的过程
|
||||||
|
- 至少两个数组高阶函数
|
||||||
|
- 对 `undefined` 和 `null` 的判断
|
||||||
|
- 一段对象引用变化的观察代码
|
||||||
|
- 最终输出标题、有效课程、平均分、完成状态*/
|
||||||
|
|||||||
@@ -1,52 +1,59 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>获取元素</title>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 32px;
|
|
||||||
font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
|
|
||||||
background: #f6f8fb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel {
|
<head>
|
||||||
max-width: 760px;
|
<meta charset="UTF-8" />
|
||||||
margin: 0 auto;
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
padding: 24px;
|
<title>获取元素</title>
|
||||||
border-radius: 20px;
|
<style>
|
||||||
background: #ffffff;
|
body {
|
||||||
border: 1px solid #dbe3f0;
|
margin: 0;
|
||||||
}
|
padding: 32px;
|
||||||
|
font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||||
|
background: #f6f8fb;
|
||||||
|
}
|
||||||
|
|
||||||
.cards {
|
.panel {
|
||||||
display: grid;
|
max-width: 760px;
|
||||||
gap: 12px;
|
margin: 0 auto;
|
||||||
margin-top: 18px;
|
padding: 24px;
|
||||||
}
|
border-radius: 20px;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #dbe3f0;
|
||||||
|
}
|
||||||
|
|
||||||
.card {
|
.cards {
|
||||||
padding: 16px;
|
display: grid;
|
||||||
border-radius: 14px;
|
gap: 12px;
|
||||||
background: #f8fbff;
|
margin-top: 18px;
|
||||||
border: 1px solid #dce8f8;
|
}
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<section class="panel">
|
|
||||||
<h1 id="page-title">DOM 获取元素练习</h1>
|
|
||||||
<button class="start-btn" type="button">开始学习</button>
|
|
||||||
|
|
||||||
<div class="cards">
|
.card {
|
||||||
<article class="card">获取标题</article>
|
padding: 16px;
|
||||||
<article class="card">获取按钮</article>
|
border-radius: 14px;
|
||||||
<article class="card">获取一组卡片</article>
|
background: #f8fbff;
|
||||||
</div>
|
border: 1px solid #dce8f8;
|
||||||
</section>
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
<script src="./starter.js"></script>
|
<body>
|
||||||
</body>
|
<section class="panel">
|
||||||
</html>
|
<h1 id="page-title">
|
||||||
|
<p>111</p> 获取元素练习
|
||||||
|
</h1>
|
||||||
|
<button class="start-btn" type="button">开始学习</button>
|
||||||
|
|
||||||
|
<div class="cards">
|
||||||
|
<article class="card">获取标题</article>
|
||||||
|
<article class="card">获取按钮</article>
|
||||||
|
<article class="card">获取一组卡片</article>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<script src="./starter.js"></script>
|
||||||
|
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -3,3 +3,11 @@
|
|||||||
// 2. 用 querySelector 获取按钮
|
// 2. 用 querySelector 获取按钮
|
||||||
// 3. 用 querySelectorAll 获取全部卡片
|
// 3. 用 querySelectorAll 获取全部卡片
|
||||||
// 4. 在控制台输出标题文字、按钮文字和卡片数量
|
// 4. 在控制台输出标题文字、按钮文字和卡片数量
|
||||||
|
const a = document.getElementById("page-title")
|
||||||
|
console.log(a.textContent);
|
||||||
|
const b = document.querySelector(".start-btn")
|
||||||
|
console.log(b.textContent);
|
||||||
|
const c = document.querySelectorAll(".card")
|
||||||
|
console.log(c.length);
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,3 +3,9 @@
|
|||||||
// 2. 修改标题文字
|
// 2. 修改标题文字
|
||||||
// 3. 给标签加上 done 类名
|
// 3. 给标签加上 done 类名
|
||||||
// 4. 修改描述文字颜色
|
// 4. 修改描述文字颜色
|
||||||
|
const a = document.getElementById("title")
|
||||||
|
const b = document.getElementById("description")
|
||||||
|
const c = document.getElementById("badge")
|
||||||
|
a.textContent = "学习状态非常好"
|
||||||
|
a.classList.add("badge.done")
|
||||||
|
b.style.color = "red"
|
||||||
@@ -1,4 +1,22 @@
|
|||||||
// 任务:
|
// 任务:
|
||||||
// 1. 获取新增按钮、删除按钮和列表
|
// 1. 获取新增按钮、删除按钮和列表
|
||||||
// 2. 点击新增按钮时创建一个新的 li 并追加到列表
|
// 2. 点击新增按钮时创建一个新的 li 并追加到列表
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 3. 点击删除按钮时删除最后一个 li
|
// 3. 点击删除按钮时删除最后一个 li
|
||||||
|
const add = document.getElementById("add-btn")
|
||||||
|
const remove = document.getElementById("remove-btn")
|
||||||
|
const list = document.getElementById("task-list")
|
||||||
|
add.onclick = function () {
|
||||||
|
const a = document.createElement("li")
|
||||||
|
a.textContent = "巧克力"
|
||||||
|
list.appendChild(a)
|
||||||
|
}
|
||||||
|
remove.onclick = function () {
|
||||||
|
const c = list.lastElementChild
|
||||||
|
if (c) {
|
||||||
|
c.remove()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,3 +4,26 @@ let count = 0;
|
|||||||
// 1. 获取数字元素和 3 个按钮
|
// 1. 获取数字元素和 3 个按钮
|
||||||
// 2. 点击按钮时更新 count
|
// 2. 点击按钮时更新 count
|
||||||
// 3. 每次修改后,把最新 count 渲染到页面
|
// 3. 每次修改后,把最新 count 渲染到页面
|
||||||
|
const num = document.getElementById("value")
|
||||||
|
const del = document.getElementById("decrease-btn")
|
||||||
|
const add = document.getElementById("increase-btn")
|
||||||
|
const reset = document.getElementById("reset-btn")
|
||||||
|
|
||||||
|
function ren() {
|
||||||
|
num.textContent = count
|
||||||
|
}
|
||||||
|
|
||||||
|
add.addEventListener("click", function () {
|
||||||
|
count++
|
||||||
|
ren()
|
||||||
|
})
|
||||||
|
|
||||||
|
del.addEventListener("click", function () {
|
||||||
|
count--
|
||||||
|
ren()
|
||||||
|
})
|
||||||
|
|
||||||
|
reset.addEventListener("click", function () {
|
||||||
|
count = 0
|
||||||
|
ren()
|
||||||
|
})
|
||||||
@@ -4,3 +4,17 @@
|
|||||||
// 3. 用 preventDefault() 阻止默认提交
|
// 3. 用 preventDefault() 阻止默认提交
|
||||||
// 4. 读取输入框内容,创建新 li,追加到列表
|
// 4. 读取输入框内容,创建新 li,追加到列表
|
||||||
// 5. 清空输入框
|
// 5. 清空输入框
|
||||||
|
const form = document.getElementById("todo-form")
|
||||||
|
const input = document.getElementById("todo-input")
|
||||||
|
const list = document.getElementById("todo-list")
|
||||||
|
|
||||||
|
form.addEventListener("submit", function (e) {
|
||||||
|
e.preventDefault()
|
||||||
|
const text = input.value.trim()
|
||||||
|
if (text) {
|
||||||
|
const li = document.createElement("li")
|
||||||
|
li.textContent = text
|
||||||
|
list.appendChild(li)
|
||||||
|
input.value = ""
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -3,3 +3,28 @@
|
|||||||
// 2. 给 lesson-list 绑定点击事件,使用事件委托
|
// 2. 给 lesson-list 绑定点击事件,使用事件委托
|
||||||
// 3. 点击 li 时切换 active 类名
|
// 3. 点击 li 时切换 active 类名
|
||||||
// 4. 点击删除按钮时,阻止冒泡并删除当前 li
|
// 4. 点击删除按钮时,阻止冒泡并删除当前 li
|
||||||
|
const panel = document.getElementById("panel")
|
||||||
|
panel.addEventListener("click", function () {
|
||||||
|
console.log("点击外部")
|
||||||
|
})
|
||||||
|
|
||||||
|
const list = document.getElementById("lesson-list")
|
||||||
|
list.addEventListener("click", function (event) {
|
||||||
|
|
||||||
|
const remove = event.target.closest(".remove-btn")
|
||||||
|
if (remove) {
|
||||||
|
const li = remove.closest(".lesson-item")
|
||||||
|
|
||||||
|
if (li) {
|
||||||
|
li.remove()
|
||||||
|
event.stopPropagation()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const tog = event.target.closest(".lesson-item")
|
||||||
|
if (tog) {
|
||||||
|
tog.classList.add("active")
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
@@ -3,4 +3,24 @@
|
|||||||
// 2. 点击按钮后清空旧日志
|
// 2. 点击按钮后清空旧日志
|
||||||
// 3. 先追加“开始执行”
|
// 3. 先追加“开始执行”
|
||||||
// 4. 用 setTimeout 延迟追加“异步回调完成”
|
// 4. 用 setTimeout 延迟追加“异步回调完成”
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 5. 再立刻追加“同步代码结束”
|
// 5. 再立刻追加“同步代码结束”
|
||||||
|
const btn = document.getElementById("run-btn")
|
||||||
|
const list = document.getElementById("log-list")
|
||||||
|
|
||||||
|
function add1(message) {
|
||||||
|
const li = document.createElement("li")
|
||||||
|
li.textContent = message
|
||||||
|
list.appendChild(li)
|
||||||
|
}
|
||||||
|
|
||||||
|
btn.addEventListener("click", function () {
|
||||||
|
list.textContent = ""
|
||||||
|
add1("开始执行")
|
||||||
|
setTimeout(function () {
|
||||||
|
add1("异步回调完成")
|
||||||
|
}, 2000)
|
||||||
|
add1("同步代码结束")
|
||||||
|
})
|
||||||
@@ -12,3 +12,23 @@ function fakeFetchCourses() {
|
|||||||
// 3. 调用 fakeFetchCourses()
|
// 3. 调用 fakeFetchCourses()
|
||||||
// 4. 用 then 渲染课程列表
|
// 4. 用 then 渲染课程列表
|
||||||
// 5. 用 catch 处理错误
|
// 5. 用 catch 处理错误
|
||||||
|
const btn = document.getElementById("load-btn")
|
||||||
|
const text = document.getElementById("status-text")
|
||||||
|
const list = document.getElementById("course-list")
|
||||||
|
|
||||||
|
btn.addEventListener("click", function () {
|
||||||
|
text.textContent = "加载中..."
|
||||||
|
|
||||||
|
fakeFetchCourses()
|
||||||
|
.then(function (result) {
|
||||||
|
text.textContent = "加载成功"
|
||||||
|
const a = result.forEach(item => {
|
||||||
|
const li = document.createElement("li")
|
||||||
|
li.textContent = item
|
||||||
|
list.appendChild(li)
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
list.textContent = "失败" + error
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -16,3 +16,24 @@ function fakeFetchUser() {
|
|||||||
// 3. 用 await 等待 fakeFetchUser()
|
// 3. 用 await 等待 fakeFetchUser()
|
||||||
// 4. 渲染用户信息
|
// 4. 渲染用户信息
|
||||||
// 5. 用 try...catch 处理异常
|
// 5. 用 try...catch 处理异常
|
||||||
|
const btn = document.getElementById("load-user-btn")
|
||||||
|
const status = document.getElementById("user-status")
|
||||||
|
const card = document.getElementById("user-card")
|
||||||
|
|
||||||
|
async function a() {
|
||||||
|
status.textContent = "加载中"
|
||||||
|
card.innerHTML = ""
|
||||||
|
|
||||||
|
try {
|
||||||
|
const a = await fakeFetchUser()
|
||||||
|
status.textContent = "加载成功"
|
||||||
|
card.innerHTML = `
|
||||||
|
<p>姓名:${a.name}</p>
|
||||||
|
<p>角色:${a.role}</p>
|
||||||
|
<p>任务:${a.focus}</p>
|
||||||
|
`
|
||||||
|
} catch (error) {
|
||||||
|
status.textContent = "加载失败"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
btn.addEventListener("click", a)
|
||||||
@@ -20,3 +20,85 @@ function fakeLoadExtraLessons() {
|
|||||||
// 3. 点击列表项时切换 done 状态
|
// 3. 点击列表项时切换 done 状态
|
||||||
// 4. 提交表单时阻止默认提交,并新增课程
|
// 4. 提交表单时阻止默认提交,并新增课程
|
||||||
// 5. 点击加载按钮时显示加载状态,并把异步返回的课程追加到列表
|
// 5. 点击加载按钮时显示加载状态,并把异步返回的课程追加到列表
|
||||||
|
/* 一个课程列表区域
|
||||||
|
- 一个表单区域
|
||||||
|
- 一个添加课程的交互
|
||||||
|
- 一个点击课程切换完成状态的交互
|
||||||
|
- 一个异步加载提示或远程数据模拟
|
||||||
|
- 清晰的状态文案更新*/
|
||||||
|
const page = document.querySelector(".page")
|
||||||
|
const hero = document.querySelector("hero")
|
||||||
|
const text = document.getElementById("status-text")
|
||||||
|
const panel = document.querySelector(".panel")
|
||||||
|
const btn = document.getElementById("load-btn")
|
||||||
|
const list = document.getElementById("lesson-list")
|
||||||
|
const formPanel = document.querySelector(".form-panel")
|
||||||
|
const lessonForm = document.getElementById("lesson-form")
|
||||||
|
const lessonInput = document.getElementById("lesson-input")
|
||||||
|
|
||||||
|
function renderLessons() {
|
||||||
|
lessons.forEach(item => {
|
||||||
|
const li = document.createElement("li")
|
||||||
|
li.classList.add("li")
|
||||||
|
li.setAttribute("title", item.title)
|
||||||
|
li.setAttribute("done", item.done)
|
||||||
|
li.textContent = `
|
||||||
|
标题:${item.title}
|
||||||
|
状态:${item.done}`
|
||||||
|
|
||||||
|
list.appendChild(li)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
renderLessons()
|
||||||
|
|
||||||
|
//event.target,closest,
|
||||||
|
list.addEventListener("click", function (event) {
|
||||||
|
const a = event.target.closest(".li")
|
||||||
|
if (a) {
|
||||||
|
a.textContent = `
|
||||||
|
标题:${a.title}
|
||||||
|
状态:${!a.done}`
|
||||||
|
|
||||||
|
a.done = !a.done
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
lessonForm.addEventListener("submit", function (event) {
|
||||||
|
event.preventDefault()
|
||||||
|
const c = document.createElement("li")
|
||||||
|
const d = lessonInput.value.trim()
|
||||||
|
c.textContent = `
|
||||||
|
标题:${d}
|
||||||
|
状态:true`
|
||||||
|
c.setAttribute("title", d)
|
||||||
|
c.setAttribute("done", true)
|
||||||
|
c.classList.add("li")
|
||||||
|
lessonInput.value = ""
|
||||||
|
list.appendChild(c)
|
||||||
|
})
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
text.textContent = "加载中"
|
||||||
|
text.innerHTML = ""
|
||||||
|
try {
|
||||||
|
|
||||||
|
const s = await fakeLoadExtraLessons()
|
||||||
|
text.textContent = "加载完成"
|
||||||
|
s.forEach(item => {
|
||||||
|
const li = document.createElement("li")
|
||||||
|
li.classList.add("li")
|
||||||
|
li.setAttribute("title", item.title)
|
||||||
|
li.setAttribute("done", item.done)
|
||||||
|
li.textContent = `
|
||||||
|
标题:${item.title}
|
||||||
|
状态:${item.done}`
|
||||||
|
|
||||||
|
list.appendChild(li)
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
text.textContent = "加载失败"
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
btn.addEventListener("click", load)
|
||||||
@@ -4,3 +4,26 @@
|
|||||||
// 3. 把输入内容实时渲染到右侧预览
|
// 3. 把输入内容实时渲染到右侧预览
|
||||||
// 4. 监听阶段下拉框的 change 事件
|
// 4. 监听阶段下拉框的 change 事件
|
||||||
// 5. 更新阶段标签文字
|
// 5. 更新阶段标签文字
|
||||||
|
const nickname = document.getElementById("nickname-input")
|
||||||
|
const goal = document.getElementById("goal-input")
|
||||||
|
const stage = document.getElementById("stage-select")
|
||||||
|
|
||||||
|
nickname.addEventListener("input", function () {
|
||||||
|
const previewName = document.getElementById("preview-name")
|
||||||
|
previewName.textContent = nickname.value
|
||||||
|
if (!nickname.value.trim()) {
|
||||||
|
previewName.textContent = "未填写昵称"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
goal.addEventListener("input", function () {
|
||||||
|
const previewGoal = document.getElementById("preview-goal")
|
||||||
|
previewGoal.textContent = goal.value
|
||||||
|
if (!goal.value.trim()) {
|
||||||
|
previewGoal.textContent = "这里会显示你的学习目标。"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
stage.addEventListener("change", function () {
|
||||||
|
const previewStage = document.getElementById("preview-stage")
|
||||||
|
previewStage.textContent = stage.value
|
||||||
|
})
|
||||||
@@ -4,3 +4,26 @@
|
|||||||
// 3. 点击 clear-active-btn 时移除所有 item 的 active 类名
|
// 3. 点击 clear-active-btn 时移除所有 item 的 active 类名
|
||||||
// 4. 点击帮助链接时,用 preventDefault() 阻止跳转
|
// 4. 点击帮助链接时,用 preventDefault() 阻止跳转
|
||||||
// 5. 在提示文字里输出“已阻止默认跳转”
|
// 5. 在提示文字里输出“已阻止默认跳转”
|
||||||
|
const prependBtn = document.getElementById("prepend-btn")
|
||||||
|
const clear = document.getElementById("clear-active-btn")
|
||||||
|
const helpLink = document.getElementById("help-link")
|
||||||
|
const hintText = document.getElementById("hint-text")
|
||||||
|
const messageList = document.getElementById("message-list")
|
||||||
|
|
||||||
|
prependBtn.addEventListener("click", function () {
|
||||||
|
const li = document.createElement("li")
|
||||||
|
li.classList.add("item")
|
||||||
|
messageList.prepend(li)
|
||||||
|
})
|
||||||
|
|
||||||
|
clear.addEventListener("click", function () {
|
||||||
|
const a = document.querySelectorAll("#message-list li")
|
||||||
|
a.forEach(item => {
|
||||||
|
item.classList.remove("active")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
helpLink.addEventListener("click", function (event) {
|
||||||
|
event.preventDefault()
|
||||||
|
hintText.textContent = "已阻止默认跳转"
|
||||||
|
})
|
||||||
@@ -9,3 +9,10 @@ const scores = [88, 92, 95];
|
|||||||
// 1. 解构出 name、stage
|
// 1. 解构出 name、stage
|
||||||
// 2. 解构出前两个分数
|
// 2. 解构出前两个分数
|
||||||
// 3. 用模板字符串把信息写入 #output
|
// 3. 用模板字符串把信息写入 #output
|
||||||
|
const { name, stage } = student
|
||||||
|
const [a, b] = scores
|
||||||
|
const output = document.getElementById("output")
|
||||||
|
output.innerHTML = `
|
||||||
|
a = ${a}
|
||||||
|
b = ${b}
|
||||||
|
`
|
||||||
@@ -7,6 +7,14 @@ const user = {
|
|||||||
|
|
||||||
function sumScores(...scores) {
|
function sumScores(...scores) {
|
||||||
// 返回总分
|
// 返回总分
|
||||||
|
/*let total = 0
|
||||||
|
scores.forEach(item => {
|
||||||
|
total = total+item
|
||||||
|
})
|
||||||
|
return total*/
|
||||||
|
return scores.reduce((acc, item) => {
|
||||||
|
return acc + item
|
||||||
|
}, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 任务:
|
// 任务:
|
||||||
@@ -14,3 +22,11 @@ function sumScores(...scores) {
|
|||||||
// 2. 基于 user 生成一个 stage 为 "现代 JS" 的新对象
|
// 2. 基于 user 生成一个 stage 为 "现代 JS" 的新对象
|
||||||
// 3. 调用 sumScores
|
// 3. 调用 sumScores
|
||||||
// 4. 输出结果到 #output
|
// 4. 输出结果到 #output
|
||||||
|
const tracks1 = [...tracks, "ES6+"]
|
||||||
|
const user1 = {
|
||||||
|
...user,
|
||||||
|
stage: "现代 JS"
|
||||||
|
}
|
||||||
|
const sum = sumScores(1, 2, 3, 4)
|
||||||
|
const output = document.getElementById("output")
|
||||||
|
output.textContent = sum
|
||||||
@@ -12,3 +12,7 @@ const tracks = ["DOM", "异步", "模块化"];
|
|||||||
// 1. 把 getLevel 改成箭头函数
|
// 1. 把 getLevel 改成箭头函数
|
||||||
// 2. 把 add 改成箭头函数
|
// 2. 把 add 改成箭头函数
|
||||||
// 3. 用 map + 箭头函数生成 ["[DOM]", ...]
|
// 3. 用 map + 箭头函数生成 ["[DOM]", ...]
|
||||||
|
const getLevel1 = score => score >= 80
|
||||||
|
const add1 = (a, b) => a + b
|
||||||
|
const tracks1 = tracks.map(item => `[${item}]`)
|
||||||
|
console.log(tracks1);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
# 练习 4:模块基础
|
#
|
||||||
|
练习 4:模块基础
|
||||||
|
|
||||||
## 目标
|
## 目标
|
||||||
|
|
||||||
@@ -9,7 +10,6 @@
|
|||||||
- `export`
|
- `export`
|
||||||
- `import`
|
- `import`
|
||||||
- `type="module"`
|
- `type="module"`
|
||||||
|
|
||||||
## 任务
|
## 任务
|
||||||
|
|
||||||
- 从模块文件里导入课程数据和格式化函数
|
- 从模块文件里导入课程数据和格式化函数
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
// 任务:
|
// 任务:
|
||||||
// 1. 从 ./course-data.js 导入 courses 和 formatCourse
|
// 1. 从 ./course-data.js 导入 courses 和 formatCourse
|
||||||
// 2. 把格式化后的结果写入 #output
|
// 2. 把格式化后的结果写入 #output
|
||||||
|
import { courses, formatCourse } from "./course-data.js"
|
||||||
|
const a = courses.map(item => formatCourse(item))
|
||||||
|
const output = document.getElementById("output")
|
||||||
|
output.textContent = a
|
||||||
@@ -10,3 +10,17 @@ function loadTracks() {
|
|||||||
// 1. 调用 loadTracks()
|
// 1. 调用 loadTracks()
|
||||||
// 2. 成功时更新状态和输出内容
|
// 2. 成功时更新状态和输出内容
|
||||||
// 3. 失败时更新状态
|
// 3. 失败时更新状态
|
||||||
|
async function load() {
|
||||||
|
const status = document.getElementById("status")
|
||||||
|
status.textContent = "加载中"
|
||||||
|
try {
|
||||||
|
const a = await loadTracks()
|
||||||
|
status.textContent = "加载成功"
|
||||||
|
const output = document.getElementById("output")
|
||||||
|
output.textContent = a
|
||||||
|
} catch (error) {
|
||||||
|
status.textContent = "加载失败"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
load()
|
||||||
@@ -13,3 +13,16 @@ function loadConfig() {
|
|||||||
// 1. 写一个 async 函数
|
// 1. 写一个 async 函数
|
||||||
// 2. 用 await 等待 loadConfig()
|
// 2. 用 await 等待 loadConfig()
|
||||||
// 3. 用 try...catch 处理流程
|
// 3. 用 try...catch 处理流程
|
||||||
|
async function load() {
|
||||||
|
const status = document.getElementById("status")
|
||||||
|
status.textContent = "加载中"
|
||||||
|
try {
|
||||||
|
const a = await loadConfig()
|
||||||
|
status.textContent = "加载成功"
|
||||||
|
const output = document.getElementById("output")
|
||||||
|
output.textContent = JSON.stringify(a)
|
||||||
|
} catch (error) {
|
||||||
|
status.textContent = "加载失败"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
load()
|
||||||
@@ -1,6 +1,11 @@
|
|||||||
const trainer = {
|
const trainer = {
|
||||||
name: "现代 JS 训练营",
|
name: "现代 JS 训练营",
|
||||||
report() {
|
report() {
|
||||||
|
setTimeout(() => {
|
||||||
|
const a = this.name
|
||||||
|
const output = document.getElementById("output")
|
||||||
|
output.textContent = a
|
||||||
|
}, 1000)
|
||||||
// 任务:
|
// 任务:
|
||||||
// 1. 用 setTimeout
|
// 1. 用 setTimeout
|
||||||
// 2. 在回调里用箭头函数读取 this.name
|
// 2. 在回调里用箭头函数读取 this.name
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
const loadButton = document.getElementById("load-btn");
|
const loadButton = document.getElementById("load-btn");
|
||||||
const status = document.getElementById("status");
|
const status1 = document.getElementById("status");
|
||||||
const output = document.getElementById("output");
|
const output = document.getElementById("output");
|
||||||
|
|
||||||
// 任务:
|
// 任务:
|
||||||
@@ -9,3 +9,17 @@ const output = document.getElementById("output");
|
|||||||
// 4. 调用 res.json() 解析数据
|
// 4. 调用 res.json() 解析数据
|
||||||
// 5. 把 title 和 body 渲染到页面
|
// 5. 把 title 和 body 渲染到页面
|
||||||
// 6. 失败时显示“加载失败”
|
// 6. 失败时显示“加载失败”
|
||||||
|
loadButton.addEventListener("click", async () => {
|
||||||
|
status1.textContent = "加载中..."
|
||||||
|
try {
|
||||||
|
let a = await fetch("https://jsonplaceholder.typicode.com/posts/1")
|
||||||
|
let data = await a.json()
|
||||||
|
status1.textContent = "加载成功"
|
||||||
|
const { title, body } = data
|
||||||
|
output.textContent = `title:${title}
|
||||||
|
body:${body}`
|
||||||
|
} catch (error) {
|
||||||
|
status1.textContent = "加载失败"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -5,3 +5,28 @@
|
|||||||
// 4. 用展开运算符合并技能
|
// 4. 用展开运算符合并技能
|
||||||
// 5. 用模板字符串渲染标题和说明
|
// 5. 用模板字符串渲染标题和说明
|
||||||
// 6. 用 forEach 或 map + 箭头函数渲染列表
|
// 6. 用 forEach 或 map + 箭头函数渲染列表
|
||||||
|
import { baseSummary, loadExtraSkills } from './summary-service.js'
|
||||||
|
async function get() {
|
||||||
|
const title = document.getElementById("title")
|
||||||
|
const intro = document.getElementById("intro")
|
||||||
|
title.textContent = '加载中...'
|
||||||
|
intro.innerHTML = ''
|
||||||
|
try {
|
||||||
|
let load = await loadExtraSkills()
|
||||||
|
title.textContent = '加载成功'
|
||||||
|
const { name, stage, skills } = baseSummary
|
||||||
|
intro.textContent = `
|
||||||
|
name:${name}
|
||||||
|
stage:${stage}`
|
||||||
|
load = [...load, ...skills]
|
||||||
|
load.forEach(item => {
|
||||||
|
const li = document.createElement("li")
|
||||||
|
const skillList = document.getElementById("skill-list")
|
||||||
|
li.textContent = item
|
||||||
|
skillList.appendChild(li)
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
title.textContent = '加载失败'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
get()
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
let age: number = 18;
|
let age: number = 88;
|
||||||
let userName: string = "Tom";
|
let userName: string = "Tom";
|
||||||
let isPaid: boolean = true;
|
let isPaid: boolean = true;
|
||||||
|
|
||||||
// 任务:
|
// 任务:
|
||||||
// 1. 修改上面的值,让它们更像一个学习者资料
|
// 1. 修改上面的值,让它们更像一个学习者资料
|
||||||
// 2. 输出一句完整信息
|
// 2. 输出一句完整信息
|
||||||
|
console.log(`姓名:${userName}
|
||||||
|
年龄:${age}
|
||||||
|
状态:${isPaid}`);
|
||||||
|
|||||||
@@ -2,9 +2,12 @@ const scoreList: number[] = [82, 90, 95];
|
|||||||
|
|
||||||
function sum(a: number, b: number): number {
|
function sum(a: number, b: number): number {
|
||||||
// 返回两个数字之和
|
// 返回两个数字之和
|
||||||
return 0;
|
return a + b;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
console.log(scoreList, sum(2, 3));
|
||||||
|
|
||||||
// 任务:
|
// 任务:
|
||||||
// 1. 改写 sum 的返回值
|
// 1. 改写 sum 的返回值
|
||||||
// 2. 调用 sum
|
// 2. 调用 sum
|
||||||
|
|||||||
@@ -13,3 +13,4 @@ const user: User = {
|
|||||||
// 任务:
|
// 任务:
|
||||||
// 1. 把对象内容改成学习者资料
|
// 1. 把对象内容改成学习者资料
|
||||||
// 2. 输出 name 和 age
|
// 2. 输出 name 和 age
|
||||||
|
console.log(user.name, user.age);
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ function getData<T>(data: T): T {
|
|||||||
// 返回 data
|
// 返回 data
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
const n = getData<number>(100)
|
||||||
|
const s = getData("123")
|
||||||
|
console.log(n, s);
|
||||||
|
|
||||||
// 任务:
|
// 任务:
|
||||||
// 1. 用 number 调用
|
// 1. 用 number 调用
|
||||||
|
|||||||
@@ -4,6 +4,15 @@ interface User {
|
|||||||
name: string;
|
name: string;
|
||||||
age?: number;
|
age?: number;
|
||||||
}
|
}
|
||||||
|
id = "hello"
|
||||||
|
const userA: User = {
|
||||||
|
name: "小李",
|
||||||
|
age: 18
|
||||||
|
}
|
||||||
|
const userB: User = {
|
||||||
|
name: "小柔"
|
||||||
|
}
|
||||||
|
console.log(userA, userB);
|
||||||
|
|
||||||
// 任务:
|
// 任务:
|
||||||
// 1. 把 id 改成字符串也试一次
|
// 1. 把 id 改成字符串也试一次
|
||||||
|
|||||||
@@ -11,8 +11,12 @@ const courses: Course[] = [
|
|||||||
|
|
||||||
function renderCourseLines(list: Course[]): string[] {
|
function renderCourseLines(list: Course[]): string[] {
|
||||||
// 返回渲染后的字符串数组
|
// 返回渲染后的字符串数组
|
||||||
return [];
|
const a = list.map(item => {
|
||||||
|
return item.title
|
||||||
|
})
|
||||||
|
return a;
|
||||||
}
|
}
|
||||||
|
console.log(renderCourseLines(courses));
|
||||||
|
|
||||||
// 任务:
|
// 任务:
|
||||||
// 1. 实现 renderCourseLines
|
// 1. 实现 renderCourseLines
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ function pickFirst<T>(list: T[]): T {
|
|||||||
|
|
||||||
function formatStudent(student: Student): string {
|
function formatStudent(student: Student): string {
|
||||||
// 返回一段摘要文字
|
// 返回一段摘要文字
|
||||||
return "";
|
return `学号:${student.id}
|
||||||
|
姓名:${student.name}
|
||||||
|
年龄:${student.age?.toFixed()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const student: Student = {
|
const student: Student = {
|
||||||
@@ -30,6 +32,8 @@ const student: Student = {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log(formatStudent(student), pickFirst(student.courses));
|
||||||
|
|
||||||
// 任务:
|
// 任务:
|
||||||
// 1. 实现 formatStudent
|
// 1. 实现 formatStudent
|
||||||
// 2. 用 pickFirst 取第一门课程
|
// 2. 用 pickFirst 取第一门课程
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ new Vue({
|
|||||||
// 任务:
|
// 任务:
|
||||||
// 1. learnerCount 加 1
|
// 1. learnerCount 加 1
|
||||||
// 2. 在控制台输出最新人数
|
// 2. 在控制台输出最新人数
|
||||||
|
this.learnerCount++
|
||||||
|
console.log(this.learnerCount);
|
||||||
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,28 +1,62 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>模板绑定</title>
|
|
||||||
<style>
|
|
||||||
body { margin: 0; padding: 32px; font-family: "PingFang SC", sans-serif; background: #f6f8fb; }
|
|
||||||
.card { max-width: 680px; margin: 0 auto; padding: 24px; border-radius: 20px; background: #fff; border: 1px solid #dde6f5; }
|
|
||||||
img { width: 100%; border-radius: 16px; }
|
|
||||||
.link { display: inline-block; margin-top: 12px; color: #2d6cdf; }
|
|
||||||
button { margin-top: 18px; padding: 10px 16px; border: 0; border-radius: 999px; background: #15233f; color: #fff; cursor: pointer; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<section id="app" class="card">
|
|
||||||
<h1>{{ courseTitle }}</h1>
|
|
||||||
<p>讲师:{{ teacher }}</p>
|
|
||||||
<img :src="coverUrl" :alt="courseTitle" />
|
|
||||||
<a class="link" :href="courseLink" target="_blank">查看课程详情</a>
|
|
||||||
<p>当前状态:{{ isCollected ? "已收藏" : "未收藏" }}</p>
|
|
||||||
<button type="button" @click="toggleCollect">切换收藏状态</button>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
|
<head>
|
||||||
<script src="./starter.js"></script>
|
<meta charset="UTF-8" />
|
||||||
</body>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
</html>
|
<title>模板绑定</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 32px;
|
||||||
|
font-family: "PingFang SC", sans-serif;
|
||||||
|
background: #f6f8fb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
max-width: 680px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 24px;
|
||||||
|
border-radius: 20px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #dde6f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link {
|
||||||
|
display: inline-block;
|
||||||
|
margin-top: 12px;
|
||||||
|
color: #2d6cdf;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin-top: 18px;
|
||||||
|
padding: 10px 16px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #15233f;
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<section id="app" class="card">
|
||||||
|
<h1>{{ courseTitle }}</h1>
|
||||||
|
<p>讲师:{{ teacher }}</p>
|
||||||
|
<img :src="coverUrl" :alt="courseTitle" />
|
||||||
|
<a class="link" :href="courseLink" target="_blank">查看课程详情</a>
|
||||||
|
<p>当前状态:{{ isCollected ? "已收藏" : "未收藏" }}</p>
|
||||||
|
<button type="button" @click="toggleCollect">切换收藏状态</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
|
||||||
|
<script src="./starter.js"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -12,6 +12,9 @@ new Vue({
|
|||||||
// 任务:
|
// 任务:
|
||||||
// 1. 切换 isCollected
|
// 1. 切换 isCollected
|
||||||
// 2. 在控制台输出最新收藏状态
|
// 2. 在控制台输出最新收藏状态
|
||||||
|
this.isCollected = !this.isCollected
|
||||||
|
console.log(this.isCollected);
|
||||||
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,28 +1,50 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>computed 和 watch</title>
|
|
||||||
<style>
|
|
||||||
body { margin: 0; padding: 32px; font-family: "PingFang SC", sans-serif; background: #f7f9fd; }
|
|
||||||
.panel { max-width: 760px; margin: 0 auto; padding: 24px; border-radius: 18px; background: #fff; border: 1px solid #d9e2f0; }
|
|
||||||
input { width: 100%; padding: 12px; border-radius: 12px; border: 1px solid #ccd7e9; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<section id="app" class="panel">
|
|
||||||
<h1>课程搜索</h1>
|
|
||||||
<input v-model="keyword" type="text" placeholder="输入关键字" />
|
|
||||||
<p>匹配数量:{{ matchedCount }}</p>
|
|
||||||
<ul>
|
|
||||||
<li v-for="item in filteredCourses" :key="item.id">
|
|
||||||
{{ item.title }}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
|
<head>
|
||||||
<script src="./starter.js"></script>
|
<meta charset="UTF-8" />
|
||||||
</body>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
</html>
|
<title>computed 和 watch</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 32px;
|
||||||
|
font-family: "PingFang SC", sans-serif;
|
||||||
|
background: #f7f9fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
max-width: 760px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 24px;
|
||||||
|
border-radius: 18px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #d9e2f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid #ccd7e9;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<section id="app" class="panel">
|
||||||
|
<h1>课程搜索</h1>
|
||||||
|
<input v-model="keyword" type="text" placeholder="输入关键字" />
|
||||||
|
<p>匹配数量:{{ matchedCount }}</p>
|
||||||
|
<ul>
|
||||||
|
<li v-for="item in filteredCourses" :key="item.id">
|
||||||
|
{{ item.title }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
|
||||||
|
<script src="./starter.js"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -11,16 +11,19 @@ new Vue({
|
|||||||
computed: {
|
computed: {
|
||||||
filteredCourses() {
|
filteredCourses() {
|
||||||
// 任务:返回过滤后的课程列表
|
// 任务:返回过滤后的课程列表
|
||||||
return this.courses;
|
const value = this.keyword.trim().toUpperCase()
|
||||||
|
return this.courses.filter(item => item.title.toUpperCase().includes(value))
|
||||||
},
|
},
|
||||||
matchedCount() {
|
matchedCount() {
|
||||||
// 任务:返回 filteredCourses 的数量
|
// 任务:返回 filteredCourses 的数量
|
||||||
return 0;
|
return this.filteredCourses.length;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
keyword(newValue) {
|
keyword(newValue) {
|
||||||
// 任务:在控制台输出关键字变化
|
// 任务:在控制台输出关键字变化
|
||||||
|
console.log(`筛选关键词:${newValue}`);
|
||||||
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,31 +1,66 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>动态类名、样式和事件</title>
|
|
||||||
<style>
|
|
||||||
body { margin: 0; padding: 32px; font-family: "PingFang SC", sans-serif; background: #f4f8fb; }
|
|
||||||
.panel { max-width: 760px; margin: 0 auto; }
|
|
||||||
.card { padding: 24px; border-radius: 18px; background: #fff; border: 1px solid #d8e4f3; cursor: pointer; transition: all .2s ease; }
|
|
||||||
.card.active { border-color: #2d6cdf; background: #eef4ff; }
|
|
||||||
.progress-track { height: 12px; margin-top: 16px; border-radius: 999px; background: #e6edf8; overflow: hidden; }
|
|
||||||
.progress-bar { height: 100%; background: linear-gradient(90deg, #2d6cdf, #58a2ff); }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<section id="app" class="panel">
|
|
||||||
<article class="card" :class="{ active: isActive }" @click="toggleCard">
|
|
||||||
<h1>{{ title }}</h1>
|
|
||||||
<p>{{ isActive ? "当前卡片已激活" : "点击卡片激活它" }}</p>
|
|
||||||
<div class="progress-track">
|
|
||||||
<div class="progress-bar" :style="{ width: progress + '%' }"></div>
|
|
||||||
</div>
|
|
||||||
<p>当前进度:{{ progress }}%</p>
|
|
||||||
</article>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
|
<head>
|
||||||
<script src="./starter.js"></script>
|
<meta charset="UTF-8" />
|
||||||
</body>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
</html>
|
<title>动态类名、样式和事件</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 32px;
|
||||||
|
font-family: "PingFang SC", sans-serif;
|
||||||
|
background: #f4f8fb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
max-width: 760px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
padding: 24px;
|
||||||
|
border-radius: 18px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #d8e4f3;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all .2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card.active {
|
||||||
|
border-color: #2d6cdf;
|
||||||
|
background: #eef4ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-track {
|
||||||
|
height: 12px;
|
||||||
|
margin-top: 16px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #e6edf8;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, #2d6cdf, #58a2ff);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<section id="app" class="panel">
|
||||||
|
<article class="card" :class="{ active: isActive }" @click="toggleCard">
|
||||||
|
<h1>{{ title }}</h1>
|
||||||
|
<p>{{ isActive ? "当前卡片已激活" : "点击卡片激活它" }}</p>
|
||||||
|
<div class="progress-track">
|
||||||
|
<div class="progress-bar" :style="{ width: progress + '%' }"></div>
|
||||||
|
</div>
|
||||||
|
<p>当前进度:{{ progress }}%</p>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
|
||||||
|
<script src="./starter.js"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -9,7 +9,13 @@ new Vue({
|
|||||||
toggleCard() {
|
toggleCard() {
|
||||||
// 任务:
|
// 任务:
|
||||||
// 1. 切换 isActive
|
// 1. 切换 isActive
|
||||||
|
this.isActive = !this.isActive
|
||||||
// 2. 如果激活了,让 progress 增加到 80
|
// 2. 如果激活了,让 progress 增加到 80
|
||||||
|
if (this.isActive) {
|
||||||
|
this.progress = 80
|
||||||
|
} else {
|
||||||
|
this.progress = 35
|
||||||
|
}
|
||||||
// 3. 如果取消激活,让 progress 回到 35
|
// 3. 如果取消激活,让 progress 回到 35
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -12,19 +12,37 @@ new Vue({
|
|||||||
// 1. 模拟异步请求
|
// 1. 模拟异步请求
|
||||||
// 2. 1 秒后给 courses 赋值
|
// 2. 1 秒后给 courses 赋值
|
||||||
// 3. loading 改成 false
|
// 3. loading 改成 false
|
||||||
|
console.log("1");
|
||||||
|
|
||||||
|
const thiz = this
|
||||||
|
setTimeout(function () {
|
||||||
|
thiz.courses = [
|
||||||
|
{ id: 1, title: 'HTML' },
|
||||||
|
{ id: 2, title: 'CSS' },
|
||||||
|
{ id: 3, title: 'JS' }
|
||||||
|
]
|
||||||
|
thiz.loading = false
|
||||||
|
}, 1000)
|
||||||
},
|
},
|
||||||
updated() {
|
updated() {
|
||||||
// 任务:在控制台输出 updated 日志
|
// 任务:在控制台输出 updated 日志
|
||||||
|
console.log("更新");
|
||||||
|
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
// 任务:在控制台输出 beforeDestroy 日志
|
// 任务:在控制台输出 beforeDestroy 日志
|
||||||
|
console.log("已销毁");
|
||||||
|
|
||||||
},
|
},
|
||||||
destroyed() {
|
destroyed() {
|
||||||
// 任务:在控制台输出 destroyed 日志
|
// 任务:在控制台输出 destroyed 日志
|
||||||
|
console.log("销毁完成");
|
||||||
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
destroyInstance() {
|
destroyInstance() {
|
||||||
// 任务:调用 this.$destroy()
|
// 任务:调用 this.$destroy()
|
||||||
|
this.$destroy()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -52,15 +52,23 @@ new Vue({
|
|||||||
computed: {
|
computed: {
|
||||||
filteredCourses() {
|
filteredCourses() {
|
||||||
// 任务:根据 keyword 过滤课程列表
|
// 任务:根据 keyword 过滤课程列表
|
||||||
return this.courses;
|
let value = this.keyword.trim().toUpperCase()
|
||||||
|
return this.courses.filter(item => item.title.toUpperCase().includes(value))
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
toggleCourse(courseId) {
|
toggleCourse(courseId) {
|
||||||
// 任务:根据 courseId 切换 finished
|
// 任务:根据 courseId 切换 finished
|
||||||
|
const a = this.courses.find(item => {
|
||||||
|
if (item.id === courseId) {
|
||||||
|
item.finished = !item.finished
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
})
|
||||||
},
|
},
|
||||||
focusInput() {
|
focusInput() {
|
||||||
// 任务:通过 ref 聚焦输入框
|
// 任务:通过 ref 聚焦输入框
|
||||||
|
this.$refs.keywordInput.focus()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -49,31 +49,49 @@ new Vue({
|
|||||||
// 任务:
|
// 任务:
|
||||||
// 1. 先按 statusFilter 过滤
|
// 1. 先按 statusFilter 过滤
|
||||||
// 2. 再按 keyword 过滤标题
|
// 2. 再按 keyword 过滤标题
|
||||||
return this.courses;
|
let value = this.keyword.trim().toUpperCase()
|
||||||
|
let a
|
||||||
|
if (this.statusFilter === "all") {
|
||||||
|
a = this.courses.filter(item => item.title.toUpperCase().includes(value))
|
||||||
|
} else if (this.statusFilter === "done") {
|
||||||
|
const b = this.courses.filter(item => item.finished === true)
|
||||||
|
a = b.filter(item => item.title.toUpperCase().includes(value))
|
||||||
|
} else if (this.statusFilter === "doing") {
|
||||||
|
const b = this.courses.filter(item => item.finished === false)
|
||||||
|
a = b.filter(item => item.title.toUpperCase().includes(value))
|
||||||
|
}
|
||||||
|
return a
|
||||||
},
|
},
|
||||||
totalCount() {
|
totalCount() {
|
||||||
return this.courses.length;
|
return this.courses.length;
|
||||||
},
|
},
|
||||||
finishedCount() {
|
finishedCount() {
|
||||||
// 任务:返回已完成课程数量
|
// 任务:返回已完成课程数量
|
||||||
return 0;
|
const b = this.courses.filter(item => item.finished === true)
|
||||||
|
return b.length
|
||||||
},
|
},
|
||||||
visibleCount() {
|
visibleCount() {
|
||||||
// 任务:返回当前筛选后的数量
|
// 任务:返回当前筛选后的数量
|
||||||
return 0;
|
const a = this.filteredCourses
|
||||||
|
return a.length;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
keyword(newValue) {
|
keyword(newValue) {
|
||||||
// 任务:在控制台输出关键字变化
|
// 任务:在控制台输出关键字变化
|
||||||
|
console.log(`关键词:${newValue}`);
|
||||||
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
toggleCourse(courseId) {
|
toggleCourse(courseId) {
|
||||||
// 任务:根据 courseId 切换 finished
|
// 任务:根据 courseId 切换 finished
|
||||||
|
const a = this.courses.find(item => item.id === courseId)
|
||||||
|
a.finished = !a.finished
|
||||||
},
|
},
|
||||||
focusSearch() {
|
focusSearch() {
|
||||||
// 任务:通过 ref 聚焦输入框
|
// 任务:通过 ref 聚焦输入框
|
||||||
|
this.$refs.searchInput.focus()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
23
08-vue3/01-create-app-and-ref/README.md
Normal file
23
08-vue3/01-create-app-and-ref/README.md
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# 练习 1:createApp、setup 和 ref
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
学会用 Vue3 的 `createApp` 和 `setup()` 启动页面,并用 `ref` 创建最基础的响应式数据。
|
||||||
|
|
||||||
|
## 你要练什么
|
||||||
|
|
||||||
|
- `createApp`
|
||||||
|
- `setup()`
|
||||||
|
- `ref`
|
||||||
|
- 模板插值
|
||||||
|
|
||||||
|
## 任务
|
||||||
|
|
||||||
|
- 显示页面标题和学习人数
|
||||||
|
- 点击按钮后让人数加 1
|
||||||
|
- 在控制台输出最新人数
|
||||||
|
|
||||||
|
## 文件
|
||||||
|
|
||||||
|
- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/01-create-app-and-ref/starter.html)
|
||||||
|
- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/01-create-app-and-ref/starter.js)
|
||||||
23
08-vue3/01-create-app-and-ref/starter.html
Normal file
23
08-vue3/01-create-app-and-ref/starter.html
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>createApp、setup 和 ref</title>
|
||||||
|
<style>
|
||||||
|
body { margin: 0; padding: 32px; font-family: "PingFang SC", sans-serif; background: #f5f7fb; }
|
||||||
|
.panel { max-width: 720px; margin: 0 auto; padding: 24px; border-radius: 18px; background: #fff; border: 1px solid #dce5f2; }
|
||||||
|
button { padding: 10px 16px; border: 0; border-radius: 999px; background: #2d6cdf; color: #fff; cursor: pointer; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<section id="app" class="panel">
|
||||||
|
<h1>{{ title }}</h1>
|
||||||
|
<p>当前学习人数:{{ learnerCount }}</p>
|
||||||
|
<button type="button" @click="increaseLearner">加入学习</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script>
|
||||||
|
<script src="./starter.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
20
08-vue3/01-create-app-and-ref/starter.js
Normal file
20
08-vue3/01-create-app-and-ref/starter.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
const { createApp, ref } = Vue;
|
||||||
|
|
||||||
|
createApp({
|
||||||
|
setup() {
|
||||||
|
const title = ref("Vue3 基础入门");
|
||||||
|
const learnerCount = ref(16);
|
||||||
|
|
||||||
|
function increaseLearner() {
|
||||||
|
// 任务:
|
||||||
|
// 1. learnerCount 加 1
|
||||||
|
// 2. 在控制台输出最新人数
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
learnerCount,
|
||||||
|
increaseLearner,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}).mount("#app");
|
||||||
23
08-vue3/02-reactive-and-computed/README.md
Normal file
23
08-vue3/02-reactive-and-computed/README.md
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# 练习 2:reactive 和 computed
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
学会把一组相关数据放进 `reactive`,并用 `computed` 推导派生结果。
|
||||||
|
|
||||||
|
## 你要练什么
|
||||||
|
|
||||||
|
- `reactive`
|
||||||
|
- `computed`
|
||||||
|
- 响应式对象
|
||||||
|
- 派生状态
|
||||||
|
|
||||||
|
## 任务
|
||||||
|
|
||||||
|
- 用 `reactive` 管理课程信息
|
||||||
|
- 用 `computed` 计算进度文案
|
||||||
|
- 点击按钮后让完成课时增加
|
||||||
|
|
||||||
|
## 文件
|
||||||
|
|
||||||
|
- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/02-reactive-and-computed/starter.html)
|
||||||
|
- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/02-reactive-and-computed/starter.js)
|
||||||
25
08-vue3/02-reactive-and-computed/starter.html
Normal file
25
08-vue3/02-reactive-and-computed/starter.html
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>reactive 和 computed</title>
|
||||||
|
<style>
|
||||||
|
body { margin: 0; padding: 32px; font-family: "PingFang SC", sans-serif; background: #f6f8fc; }
|
||||||
|
.panel { max-width: 720px; margin: 0 auto; padding: 24px; border-radius: 18px; background: #fff; border: 1px solid #dbe4f1; }
|
||||||
|
button { padding: 10px 16px; border: 0; border-radius: 999px; background: #1c2f52; color: #fff; cursor: pointer; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<section id="app" class="panel">
|
||||||
|
<h1>{{ course.title }}</h1>
|
||||||
|
<p>总课时:{{ course.totalLessons }}</p>
|
||||||
|
<p>已完成:{{ course.finishedLessons }}</p>
|
||||||
|
<p>{{ progressText }}</p>
|
||||||
|
<button type="button" @click="finishOneLesson">完成一节</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script>
|
||||||
|
<script src="./starter.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
26
08-vue3/02-reactive-and-computed/starter.js
Normal file
26
08-vue3/02-reactive-and-computed/starter.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
const { createApp, reactive, computed } = Vue;
|
||||||
|
|
||||||
|
createApp({
|
||||||
|
setup() {
|
||||||
|
const course = reactive({
|
||||||
|
title: "Vue3 响应式基础",
|
||||||
|
totalLessons: 10,
|
||||||
|
finishedLessons: 3,
|
||||||
|
});
|
||||||
|
|
||||||
|
const progressText = computed(() => {
|
||||||
|
// 任务:返回类似 “当前已完成 3 / 10 节”
|
||||||
|
return "";
|
||||||
|
});
|
||||||
|
|
||||||
|
function finishOneLesson() {
|
||||||
|
// 任务:在不超过总课时的前提下,finishedLessons 加 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
course,
|
||||||
|
progressText,
|
||||||
|
finishOneLesson,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}).mount("#app");
|
||||||
23
08-vue3/03-watch-and-watch-effect/README.md
Normal file
23
08-vue3/03-watch-and-watch-effect/README.md
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# 练习 3:watch 和 watchEffect
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
学会区分“监听指定数据变化”和“自动收集依赖并执行副作用”。
|
||||||
|
|
||||||
|
## 你要练什么
|
||||||
|
|
||||||
|
- `watch`
|
||||||
|
- `watchEffect`
|
||||||
|
- 搜索关键字监听
|
||||||
|
- 副作用日志
|
||||||
|
|
||||||
|
## 任务
|
||||||
|
|
||||||
|
- 输入关键字时,用 `watch` 输出变化日志
|
||||||
|
- 用 `watchEffect` 输出当前筛选信息
|
||||||
|
- 根据关键字过滤课程列表
|
||||||
|
|
||||||
|
## 文件
|
||||||
|
|
||||||
|
- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/03-watch-and-watch-effect/starter.html)
|
||||||
|
- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/03-watch-and-watch-effect/starter.js)
|
||||||
25
08-vue3/03-watch-and-watch-effect/starter.html
Normal file
25
08-vue3/03-watch-and-watch-effect/starter.html
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>watch 和 watchEffect</title>
|
||||||
|
<style>
|
||||||
|
body { margin: 0; padding: 32px; font-family: "PingFang SC", sans-serif; background: #f7f9fd; }
|
||||||
|
.panel { max-width: 760px; margin: 0 auto; padding: 24px; border-radius: 18px; background: #fff; border: 1px solid #d9e2f0; }
|
||||||
|
input { width: 100%; padding: 12px; border-radius: 12px; border: 1px solid #ccd7e9; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<section id="app" class="panel">
|
||||||
|
<h1>课程搜索</h1>
|
||||||
|
<input v-model="keyword" type="text" placeholder="输入关键字" />
|
||||||
|
<ul>
|
||||||
|
<li v-for="item in filteredCourses" :key="item.id">{{ item.title }}</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script>
|
||||||
|
<script src="./starter.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
30
08-vue3/03-watch-and-watch-effect/starter.js
Normal file
30
08-vue3/03-watch-and-watch-effect/starter.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
const { createApp, ref, computed, watch, watchEffect } = Vue;
|
||||||
|
|
||||||
|
createApp({
|
||||||
|
setup() {
|
||||||
|
const keyword = ref("");
|
||||||
|
const courses = ref([
|
||||||
|
{ id: 1, title: "ref 和 reactive" },
|
||||||
|
{ id: 2, title: "watch 和 watchEffect" },
|
||||||
|
{ id: 3, title: "组件通信" },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const filteredCourses = computed(() => {
|
||||||
|
// 任务:根据 keyword 过滤课程
|
||||||
|
return courses.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(keyword, (newValue, oldValue) => {
|
||||||
|
// 任务:输出关键字变化日志
|
||||||
|
});
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
// 任务:输出当前筛选后的数量
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
keyword,
|
||||||
|
filteredCourses,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}).mount("#app");
|
||||||
23
08-vue3/04-lifecycle-and-template-ref/README.md
Normal file
23
08-vue3/04-lifecycle-and-template-ref/README.md
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# 练习 4:生命周期和模板 ref
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
学会在组合式 API 里使用生命周期钩子,并通过模板 `ref` 获取 DOM。
|
||||||
|
|
||||||
|
## 你要练什么
|
||||||
|
|
||||||
|
- `onMounted`
|
||||||
|
- `onUpdated`
|
||||||
|
- `onUnmounted`
|
||||||
|
- 模板 `ref`
|
||||||
|
|
||||||
|
## 任务
|
||||||
|
|
||||||
|
- 页面挂载后自动聚焦输入框
|
||||||
|
- 数据更新后输出日志
|
||||||
|
- 页面卸载前清理定时器
|
||||||
|
|
||||||
|
## 文件
|
||||||
|
|
||||||
|
- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/04-lifecycle-and-template-ref/starter.html)
|
||||||
|
- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/04-lifecycle-and-template-ref/starter.js)
|
||||||
23
08-vue3/04-lifecycle-and-template-ref/starter.html
Normal file
23
08-vue3/04-lifecycle-and-template-ref/starter.html
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>生命周期和模板 ref</title>
|
||||||
|
<style>
|
||||||
|
body { margin: 0; padding: 32px; font-family: "PingFang SC", sans-serif; background: #f4f8fb; }
|
||||||
|
.panel { max-width: 720px; margin: 0 auto; padding: 24px; border-radius: 18px; background: #fff; border: 1px solid #d8e4f3; }
|
||||||
|
input { width: 100%; padding: 12px; border-radius: 12px; border: 1px solid #cad7ea; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<section id="app" class="panel">
|
||||||
|
<h1>生命周期练习</h1>
|
||||||
|
<input ref="keywordInput" v-model="keyword" type="text" placeholder="页面挂载后请自动聚焦" />
|
||||||
|
<p>当前输入:{{ keyword }}</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script>
|
||||||
|
<script src="./starter.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
28
08-vue3/04-lifecycle-and-template-ref/starter.js
Normal file
28
08-vue3/04-lifecycle-and-template-ref/starter.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
const { createApp, ref, onMounted, onUpdated, onUnmounted } = Vue;
|
||||||
|
|
||||||
|
createApp({
|
||||||
|
setup() {
|
||||||
|
const keyword = ref("");
|
||||||
|
const keywordInput = ref(null);
|
||||||
|
let timer = null;
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 任务:
|
||||||
|
// 1. 让输入框自动聚焦
|
||||||
|
// 2. 建立一个定时器
|
||||||
|
});
|
||||||
|
|
||||||
|
onUpdated(() => {
|
||||||
|
// 任务:输出 updated 日志
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
// 任务:清理定时器并输出销毁日志
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
keyword,
|
||||||
|
keywordInput,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}).mount("#app");
|
||||||
22
08-vue3/05-props-and-emits/README.md
Normal file
22
08-vue3/05-props-and-emits/README.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# 练习 5:props 和 emit
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
学会在 Vue3 里写基础组件通信。
|
||||||
|
|
||||||
|
## 你要练什么
|
||||||
|
|
||||||
|
- `props`
|
||||||
|
- `emit`
|
||||||
|
- 组件拆分
|
||||||
|
|
||||||
|
## 任务
|
||||||
|
|
||||||
|
- 把课程项拆成子组件
|
||||||
|
- 父组件传入课程对象
|
||||||
|
- 子组件点击按钮后通知父组件切换完成状态
|
||||||
|
|
||||||
|
## 文件
|
||||||
|
|
||||||
|
- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/05-props-and-emits/starter.html)
|
||||||
|
- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/05-props-and-emits/starter.js)
|
||||||
28
08-vue3/05-props-and-emits/starter.html
Normal file
28
08-vue3/05-props-and-emits/starter.html
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>props 和 emit</title>
|
||||||
|
<style>
|
||||||
|
body { margin: 0; padding: 32px; font-family: "PingFang SC", sans-serif; background: #f4f7fb; }
|
||||||
|
.wrap { max-width: 820px; margin: 0 auto; display: grid; gap: 14px; }
|
||||||
|
.card { padding: 18px; border-radius: 16px; background: #fff; border: 1px solid #d9e3f2; }
|
||||||
|
.done { color: #1f8f54; }
|
||||||
|
button { padding: 10px 14px; border: 0; border-radius: 999px; background: #2d6cdf; color: #fff; cursor: pointer; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<section id="app" class="wrap">
|
||||||
|
<course-item
|
||||||
|
v-for="course in courses"
|
||||||
|
:key="course.id"
|
||||||
|
:course="course"
|
||||||
|
@toggle="toggleCourse"
|
||||||
|
></course-item>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script>
|
||||||
|
<script src="./starter.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
39
08-vue3/05-props-and-emits/starter.js
Normal file
39
08-vue3/05-props-and-emits/starter.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
const { createApp } = Vue;
|
||||||
|
|
||||||
|
createApp({
|
||||||
|
components: {
|
||||||
|
CourseItem: {
|
||||||
|
props: {
|
||||||
|
course: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: ["toggle"],
|
||||||
|
template: `
|
||||||
|
<article class="card">
|
||||||
|
<h2>{{ course.title }}</h2>
|
||||||
|
<p :class="{ done: course.finished }">
|
||||||
|
{{ course.finished ? "已完成" : "学习中" }}
|
||||||
|
</p>
|
||||||
|
<button type="button" @click="$emit('toggle', course.id)">切换状态</button>
|
||||||
|
</article>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
const courses = Vue.ref([
|
||||||
|
{ id: 1, title: "组合式 API", finished: true },
|
||||||
|
{ id: 2, title: "组件通信", finished: false },
|
||||||
|
]);
|
||||||
|
|
||||||
|
function toggleCourse(courseId) {
|
||||||
|
// 任务:根据 courseId 切换对应课程的 finished
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
courses,
|
||||||
|
toggleCourse,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}).mount("#app");
|
||||||
22
08-vue3/06-slots-and-provide-inject/README.md
Normal file
22
08-vue3/06-slots-and-provide-inject/README.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# 练习 6:slot 与 provide / inject
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
学会更灵活地组织组件树中的内容和共享信息。
|
||||||
|
|
||||||
|
## 你要练什么
|
||||||
|
|
||||||
|
- `slot`
|
||||||
|
- `provide`
|
||||||
|
- `inject`
|
||||||
|
|
||||||
|
## 任务
|
||||||
|
|
||||||
|
- 用插槽自定义卡片按钮文案
|
||||||
|
- 父组件通过 `provide` 提供主题色
|
||||||
|
- 子组件通过 `inject` 使用主题信息
|
||||||
|
|
||||||
|
## 文件
|
||||||
|
|
||||||
|
- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/06-slots-and-provide-inject/starter.html)
|
||||||
|
- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/06-slots-and-provide-inject/starter.js)
|
||||||
26
08-vue3/06-slots-and-provide-inject/starter.html
Normal file
26
08-vue3/06-slots-and-provide-inject/starter.html
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>slot 与 provide/inject</title>
|
||||||
|
<style>
|
||||||
|
body { margin: 0; padding: 32px; font-family: "PingFang SC", sans-serif; background: #f6f8fc; }
|
||||||
|
.wrap { max-width: 820px; margin: 0 auto; }
|
||||||
|
.card { padding: 20px; border-radius: 18px; background: #fff; border: 1px solid #dce5f2; }
|
||||||
|
button { padding: 10px 14px; border: 0; border-radius: 999px; color: #fff; cursor: pointer; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<section id="app" class="wrap">
|
||||||
|
<theme-card>
|
||||||
|
<template #default>
|
||||||
|
继续学习
|
||||||
|
</template>
|
||||||
|
</theme-card>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script>
|
||||||
|
<script src="./starter.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
27
08-vue3/06-slots-and-provide-inject/starter.js
Normal file
27
08-vue3/06-slots-and-provide-inject/starter.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
const { createApp, provide, inject } = Vue;
|
||||||
|
|
||||||
|
createApp({
|
||||||
|
components: {
|
||||||
|
ThemeCard: {
|
||||||
|
setup() {
|
||||||
|
const themeColor = inject("themeColor");
|
||||||
|
|
||||||
|
return {
|
||||||
|
themeColor,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<article class="card">
|
||||||
|
<h2>主题卡片</h2>
|
||||||
|
<button type="button" :style="{ background: themeColor }">
|
||||||
|
<slot>默认按钮</slot>
|
||||||
|
</button>
|
||||||
|
</article>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
// 任务:通过 provide 提供 themeColor
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
}).mount("#app");
|
||||||
26
08-vue3/07-composable-and-async/README.md
Normal file
26
08-vue3/07-composable-and-async/README.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# 练习 7:composable 和异步状态
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
学会把可复用逻辑抽成 composable,并管理 loading / error / data。
|
||||||
|
|
||||||
|
## 你要练什么
|
||||||
|
|
||||||
|
- composable
|
||||||
|
- `ref`
|
||||||
|
- 异步状态
|
||||||
|
- `loading`
|
||||||
|
- `error`
|
||||||
|
|
||||||
|
## 任务
|
||||||
|
|
||||||
|
- 把课程请求逻辑抽成 `useCourses`
|
||||||
|
- 页面加载时调用它
|
||||||
|
- 显示 loading
|
||||||
|
- 请求成功后渲染列表
|
||||||
|
- 请求失败时显示错误信息
|
||||||
|
|
||||||
|
## 文件
|
||||||
|
|
||||||
|
- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/07-composable-and-async/starter.html)
|
||||||
|
- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/07-composable-and-async/starter.js)
|
||||||
26
08-vue3/07-composable-and-async/starter.html
Normal file
26
08-vue3/07-composable-and-async/starter.html
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>composable 和异步状态</title>
|
||||||
|
<style>
|
||||||
|
body { margin: 0; padding: 32px; font-family: "PingFang SC", sans-serif; background: #f5f8fc; }
|
||||||
|
.panel { max-width: 760px; margin: 0 auto; padding: 24px; border-radius: 18px; background: #fff; border: 1px solid #dbe4f2; }
|
||||||
|
.error { color: #b42318; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<section id="app" class="panel">
|
||||||
|
<h1>课程请求练习</h1>
|
||||||
|
<p v-if="loading">数据加载中...</p>
|
||||||
|
<p v-if="error" class="error">{{ error }}</p>
|
||||||
|
<ul v-if="!loading && !error">
|
||||||
|
<li v-for="item in courses" :key="item.id">{{ item.title }}</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script>
|
||||||
|
<script src="./starter.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
38
08-vue3/07-composable-and-async/starter.js
Normal file
38
08-vue3/07-composable-and-async/starter.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
const { createApp, ref, onMounted } = Vue;
|
||||||
|
|
||||||
|
function useCourses() {
|
||||||
|
const courses = ref([]);
|
||||||
|
const loading = ref(true);
|
||||||
|
const error = ref("");
|
||||||
|
|
||||||
|
async function loadCourses() {
|
||||||
|
// 任务:
|
||||||
|
// 1. 模拟异步请求
|
||||||
|
// 2. 成功时给 courses 赋值
|
||||||
|
// 3. 失败时给 error 赋值
|
||||||
|
// 4. 最后把 loading 设为 false
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
courses,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
loadCourses,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
createApp({
|
||||||
|
setup() {
|
||||||
|
const { courses, loading, error, loadCourses } = useCourses();
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 任务:页面挂载后调用 loadCourses
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
courses,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}).mount("#app");
|
||||||
30
08-vue3/08-final-dashboard/README.md
Normal file
30
08-vue3/08-final-dashboard/README.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# 练习 8:Vue3 综合小面板
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
把 Vue3 组合式 API 的主线能力串起来,完成一个小型课程面板。
|
||||||
|
|
||||||
|
## 你要练什么
|
||||||
|
|
||||||
|
- `ref`
|
||||||
|
- `reactive`
|
||||||
|
- `computed`
|
||||||
|
- `watch`
|
||||||
|
- 组件通信
|
||||||
|
- composable
|
||||||
|
- 模板 `ref`
|
||||||
|
|
||||||
|
## 任务
|
||||||
|
|
||||||
|
- 做一个课程搜索和筛选面板
|
||||||
|
- 用 `computed` 计算筛选结果和统计数据
|
||||||
|
- 用 `watch` 输出搜索关键字变化
|
||||||
|
- 用子组件渲染课程卡片
|
||||||
|
- 点击按钮切换课程完成状态
|
||||||
|
- 点击按钮聚焦搜索框
|
||||||
|
- 抽一个 composable 管理课程数据
|
||||||
|
|
||||||
|
## 文件
|
||||||
|
|
||||||
|
- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/08-final-dashboard/starter.html)
|
||||||
|
- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/08-final-dashboard/starter.js)
|
||||||
56
08-vue3/08-final-dashboard/starter.html
Normal file
56
08-vue3/08-final-dashboard/starter.html
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Vue3 综合小面板</title>
|
||||||
|
<style>
|
||||||
|
body { margin: 0; padding: 32px; font-family: "PingFang SC", sans-serif; background: #f4f7fb; }
|
||||||
|
.page { max-width: 960px; margin: 0 auto; display: grid; gap: 18px; }
|
||||||
|
.panel { padding: 22px; border-radius: 20px; background: #fff; border: 1px solid #d9e4f1; }
|
||||||
|
.toolbar { display: grid; grid-template-columns: 1fr 180px 160px; gap: 12px; }
|
||||||
|
input, select, button { padding: 12px 14px; border-radius: 12px; border: 1px solid #cad6e8; font: inherit; }
|
||||||
|
button { border: 0; background: #2d6cdf; color: #fff; cursor: pointer; }
|
||||||
|
.list { display: grid; gap: 14px; }
|
||||||
|
.course-card { padding: 18px; border-radius: 16px; border: 1px solid #dbe4f1; background: #fbfcfe; }
|
||||||
|
.course-card.is-done { border-color: #6cc18a; background: #f3fbf6; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<section id="app" class="page">
|
||||||
|
<article class="panel">
|
||||||
|
<h1>Vue3 综合课程面板</h1>
|
||||||
|
<div class="toolbar">
|
||||||
|
<input ref="searchInput" v-model="keyword" type="text" placeholder="输入课程关键字" />
|
||||||
|
<select v-model="statusFilter">
|
||||||
|
<option value="all">全部</option>
|
||||||
|
<option value="done">已完成</option>
|
||||||
|
<option value="doing">学习中</option>
|
||||||
|
</select>
|
||||||
|
<button type="button" @click="focusSearch">聚焦搜索框</button>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<section class="panel">
|
||||||
|
<div v-if="!filteredCourses.length">暂无匹配结果</div>
|
||||||
|
<div v-show="filteredCourses.length" class="list">
|
||||||
|
<course-item
|
||||||
|
v-for="course in filteredCourses"
|
||||||
|
:key="course.id"
|
||||||
|
:course="course"
|
||||||
|
@toggle="toggleCourse"
|
||||||
|
></course-item>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer class="panel">
|
||||||
|
<p>总课程数:{{ totalCount }}</p>
|
||||||
|
<p>已完成数:{{ finishedCount }}</p>
|
||||||
|
<p>当前筛选数:{{ visibleCount }}</p>
|
||||||
|
</footer>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script>
|
||||||
|
<script src="./starter.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
85
08-vue3/08-final-dashboard/starter.js
Normal file
85
08-vue3/08-final-dashboard/starter.js
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
const { createApp, ref, computed, watch } = Vue;
|
||||||
|
|
||||||
|
function useCourses() {
|
||||||
|
const courses = ref([
|
||||||
|
{ id: 1, title: "setup 和 ref", finished: true },
|
||||||
|
{ id: 2, title: "reactive 和 computed", finished: false },
|
||||||
|
{ id: 3, title: "组件通信", finished: false },
|
||||||
|
{ id: 4, title: "composable 实战", finished: true },
|
||||||
|
]);
|
||||||
|
|
||||||
|
function toggleCourse(courseId) {
|
||||||
|
// 任务:根据 courseId 切换课程完成状态
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
courses,
|
||||||
|
toggleCourse,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
createApp({
|
||||||
|
components: {
|
||||||
|
CourseItem: {
|
||||||
|
props: {
|
||||||
|
course: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: ["toggle"],
|
||||||
|
template: `
|
||||||
|
<article class="course-card" :class="{ 'is-done': course.finished }">
|
||||||
|
<h2>{{ course.title }}</h2>
|
||||||
|
<p>{{ course.finished ? "已完成" : "学习中" }}</p>
|
||||||
|
<button type="button" @click="$emit('toggle', course.id)">切换状态</button>
|
||||||
|
</article>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
const keyword = ref("");
|
||||||
|
const statusFilter = ref("all");
|
||||||
|
const searchInput = ref(null);
|
||||||
|
const { courses, toggleCourse } = useCourses();
|
||||||
|
|
||||||
|
const filteredCourses = computed(() => {
|
||||||
|
// 任务:同时按关键字和状态筛选课程
|
||||||
|
return courses.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalCount = computed(() => {
|
||||||
|
return courses.value.length;
|
||||||
|
});
|
||||||
|
|
||||||
|
const finishedCount = computed(() => {
|
||||||
|
// 任务:返回已完成数量
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
const visibleCount = computed(() => {
|
||||||
|
// 任务:返回当前筛选后的数量
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(keyword, (newValue) => {
|
||||||
|
// 任务:输出关键字变化
|
||||||
|
});
|
||||||
|
|
||||||
|
function focusSearch() {
|
||||||
|
// 任务:通过模板 ref 聚焦输入框
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
keyword,
|
||||||
|
statusFilter,
|
||||||
|
searchInput,
|
||||||
|
filteredCourses,
|
||||||
|
totalCount,
|
||||||
|
finishedCount,
|
||||||
|
visibleCount,
|
||||||
|
toggleCourse,
|
||||||
|
focusSearch,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}).mount("#app");
|
||||||
24
08-vue3/09-reactivity-helpers/README.md
Normal file
24
08-vue3/09-reactivity-helpers/README.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# 练习 9:toRef、toRefs 和 readonly
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
学会把响应式对象里的字段拆出来继续保持响应式,并理解只读数据的使用场景。
|
||||||
|
|
||||||
|
## 你要练什么
|
||||||
|
|
||||||
|
- `toRef`
|
||||||
|
- `toRefs`
|
||||||
|
- `readonly`
|
||||||
|
|
||||||
|
## 任务
|
||||||
|
|
||||||
|
- 用 `reactive` 管理学习者信息
|
||||||
|
- 用 `toRef` 单独取出 `name`
|
||||||
|
- 用 `toRefs` 拆出其余字段
|
||||||
|
- 用 `readonly` 包一层设置项
|
||||||
|
- 点击按钮更新学习者信息并观察页面变化
|
||||||
|
|
||||||
|
## 文件
|
||||||
|
|
||||||
|
- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/09-reactivity-helpers/starter.html)
|
||||||
|
- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/09-reactivity-helpers/starter.js)
|
||||||
26
08-vue3/09-reactivity-helpers/starter.html
Normal file
26
08-vue3/09-reactivity-helpers/starter.html
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>toRef、toRefs 和 readonly</title>
|
||||||
|
<style>
|
||||||
|
body { margin: 0; padding: 32px; font-family: "PingFang SC", sans-serif; background: #f5f8fc; }
|
||||||
|
.panel { max-width: 760px; margin: 0 auto; padding: 24px; border-radius: 18px; background: #fff; border: 1px solid #dbe4f2; }
|
||||||
|
button { padding: 10px 16px; border: 0; border-radius: 999px; background: #2d6cdf; color: #fff; cursor: pointer; }
|
||||||
|
code { background: #f2f6fb; padding: 2px 6px; border-radius: 8px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<section id="app" class="panel">
|
||||||
|
<h1>{{ name }}</h1>
|
||||||
|
<p>当前阶段:{{ stage }}</p>
|
||||||
|
<p>学习天数:{{ studyDays }}</p>
|
||||||
|
<p>主题模式:<code>{{ settings.theme }}</code></p>
|
||||||
|
<button type="button" @click="updateProfile">更新资料</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script>
|
||||||
|
<script src="./starter.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
33
08-vue3/09-reactivity-helpers/starter.js
Normal file
33
08-vue3/09-reactivity-helpers/starter.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
const { createApp, reactive, toRef, toRefs, readonly } = Vue;
|
||||||
|
|
||||||
|
createApp({
|
||||||
|
setup() {
|
||||||
|
const profile = reactive({
|
||||||
|
name: "林晨",
|
||||||
|
stage: "Vue3 入门",
|
||||||
|
studyDays: 12,
|
||||||
|
});
|
||||||
|
|
||||||
|
const name = toRef(profile, "name");
|
||||||
|
const { stage, studyDays } = toRefs(profile);
|
||||||
|
const settings = readonly({
|
||||||
|
theme: "light",
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateProfile() {
|
||||||
|
// 任务:
|
||||||
|
// 1. 更新 name.value
|
||||||
|
// 2. 更新 stage.value
|
||||||
|
// 3. 让 studyDays.value + 1
|
||||||
|
// 4. 不要直接修改 settings.theme
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
stage,
|
||||||
|
studyDays,
|
||||||
|
settings,
|
||||||
|
updateProfile,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}).mount("#app");
|
||||||
24
08-vue3/10-next-tick-and-component-v-model/README.md
Normal file
24
08-vue3/10-next-tick-and-component-v-model/README.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# 练习 10:nextTick 和组件 v-model
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
学会在 DOM 更新完成后执行逻辑,并理解 Vue3 组件 `v-model` 的通信约定。
|
||||||
|
|
||||||
|
## 你要练什么
|
||||||
|
|
||||||
|
- `nextTick`
|
||||||
|
- 组件 `v-model`
|
||||||
|
- `modelValue`
|
||||||
|
- `update:modelValue`
|
||||||
|
|
||||||
|
## 任务
|
||||||
|
|
||||||
|
- 封装一个搜索输入子组件
|
||||||
|
- 父组件通过 `v-model` 绑定关键字
|
||||||
|
- 点击“展开搜索区”后,等 DOM 更新完成再聚焦输入框
|
||||||
|
- 在控制台输出关键字变化
|
||||||
|
|
||||||
|
## 文件
|
||||||
|
|
||||||
|
- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/10-next-tick-and-component-v-model/starter.html)
|
||||||
|
- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/10-next-tick-and-component-v-model/starter.js)
|
||||||
29
08-vue3/10-next-tick-and-component-v-model/starter.html
Normal file
29
08-vue3/10-next-tick-and-component-v-model/starter.html
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>nextTick 和组件 v-model</title>
|
||||||
|
<style>
|
||||||
|
body { margin: 0; padding: 32px; font-family: "PingFang SC", sans-serif; background: #f4f7fb; }
|
||||||
|
.panel { max-width: 760px; margin: 0 auto; padding: 24px; border-radius: 18px; background: #fff; border: 1px solid #d9e4f1; }
|
||||||
|
input, button { padding: 12px 14px; border-radius: 12px; font: inherit; }
|
||||||
|
input { width: 100%; border: 1px solid #cad6e8; }
|
||||||
|
button { border: 0; background: #2d6cdf; color: #fff; cursor: pointer; margin-bottom: 16px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<section id="app" class="panel">
|
||||||
|
<button type="button" @click="toggleSearch">展开搜索区</button>
|
||||||
|
|
||||||
|
<div v-if="showSearch">
|
||||||
|
<search-input ref="searchBox" v-model="keyword"></search-input>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>当前关键字:{{ keyword }}</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script>
|
||||||
|
<script src="./starter.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
52
08-vue3/10-next-tick-and-component-v-model/starter.js
Normal file
52
08-vue3/10-next-tick-and-component-v-model/starter.js
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
const { createApp, ref, nextTick, watch } = Vue;
|
||||||
|
|
||||||
|
createApp({
|
||||||
|
components: {
|
||||||
|
SearchInput: {
|
||||||
|
props: {
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: ["update:modelValue"],
|
||||||
|
template: `
|
||||||
|
<input
|
||||||
|
ref="inputEl"
|
||||||
|
:value="modelValue"
|
||||||
|
type="text"
|
||||||
|
placeholder="请输入课程关键字"
|
||||||
|
@input="$emit('update:modelValue', $event.target.value)"
|
||||||
|
/>
|
||||||
|
`,
|
||||||
|
methods: {
|
||||||
|
focus() {
|
||||||
|
this.$refs.inputEl.focus();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
const showSearch = ref(false);
|
||||||
|
const keyword = ref("");
|
||||||
|
const searchBox = ref(null);
|
||||||
|
|
||||||
|
watch(keyword, (newValue) => {
|
||||||
|
// 任务:在控制台输出关键字变化
|
||||||
|
});
|
||||||
|
|
||||||
|
async function toggleSearch() {
|
||||||
|
// 任务:
|
||||||
|
// 1. 切换 showSearch.value
|
||||||
|
// 2. 如果展开了,await nextTick()
|
||||||
|
// 3. 通过 searchBox.value.focus() 聚焦输入框
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
showSearch,
|
||||||
|
keyword,
|
||||||
|
searchBox,
|
||||||
|
toggleSearch,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}).mount("#app");
|
||||||
24
08-vue3/11-before-hooks-and-expose/README.md
Normal file
24
08-vue3/11-before-hooks-and-expose/README.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# 练习 11:before 系列生命周期和 expose
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
学会在组合式 API 中使用 before 系列生命周期,并理解子组件如何有选择地暴露能力给父组件。
|
||||||
|
|
||||||
|
## 你要练什么
|
||||||
|
|
||||||
|
- `onBeforeMount`
|
||||||
|
- `onBeforeUpdate`
|
||||||
|
- `onBeforeUnmount`
|
||||||
|
- `expose`
|
||||||
|
|
||||||
|
## 任务
|
||||||
|
|
||||||
|
- 在不同生命周期里输出日志
|
||||||
|
- 父组件通过模板 `ref` 获取子组件实例
|
||||||
|
- 子组件通过 `expose` 暴露一个 `focusInput` 方法
|
||||||
|
- 父组件点击按钮后调用这个暴露出来的方法
|
||||||
|
|
||||||
|
## 文件
|
||||||
|
|
||||||
|
- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/11-before-hooks-and-expose/starter.html)
|
||||||
|
- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/11-before-hooks-and-expose/starter.js)
|
||||||
25
08-vue3/11-before-hooks-and-expose/starter.html
Normal file
25
08-vue3/11-before-hooks-and-expose/starter.html
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>before 系列生命周期和 expose</title>
|
||||||
|
<style>
|
||||||
|
body { margin: 0; padding: 32px; font-family: "PingFang SC", sans-serif; background: #f6f8fc; }
|
||||||
|
.panel { max-width: 760px; margin: 0 auto; padding: 24px; border-radius: 18px; background: #fff; border: 1px solid #dde5f2; }
|
||||||
|
button, input { padding: 12px 14px; border-radius: 12px; font: inherit; }
|
||||||
|
input { width: 100%; border: 1px solid #ccd7e9; margin-top: 12px; }
|
||||||
|
button { border: 0; background: #2d6cdf; color: #fff; cursor: pointer; margin-right: 10px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<section id="app" class="panel">
|
||||||
|
<button type="button" @click="focusChildInput">聚焦子组件输入框</button>
|
||||||
|
<button type="button" @click="showChild = !showChild">切换子组件显示</button>
|
||||||
|
<child-panel v-if="showChild" ref="childPanel"></child-panel>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script>
|
||||||
|
<script src="./starter.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
59
08-vue3/11-before-hooks-and-expose/starter.js
Normal file
59
08-vue3/11-before-hooks-and-expose/starter.js
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
const {
|
||||||
|
createApp,
|
||||||
|
ref,
|
||||||
|
onBeforeMount,
|
||||||
|
onBeforeUpdate,
|
||||||
|
onBeforeUnmount,
|
||||||
|
} = Vue;
|
||||||
|
|
||||||
|
createApp({
|
||||||
|
components: {
|
||||||
|
ChildPanel: {
|
||||||
|
template: `
|
||||||
|
<div>
|
||||||
|
<p>我是子组件</p>
|
||||||
|
<input ref="inputEl" type="text" placeholder="等待父组件调用 focus" />
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
setup(props, { expose }) {
|
||||||
|
const inputEl = ref(null);
|
||||||
|
|
||||||
|
onBeforeMount(() => {
|
||||||
|
// 任务:输出 beforeMount 日志
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUpdate(() => {
|
||||||
|
// 任务:输出 beforeUpdate 日志
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
// 任务:输出 beforeUnmount 日志
|
||||||
|
});
|
||||||
|
|
||||||
|
function focusInput() {
|
||||||
|
// 任务:聚焦 inputEl
|
||||||
|
}
|
||||||
|
|
||||||
|
// 任务:通过 expose 暴露 focusInput
|
||||||
|
|
||||||
|
return {
|
||||||
|
inputEl,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
const showChild = ref(true);
|
||||||
|
const childPanel = ref(null);
|
||||||
|
|
||||||
|
function focusChildInput() {
|
||||||
|
// 任务:调用 childPanel.value 暴露出来的方法
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
showChild,
|
||||||
|
childPanel,
|
||||||
|
focusChildInput,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}).mount("#app");
|
||||||
22
08-vue3/12-built-in-components/README.md
Normal file
22
08-vue3/12-built-in-components/README.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# 练习 12:Teleport、Suspense 和 Transition
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
认识 Vue3 常见内置组件在实际页面里的使用方式。
|
||||||
|
|
||||||
|
## 你要练什么
|
||||||
|
|
||||||
|
- `Teleport`
|
||||||
|
- `Suspense`
|
||||||
|
- `Transition`
|
||||||
|
|
||||||
|
## 任务
|
||||||
|
|
||||||
|
- 用 `Teleport` 把弹层渲染到 `body`
|
||||||
|
- 用 `Transition` 给弹层或提示做显隐动画
|
||||||
|
- 用 `Suspense` 包裹一个异步组件,并显示 fallback
|
||||||
|
|
||||||
|
## 文件
|
||||||
|
|
||||||
|
- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/12-built-in-components/starter.html)
|
||||||
|
- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/12-built-in-components/starter.js)
|
||||||
47
08-vue3/12-built-in-components/starter.html
Normal file
47
08-vue3/12-built-in-components/starter.html
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Teleport、Suspense 和 Transition</title>
|
||||||
|
<style>
|
||||||
|
body { margin: 0; padding: 32px; font-family: "PingFang SC", sans-serif; background: #f4f7fb; }
|
||||||
|
.panel { max-width: 760px; margin: 0 auto; padding: 24px; border-radius: 18px; background: #fff; border: 1px solid #d9e4f1; }
|
||||||
|
.modal { position: fixed; inset: 0; display: grid; place-items: center; background: rgba(12, 17, 29, 0.45); }
|
||||||
|
.modal-card { width: min(420px, calc(100vw - 32px)); padding: 24px; border-radius: 18px; background: #fff; }
|
||||||
|
.fade-enter-active, .fade-leave-active { transition: opacity .24s ease; }
|
||||||
|
.fade-enter-from, .fade-leave-to { opacity: 0; }
|
||||||
|
button { padding: 10px 14px; border-radius: 12px; border: 0; background: #2d6cdf; color: #fff; cursor: pointer; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<section id="app" class="panel">
|
||||||
|
<h1>内置组件练习</h1>
|
||||||
|
<button type="button" @click="showModal = !showModal">切换弹层</button>
|
||||||
|
|
||||||
|
<Suspense>
|
||||||
|
<template #default>
|
||||||
|
<async-info></async-info>
|
||||||
|
</template>
|
||||||
|
<template #fallback>
|
||||||
|
<p>异步组件加载中...</p>
|
||||||
|
</template>
|
||||||
|
</Suspense>
|
||||||
|
|
||||||
|
<Teleport to="body">
|
||||||
|
<Transition name="fade">
|
||||||
|
<div v-if="showModal" class="modal">
|
||||||
|
<div class="modal-card">
|
||||||
|
<h2>练习弹层</h2>
|
||||||
|
<p>这里应该通过 Teleport 渲染到 body。</p>
|
||||||
|
<button type="button" @click="showModal = false">关闭</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</Teleport>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script>
|
||||||
|
<script src="./starter.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
24
08-vue3/12-built-in-components/starter.js
Normal file
24
08-vue3/12-built-in-components/starter.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
const { createApp, ref } = Vue;
|
||||||
|
|
||||||
|
createApp({
|
||||||
|
components: {
|
||||||
|
AsyncInfo: {
|
||||||
|
async setup() {
|
||||||
|
// 任务:
|
||||||
|
// 1. 模拟等待
|
||||||
|
// 2. 返回需要在模板中展示的数据
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<p>这里会展示异步组件内容。</p>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
const showModal = ref(false);
|
||||||
|
|
||||||
|
return {
|
||||||
|
showModal,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}).mount("#app");
|
||||||
26
08-vue3/13-script-setup-macros/README.md
Normal file
26
08-vue3/13-script-setup-macros/README.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# 练习 13:script setup、defineProps、defineEmits、defineExpose
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
补上 Vue3 在工程化单文件组件里的核心宏语法。
|
||||||
|
|
||||||
|
## 你要练什么
|
||||||
|
|
||||||
|
- `<script setup>`
|
||||||
|
- `defineProps`
|
||||||
|
- `defineEmits`
|
||||||
|
- `defineExpose`
|
||||||
|
|
||||||
|
## 说明
|
||||||
|
|
||||||
|
这一题不是浏览器 CDN 练习,而是单文件组件语法练习,需要放在 `Vite + Vue3` 之类的工程里使用。
|
||||||
|
|
||||||
|
## 任务
|
||||||
|
|
||||||
|
- 给子组件定义 `title` 和 `finished` 两个 props
|
||||||
|
- 定义 `toggle` 事件并在按钮点击时触发
|
||||||
|
- 暴露一个 `focusAction` 方法给父组件调用
|
||||||
|
|
||||||
|
## 文件
|
||||||
|
|
||||||
|
- [starter.vue](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/13-script-setup-macros/starter.vue)
|
||||||
38
08-vue3/13-script-setup-macros/starter.vue
Normal file
38
08-vue3/13-script-setup-macros/starter.vue
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<template>
|
||||||
|
<article class="course-card">
|
||||||
|
<h2>{{ title }}</h2>
|
||||||
|
<p>{{ finished ? "已完成" : "学习中" }}</p>
|
||||||
|
<button ref="actionButton" type="button" @click="handleToggle">
|
||||||
|
切换状态
|
||||||
|
</button>
|
||||||
|
</article>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from "vue";
|
||||||
|
|
||||||
|
// 任务:
|
||||||
|
// 1. 用 defineProps 定义 title 和 finished
|
||||||
|
// 2. 用 defineEmits 定义 toggle
|
||||||
|
// 3. 点击按钮时触发 toggle
|
||||||
|
// 4. 用 defineExpose 暴露 focusAction
|
||||||
|
|
||||||
|
const actionButton = ref(null);
|
||||||
|
|
||||||
|
function handleToggle() {
|
||||||
|
// 在这里触发 emit
|
||||||
|
}
|
||||||
|
|
||||||
|
function focusAction() {
|
||||||
|
actionButton.value?.focus();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.course-card {
|
||||||
|
padding: 18px;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid #dbe4f1;
|
||||||
|
background: #fbfcfe;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
141
08-vue3/README.md
Normal file
141
08-vue3/README.md
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
# Vue3(组合式 API + 响应式原理)
|
||||||
|
|
||||||
|
你的学习文档里这一章的定位是 `Vue3(组合式API + 响应式原理)`。这一章我按这个方向来拆,不再重复 Vue2 的 Options API 主线,而是重点转到 Vue3 的组合式写法和响应式思维。
|
||||||
|
|
||||||
|
## 学完后你应该掌握
|
||||||
|
|
||||||
|
- Vue3 和 Vue2 的核心差异
|
||||||
|
- `createApp`
|
||||||
|
- `setup()`
|
||||||
|
- `ref`
|
||||||
|
- `reactive`
|
||||||
|
- `toRef`
|
||||||
|
- `toRefs`
|
||||||
|
- `readonly`
|
||||||
|
- `computed`
|
||||||
|
- `watch`
|
||||||
|
- `watchEffect`
|
||||||
|
- `nextTick`
|
||||||
|
- 组合式 API 生命周期
|
||||||
|
- 模板 `ref`
|
||||||
|
- `props` 和 `emit`
|
||||||
|
- 组件 `v-model`
|
||||||
|
- `slot`
|
||||||
|
- `provide` / `inject`
|
||||||
|
- `expose`
|
||||||
|
- composable 的基本抽离方式
|
||||||
|
- `script setup`
|
||||||
|
- `defineProps`
|
||||||
|
- `defineEmits`
|
||||||
|
- `defineExpose`
|
||||||
|
- `Teleport`
|
||||||
|
- `Suspense`
|
||||||
|
- `Transition`
|
||||||
|
- 如何用 Vue3 写一个小型管理面板
|
||||||
|
|
||||||
|
## 这一章在解决什么
|
||||||
|
|
||||||
|
Vue2 更强调“选项式组织”。
|
||||||
|
|
||||||
|
Vue3 这一章要解决的是:
|
||||||
|
|
||||||
|
- 逻辑如何按功能组织,而不是按选项分散
|
||||||
|
- 响应式数据如何在 `setup()` 里组合
|
||||||
|
- 一段可复用逻辑如何抽成 composable
|
||||||
|
- 组件之间如何在组合式 API 下继续通信
|
||||||
|
|
||||||
|
## 全部知识点清单
|
||||||
|
|
||||||
|
### 基础入口
|
||||||
|
|
||||||
|
- `createApp`
|
||||||
|
- `setup()`
|
||||||
|
- `return`
|
||||||
|
|
||||||
|
### 响应式核心
|
||||||
|
|
||||||
|
- `ref`
|
||||||
|
- `reactive`
|
||||||
|
- `toRef`
|
||||||
|
- `toRefs`
|
||||||
|
- `readonly`
|
||||||
|
- `computed`
|
||||||
|
- `watch`
|
||||||
|
- `watchEffect`
|
||||||
|
- `nextTick`
|
||||||
|
|
||||||
|
### 生命周期与 DOM
|
||||||
|
|
||||||
|
- `onBeforeMount`
|
||||||
|
- `onMounted`
|
||||||
|
- `onBeforeUpdate`
|
||||||
|
- `onUpdated`
|
||||||
|
- `onBeforeUnmount`
|
||||||
|
- `onUnmounted`
|
||||||
|
- 模板 `ref`
|
||||||
|
- `expose`
|
||||||
|
|
||||||
|
### 组件通信
|
||||||
|
|
||||||
|
- `props`
|
||||||
|
- `emit`
|
||||||
|
- 组件 `v-model`
|
||||||
|
- `slot`
|
||||||
|
- `provide`
|
||||||
|
- `inject`
|
||||||
|
|
||||||
|
### 逻辑复用
|
||||||
|
|
||||||
|
- composable
|
||||||
|
- 异步状态管理
|
||||||
|
- `loading`
|
||||||
|
- `error`
|
||||||
|
|
||||||
|
### 工程化语法与内置组件
|
||||||
|
|
||||||
|
- `script setup`
|
||||||
|
- `defineProps`
|
||||||
|
- `defineEmits`
|
||||||
|
- `defineExpose`
|
||||||
|
- `Teleport`
|
||||||
|
- `Suspense`
|
||||||
|
- `Transition`
|
||||||
|
|
||||||
|
## 学习顺序
|
||||||
|
|
||||||
|
1. `createApp`、`setup()` 和 `ref`
|
||||||
|
2. `reactive` 和 `computed`
|
||||||
|
3. `watch` 和 `watchEffect`
|
||||||
|
4. 生命周期和模板 `ref`
|
||||||
|
5. `props` 和 `emit`
|
||||||
|
6. `slot` 与 `provide` / `inject`
|
||||||
|
7. composable 与异步状态
|
||||||
|
8. `toRefs`、`readonly` 等响应式辅助工具
|
||||||
|
9. `nextTick` 和组件 `v-model`
|
||||||
|
10. before 系列生命周期与 `expose`
|
||||||
|
11. `Teleport`、`Suspense`、`Transition`
|
||||||
|
12. `script setup` 宏
|
||||||
|
13. 综合小页面
|
||||||
|
|
||||||
|
## 练习目录
|
||||||
|
|
||||||
|
- [01-create-app-and-ref/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/01-create-app-and-ref/README.md)
|
||||||
|
- [02-reactive-and-computed/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/02-reactive-and-computed/README.md)
|
||||||
|
- [03-watch-and-watch-effect/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/03-watch-and-watch-effect/README.md)
|
||||||
|
- [04-lifecycle-and-template-ref/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/04-lifecycle-and-template-ref/README.md)
|
||||||
|
- [05-props-and-emits/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/05-props-and-emits/README.md)
|
||||||
|
- [06-slots-and-provide-inject/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/06-slots-and-provide-inject/README.md)
|
||||||
|
- [07-composable-and-async/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/07-composable-and-async/README.md)
|
||||||
|
- [08-final-dashboard/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/08-final-dashboard/README.md)
|
||||||
|
- [09-reactivity-helpers/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/09-reactivity-helpers/README.md)
|
||||||
|
- [10-next-tick-and-component-v-model/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/10-next-tick-and-component-v-model/README.md)
|
||||||
|
- [11-before-hooks-and-expose/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/11-before-hooks-and-expose/README.md)
|
||||||
|
- [12-built-in-components/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/12-built-in-components/README.md)
|
||||||
|
- [13-script-setup-macros/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/13-script-setup-macros/README.md)
|
||||||
|
|
||||||
|
## 说明
|
||||||
|
|
||||||
|
- 这一章只提供 `starter`,不提供 `answer`
|
||||||
|
- 为了降低门槛,练习使用浏览器 CDN 版本的 Vue3
|
||||||
|
- `13-script-setup-macros` 是 SFC 语法练习,文件是 `.vue` starter,不是直接双击运行的 HTML
|
||||||
|
- 如果后面你要把这一章升级成 `Vite + Vue3`,我可以再继续补工程化版本
|
||||||
217
09-interview-plan/README.md
Normal file
217
09-interview-plan/README.md
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
# 前端面试官 Prompt
|
||||||
|
|
||||||
|
你现在扮演一位有经验的前端面试官,负责基于这个学习仓库的内容,对候选人进行系统化面试训练。
|
||||||
|
|
||||||
|
## 你的任务
|
||||||
|
|
||||||
|
围绕下面 8 个知识单元出题、追问、点评,并帮助候选人把“会写”升级成“会讲”:
|
||||||
|
|
||||||
|
- HTML
|
||||||
|
- CSS
|
||||||
|
- JavaScript Core
|
||||||
|
- DOM + 事件 + 异步
|
||||||
|
- ES6+
|
||||||
|
- TypeScript
|
||||||
|
- Vue2
|
||||||
|
- Vue3
|
||||||
|
|
||||||
|
## 你的工作方式
|
||||||
|
|
||||||
|
### 1. 按阶段出题
|
||||||
|
|
||||||
|
请按下面 4 个阶段组织面试题:
|
||||||
|
|
||||||
|
#### 第一阶段:结构与样式
|
||||||
|
|
||||||
|
- HTML
|
||||||
|
- CSS
|
||||||
|
|
||||||
|
目标:
|
||||||
|
|
||||||
|
- 检查候选人是否能把页面拆成结构
|
||||||
|
- 检查候选人是否能解释常见布局方案
|
||||||
|
|
||||||
|
#### 第二阶段:JavaScript 主线
|
||||||
|
|
||||||
|
- JavaScript Core
|
||||||
|
- DOM + 事件 + 异步
|
||||||
|
- ES6+
|
||||||
|
|
||||||
|
目标:
|
||||||
|
|
||||||
|
- 检查候选人是否能说清基础语法、作用域、闭包、`this`
|
||||||
|
- 检查候选人是否能说清事件流、异步、模块化
|
||||||
|
|
||||||
|
#### 第三阶段:类型与 Vue2
|
||||||
|
|
||||||
|
- TypeScript
|
||||||
|
- Vue2
|
||||||
|
|
||||||
|
目标:
|
||||||
|
|
||||||
|
- 检查候选人是否能解释类型系统的价值
|
||||||
|
- 检查候选人是否能说清 Vue2 的数据驱动和组件通信
|
||||||
|
|
||||||
|
#### 第四阶段:Vue3 与综合表达
|
||||||
|
|
||||||
|
- Vue3
|
||||||
|
|
||||||
|
目标:
|
||||||
|
|
||||||
|
- 检查候选人是否能说清组合式 API、响应式、composable 和工程化语法
|
||||||
|
|
||||||
|
### 2. 每次只出一个单元
|
||||||
|
|
||||||
|
每次面试时:
|
||||||
|
|
||||||
|
1. 先让我选择一个单元
|
||||||
|
2. 再连续提出 5 到 8 道问题
|
||||||
|
3. 每题都允许我回答
|
||||||
|
4. 你根据我的回答继续追问
|
||||||
|
|
||||||
|
### 3. 每道题都按这个顺序处理
|
||||||
|
|
||||||
|
对每道题,请按下面流程进行:
|
||||||
|
|
||||||
|
1. 先提出问题
|
||||||
|
2. 等我回答
|
||||||
|
3. 判断回答是否完整
|
||||||
|
4. 如果不完整,继续追问
|
||||||
|
5. 最后给出点评
|
||||||
|
|
||||||
|
## 你的提问标准
|
||||||
|
|
||||||
|
每道题尽量围绕这 4 层来设计:
|
||||||
|
|
||||||
|
1. 定义是什么
|
||||||
|
2. 使用场景是什么
|
||||||
|
3. 常见坑点是什么
|
||||||
|
4. 能不能举一个小例子
|
||||||
|
|
||||||
|
## 题目池
|
||||||
|
|
||||||
|
### HTML
|
||||||
|
|
||||||
|
- 什么是语义化标签?为什么不要只用 `div`?
|
||||||
|
- 一个完整 HTML 文档的基本骨架包含哪些部分?
|
||||||
|
- 块级元素和行内元素的区别是什么?
|
||||||
|
- `section`、`article`、`aside` 分别适合什么场景?
|
||||||
|
- `ul` / `ol` / `li` 的嵌套规则是什么?
|
||||||
|
- `form`、`label`、`input` 的正确关系是什么?
|
||||||
|
- `alt`、`href`、`src`、`name` 分别有什么作用?
|
||||||
|
- 为什么说 HTML 更像页面骨架而不是样式代码?
|
||||||
|
|
||||||
|
### CSS
|
||||||
|
|
||||||
|
- 盒模型由哪些部分组成?
|
||||||
|
- `margin` 和 `padding` 的区别是什么?
|
||||||
|
- `display: block / inline / inline-block / flex / grid` 的常见差异是什么?
|
||||||
|
- Flex 最常用的几个属性分别解决什么问题?
|
||||||
|
- Grid 适合什么场景?和 Flex 的区别是什么?
|
||||||
|
- `position: relative / absolute / fixed / sticky` 分别怎么理解?
|
||||||
|
- 什么是文档流?脱离文档流会带来什么影响?
|
||||||
|
- 如何做水平垂直居中?
|
||||||
|
|
||||||
|
### JavaScript Core
|
||||||
|
|
||||||
|
- `var`、`let`、`const` 的区别是什么?
|
||||||
|
- JavaScript 常见数据类型有哪些?
|
||||||
|
- `undefined` 和 `null` 的区别是什么?
|
||||||
|
- `if / else` 和 `switch` 各适合什么场景?
|
||||||
|
- `for` 和 `while` 的区别是什么?
|
||||||
|
- 什么是函数?参数和返回值怎么理解?
|
||||||
|
- 数组和对象分别适合存什么数据?
|
||||||
|
- 什么是作用域?什么是闭包?
|
||||||
|
- `this` 在普通函数、对象方法、箭头函数里的区别是什么?
|
||||||
|
- 值传递和引用传递怎么理解?
|
||||||
|
|
||||||
|
### DOM + 事件 + 异步
|
||||||
|
|
||||||
|
- 如何选中页面元素?
|
||||||
|
- `textContent`、`innerHTML`、`classList`、`style` 有什么常见用途?
|
||||||
|
- 如何创建、插入、删除节点?
|
||||||
|
- `addEventListener` 的作用是什么?
|
||||||
|
- 事件冒泡是什么?事件委托为什么有用?
|
||||||
|
- `preventDefault()` 和 `stopPropagation()` 分别解决什么问题?
|
||||||
|
- `setTimeout` 为什么体现异步?
|
||||||
|
- Promise 和 `async/await` 在页面交互里怎么配合?
|
||||||
|
|
||||||
|
### ES6+
|
||||||
|
|
||||||
|
- 模板字符串和字符串拼接相比有什么优势?
|
||||||
|
- 解构赋值的典型使用场景是什么?
|
||||||
|
- 展开运算符和剩余参数分别做什么?
|
||||||
|
- 箭头函数和普通函数的差异有哪些?
|
||||||
|
- 为什么箭头函数里的 `this` 容易被问到?
|
||||||
|
- `import / export` 的基本写法是什么?
|
||||||
|
- `fetch()` 和 `res.json()` 的关系是什么?
|
||||||
|
- Promise 和 `async/await` 的关系是什么?
|
||||||
|
|
||||||
|
### TypeScript
|
||||||
|
|
||||||
|
- TypeScript 和 JavaScript 的核心差异是什么?
|
||||||
|
- 为什么说 TypeScript 的价值主要发生在运行前?
|
||||||
|
- 基本类型、数组类型、函数类型怎么写?
|
||||||
|
- `interface` 适合解决什么问题?
|
||||||
|
- 泛型 `<T>` 的核心价值是什么?
|
||||||
|
- 联合类型和可选属性的常见场景是什么?
|
||||||
|
- TypeScript 报错时应该先看什么?
|
||||||
|
- 为什么说类型要服务于业务?
|
||||||
|
|
||||||
|
### Vue2
|
||||||
|
|
||||||
|
- Vue2 为什么说是数据驱动视图?
|
||||||
|
- `new Vue()` 里最常见的几个选项是什么?
|
||||||
|
- `v-bind`、`v-on`、`v-model` 分别做什么?
|
||||||
|
- `v-if` 和 `v-show` 的区别是什么?
|
||||||
|
- `computed` 和 `watch` 的区别是什么?
|
||||||
|
- Vue2 的生命周期最常问哪几个?
|
||||||
|
- 父子组件怎么通过 `props` 和 `$emit` 通信?
|
||||||
|
- `slot` 和 `ref` 分别适合什么场景?
|
||||||
|
|
||||||
|
### Vue3
|
||||||
|
|
||||||
|
- Vue3 和 Vue2 最大的思维差异是什么?
|
||||||
|
- `setup()` 为什么是组合式 API 的入口?
|
||||||
|
- `ref` 和 `reactive` 的区别是什么?
|
||||||
|
- `watch` 和 `watchEffect` 的区别是什么?
|
||||||
|
- `toRef`、`toRefs`、`readonly` 解决什么问题?
|
||||||
|
- `nextTick` 适合什么场景?
|
||||||
|
- Vue3 组件 `v-model` 的底层约定是什么?
|
||||||
|
- `provide / inject` 适合什么场景?
|
||||||
|
- 什么是 composable?
|
||||||
|
- `script setup`、`defineProps`、`defineEmits` 有什么作用?
|
||||||
|
- `Teleport`、`Suspense`、`Transition` 分别用来解决什么问题?
|
||||||
|
|
||||||
|
## 回答评估标准
|
||||||
|
|
||||||
|
当我回答后,请从这 4 个维度给出判断:
|
||||||
|
|
||||||
|
- 是否说对定义
|
||||||
|
- 是否说出使用场景
|
||||||
|
- 是否提到常见坑点
|
||||||
|
- 是否给出例子或代码思路
|
||||||
|
|
||||||
|
如果回答不完整,请继续追问,不要立刻公布标准答案。
|
||||||
|
|
||||||
|
## 点评格式
|
||||||
|
|
||||||
|
每道题点评时请使用这个格式:
|
||||||
|
|
||||||
|
```md
|
||||||
|
问题:...
|
||||||
|
评价:回答完整 / 基本正确 / 不完整 / 有明显错误
|
||||||
|
缺失点:...
|
||||||
|
标准表达:...
|
||||||
|
追问题:...
|
||||||
|
```
|
||||||
|
|
||||||
|
## 启动方式
|
||||||
|
|
||||||
|
当我说“开始面试”时,请先问我:
|
||||||
|
|
||||||
|
1. 你想刷哪个单元?
|
||||||
|
2. 你想要偏基础、偏中等,还是偏追问型?
|
||||||
|
3. 你想一次刷 5 题还是 8 题?
|
||||||
|
|
||||||
|
然后直接开始扮演面试官,不要再解释规则。
|
||||||
28
README.md
28
README.md
@@ -11,6 +11,8 @@
|
|||||||
- `05-es6-plus`:预留给 ES6+(现代 JS)
|
- `05-es6-plus`:预留给 ES6+(现代 JS)
|
||||||
- `06-typescript`:预留给 TypeScript
|
- `06-typescript`:预留给 TypeScript
|
||||||
- `07-vue2`:预留给 Vue2
|
- `07-vue2`:预留给 Vue2
|
||||||
|
- `08-vue3`:预留给 Vue3
|
||||||
|
- `09-interview-plan`:前端面试题计划
|
||||||
|
|
||||||
## 当前可学内容
|
## 当前可学内容
|
||||||
|
|
||||||
@@ -64,7 +66,21 @@
|
|||||||
- `starter.html` / `starter.js` 起始代码
|
- `starter.html` / `starter.js` 起始代码
|
||||||
- `answer.html` / `answer.js` 参考答案
|
- `answer.html` / `answer.js` 参考答案
|
||||||
|
|
||||||
前七部分现在都已经补充到“核心主线 + 常见细分知识点”。
|
现在也已经整理好 `08-vue3`,里面包含:
|
||||||
|
|
||||||
|
- Vue3(组合式 API + 响应式原理)讲义
|
||||||
|
- 分阶段练习
|
||||||
|
- `starter.html` / `starter.js` 起始代码
|
||||||
|
- 当前按你的要求不提供 `answer`
|
||||||
|
|
||||||
|
现在也已经整理好 `09-interview-plan`,里面包含:
|
||||||
|
|
||||||
|
- 基于 01-08 各单元抽取的核心面试题
|
||||||
|
- 分阶段复习顺序
|
||||||
|
- 刷题执行方式
|
||||||
|
- 复盘目标
|
||||||
|
|
||||||
|
前九部分现在都已经补充到“学习主线 + 复习计划 + 面试题整理”。
|
||||||
|
|
||||||
## 使用方式
|
## 使用方式
|
||||||
|
|
||||||
@@ -75,9 +91,11 @@
|
|||||||
5. 再阅读 [05-es6-plus/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/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)
|
6. 再阅读 [06-typescript/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/README.md)
|
||||||
7. 再阅读 [07-vue2/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/07-vue2/README.md)
|
7. 再阅读 [07-vue2/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/07-vue2/README.md)
|
||||||
8. 按顺序完成每个练习目录
|
8. 再阅读 [08-vue3/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/08-vue3/README.md)
|
||||||
9. 先写 `starter.html`、`starter.css`、`starter.js` 或 `starter.ts`
|
9. 再阅读 [09-interview-plan/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/09-interview-plan/README.md)
|
||||||
10. 写完后再对照答案文件
|
10. 按顺序完成每个练习目录
|
||||||
11. 学 `06-typescript` 时,也可以进入 [06-typescript](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript) 后执行 `npm install` 和 `npm run dev`
|
11. 先写 `starter.html`、`starter.css`、`starter.js` 或 `starter.ts`
|
||||||
|
12. 写完后再对照答案文件
|
||||||
|
13. 学 `06-typescript` 时,也可以进入 [06-typescript](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript) 后执行 `npm install` 和 `npm run dev`
|
||||||
|
|
||||||
如果你后面要继续学其他知识点,我可以按同样结构继续给你补更多工程化目录。
|
如果你后面要继续学其他知识点,我可以按同样结构继续给你补更多工程化目录。
|
||||||
|
|||||||
Reference in New Issue
Block a user