This commit is contained in:
rou
2026-03-23 14:56:21 +08:00
48 changed files with 1705 additions and 48 deletions

View File

@@ -1,10 +1,7 @@
import "./style.css";
import { lessons } from "./lessons";
function getRequiredElement<T extends Element>(
selector: string,
parent: ParentNode = document,
): T {
function getRequiredElement<T extends Element>(selector: string, parent: ParentNode = document): T {
const element = parent.querySelector<T>(selector);
if (!element) {
@@ -35,34 +32,21 @@ app.innerHTML = `
<p id="lesson-focus" class="focus"></p>
<p id="lesson-summary" class="summary"></p>
</section>
<section class="grid">
<section >
<article class="card">
<div class="card-head">
<h3>知识点</h3>
</div>
<ul id="lesson-points" class="point-list"></ul>
</article>
<article class="card">
<div class="card-head">
<h3>运行结果</h3>
<button id="run-demo" class="run-button" type="button">运行演示</button>
</div>
<div id="lesson-output" class="output-panel"></div>
</article>
</section>
<section class="grid code-grid">
<section >
<article class="card code-card">
<div class="card-head">
<h3>Starter</h3>
</div>
<pre><code id="starter-code"></code></pre>
</article>
<article class="card code-card">
<div class="card-head">
<h3>Answer</h3>
</div>
<pre><code id="answer-code"></code></pre>
</article>
</section>
</main>
</div>
@@ -73,10 +57,7 @@ const title = getRequiredElement<HTMLElement>("#lesson-title");
const focus = getRequiredElement<HTMLElement>("#lesson-focus");
const summary = getRequiredElement<HTMLElement>("#lesson-summary");
const pointList = getRequiredElement<HTMLElement>("#lesson-points");
const output = getRequiredElement<HTMLElement>("#lesson-output");
const starterCode = getRequiredElement<HTMLElement>("#starter-code");
const answerCode = getRequiredElement<HTMLElement>("#answer-code");
const runButton = getRequiredElement<HTMLButtonElement>("#run-demo");
let activeLessonId = lessons[0]?.id ?? "";
@@ -101,12 +82,6 @@ function renderLessonList(): void {
.join("");
}
function renderOutput(lines: string[]): void {
output.innerHTML = lines
.map((line) => `<div class="output-line">${line}</div>`)
.join("");
}
function renderActiveLesson(): void {
const lesson = lessons.find((item) => item.id === activeLessonId);
@@ -117,12 +92,8 @@ function renderActiveLesson(): void {
title.textContent = `${lesson.id}. ${lesson.title}`;
focus.textContent = lesson.focus;
summary.textContent = lesson.summary;
pointList.innerHTML = lesson.keyPoints
.map((item) => `<li>${item}</li>`)
.join("");
pointList.innerHTML = lesson.keyPoints.map((item) => `<li>${item}</li>`).join("");
starterCode.textContent = lesson.starterCode;
answerCode.textContent = lesson.answerCode;
renderOutput(lesson.runDemo());
renderLessonList();
}
@@ -138,14 +109,4 @@ nav.addEventListener("click", (event) => {
renderActiveLesson();
});
runButton.addEventListener("click", () => {
const lesson = lessons.find((item) => item.id === activeLessonId);
if (!lesson) {
return;
}
renderOutput(lesson.runDemo());
});
renderActiveLesson();

View File

@@ -0,0 +1,28 @@
# 练习 1Vue 实例和数据
## 目标
学会写一个最基础的 Vue2 实例,并让页面内容跟着数据变化。
## 你要练什么
- `new Vue()`
- `el`
- `data`
- `methods`
- 模板插值 `{{ }}`
## 任务
请基于页面结构完成以下操作:
- 显示课程标题和章节数量
- 点击按钮后让学习人数加 1
- 在控制台输出当前学习人数
## 文件
- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/07-vue2/01-vue-instance-and-data/starter.html)
- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/07-vue2/01-vue-instance-and-data/starter.js)
- [answer.html](/Users/lijiaqing/home/wwwroot/front-end-example/07-vue2/01-vue-instance-and-data/answer.html)
- [answer.js](/Users/lijiaqing/home/wwwroot/front-end-example/07-vue2/01-vue-instance-and-data/answer.js)

View File

@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vue 实例和数据</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>本章共有 {{ lessonCount }} 个小节。</p>
<p>当前学习人数:{{ learnerCount }}</p>
<button type="button" @click="increaseLearner">加入学习</button>
</section>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script src="./answer.js"></script>
</body>
</html>

View File

@@ -0,0 +1,14 @@
new Vue({
el: "#app",
data: {
title: "Vue2 基础入门",
lessonCount: 8,
learnerCount: 12,
},
methods: {
increaseLearner() {
this.learnerCount += 1;
console.log("当前学习人数:", this.learnerCount);
},
},
});

View File

@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vue 实例和数据</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>本章共有 {{ lessonCount }} 个小节。</p>
<p>当前学习人数:{{ learnerCount }}</p>
<button type="button" @click="increaseLearner">加入学习</button>
</section>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script src="./starter.js"></script>
</body>
</html>

View File

@@ -0,0 +1,15 @@
new Vue({
el: "#app",
data: {
title: "Vue2 基础入门",
lessonCount: 8,
learnerCount: 12,
},
methods: {
increaseLearner() {
// 任务:
// 1. learnerCount 加 1
// 2. 在控制台输出最新人数
},
},
});

View File

@@ -0,0 +1,27 @@
# 练习 2模板语法和绑定
## 目标
学会把数据绑定到属性和事件上,而不是手动改 DOM。
## 你要练什么
- `{{ }}`
- `v-bind`
- `v-on`
- 事件方法
## 任务
请基于页面结构完成以下操作:
- 显示课程名和讲师名
- 绑定图片地址和课程链接
- 点击按钮后切换收藏状态
## 文件
- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/07-vue2/02-template-bindings/starter.html)
- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/07-vue2/02-template-bindings/starter.js)
- [answer.html](/Users/lijiaqing/home/wwwroot/front-end-example/07-vue2/02-template-bindings/answer.html)
- [answer.js](/Users/lijiaqing/home/wwwroot/front-end-example/07-vue2/02-template-bindings/answer.js)

View 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>模板绑定</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="./answer.js"></script>
</body>
</html>

View File

@@ -0,0 +1,16 @@
new Vue({
el: "#app",
data: {
courseTitle: "Vue2 模板语法",
teacher: "李老师",
coverUrl: "https://picsum.photos/800/320?random=21",
courseLink: "https://cn.vuejs.org/",
isCollected: false,
},
methods: {
toggleCollect() {
this.isCollected = !this.isCollected;
console.log("收藏状态:", this.isCollected ? "已收藏" : "未收藏");
},
},
});

View 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>模板绑定</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>

View File

@@ -0,0 +1,17 @@
new Vue({
el: "#app",
data: {
courseTitle: "Vue2 模板语法",
teacher: "李老师",
coverUrl: "https://picsum.photos/800/320?random=21",
courseLink: "https://cn.vuejs.org/",
isCollected: false,
},
methods: {
toggleCollect() {
// 任务:
// 1. 切换 isCollected
// 2. 在控制台输出最新收藏状态
},
},
});

View File

@@ -0,0 +1,31 @@
# 练习 3v-model 和表单
## 目标
学会用 `v-model` 处理输入、选择和实时预览。
## 你要练什么
- `v-model`
- 输入框
- 多行文本
- 下拉框
- 单选框
- 复选框
## 任务
请基于页面结构完成以下操作:
- 输入昵称并实时显示
- 输入学习目标并实时显示
- 选择当前阶段并显示结果
- 选择偏好的学习节奏
- 勾选已经掌握的基础能力
## 文件
- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/07-vue2/03-v-model-and-form/starter.html)
- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/07-vue2/03-v-model-and-form/starter.js)
- [answer.html](/Users/lijiaqing/home/wwwroot/front-end-example/07-vue2/03-v-model-and-form/answer.html)
- [answer.js](/Users/lijiaqing/home/wwwroot/front-end-example/07-vue2/03-v-model-and-form/answer.js)

View File

@@ -0,0 +1,59 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>v-model 和表单</title>
<style>
body { margin: 0; padding: 32px; font-family: "PingFang SC", sans-serif; background: #f7f9fc; }
.wrap { max-width: 760px; margin: 0 auto; display: grid; gap: 18px; }
.card { padding: 22px; border-radius: 18px; background: #fff; border: 1px solid #dde5f3; }
input, textarea, select { width: 100%; margin-top: 8px; padding: 12px; border-radius: 12px; border: 1px solid #cfd8ea; }
</style>
</head>
<body>
<section id="app" class="wrap">
<article class="card">
<label>
昵称
<input v-model="nickname" type="text" placeholder="请输入昵称" />
</label>
<label>
学习目标
<textarea v-model="goal" rows="4" placeholder="请输入学习目标"></textarea>
</label>
<label>
当前阶段
<select v-model="stage">
<option value="入门">入门</option>
<option value="进阶">进阶</option>
<option value="项目实战">项目实战</option>
</select>
</label>
<div style="margin-top: 16px;">
<p>偏好的学习节奏</p>
<label><input v-model="pace" type="radio" value="每天学习" /> 每天学习</label>
<label><input v-model="pace" type="radio" value="每周集中学习" /> 每周集中学习</label>
</div>
<div style="margin-top: 16px;">
<p>已掌握的基础能力</p>
<label><input v-model="skills" type="checkbox" value="HTML" /> HTML</label>
<label><input v-model="skills" type="checkbox" value="CSS" /> CSS</label>
<label><input v-model="skills" type="checkbox" value="JavaScript" /> JavaScript</label>
</div>
</article>
<article class="card">
<h2>实时预览</h2>
<p>昵称:{{ nickname || "未填写" }}</p>
<p>学习目标:{{ goal || "还没有输入目标" }}</p>
<p>当前阶段:{{ stage }}</p>
<p>学习节奏:{{ pace }}</p>
<p>已掌握:{{ skills.length ? skills.join("、") : "还没有勾选" }}</p>
</article>
</section>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script src="./answer.js"></script>
</body>
</html>

View File

@@ -0,0 +1,10 @@
new Vue({
el: "#app",
data: {
nickname: "林晨",
goal: "希望独立完成一个 Vue2 管理后台页面。",
stage: "进阶",
pace: "每周集中学习",
skills: ["HTML", "CSS"],
},
});

View File

@@ -0,0 +1,59 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>v-model 和表单</title>
<style>
body { margin: 0; padding: 32px; font-family: "PingFang SC", sans-serif; background: #f7f9fc; }
.wrap { max-width: 760px; margin: 0 auto; display: grid; gap: 18px; }
.card { padding: 22px; border-radius: 18px; background: #fff; border: 1px solid #dde5f3; }
input, textarea, select { width: 100%; margin-top: 8px; padding: 12px; border-radius: 12px; border: 1px solid #cfd8ea; }
</style>
</head>
<body>
<section id="app" class="wrap">
<article class="card">
<label>
昵称
<input v-model="nickname" type="text" placeholder="请输入昵称" />
</label>
<label>
学习目标
<textarea v-model="goal" rows="4" placeholder="请输入学习目标"></textarea>
</label>
<label>
当前阶段
<select v-model="stage">
<option value="入门">入门</option>
<option value="进阶">进阶</option>
<option value="项目实战">项目实战</option>
</select>
</label>
<div style="margin-top: 16px;">
<p>偏好的学习节奏</p>
<label><input v-model="pace" type="radio" value="每天学习" /> 每天学习</label>
<label><input v-model="pace" type="radio" value="每周集中学习" /> 每周集中学习</label>
</div>
<div style="margin-top: 16px;">
<p>已掌握的基础能力</p>
<label><input v-model="skills" type="checkbox" value="HTML" /> HTML</label>
<label><input v-model="skills" type="checkbox" value="CSS" /> CSS</label>
<label><input v-model="skills" type="checkbox" value="JavaScript" /> JavaScript</label>
</div>
</article>
<article class="card">
<h2>实时预览</h2>
<p>昵称:{{ nickname }}</p>
<p>学习目标:{{ goal }}</p>
<p>当前阶段:{{ stage }}</p>
<p>学习节奏:{{ pace }}</p>
<p>已掌握:{{ skills.join("、") }}</p>
</article>
</section>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script src="./starter.js"></script>
</body>
</html>

View File

@@ -0,0 +1,10 @@
new Vue({
el: "#app",
data: {
nickname: "",
goal: "",
stage: "入门",
pace: "每天学习",
skills: [],
},
});

View File

@@ -0,0 +1,28 @@
# 练习 4条件渲染和列表渲染
## 目标
学会根据状态显示不同内容,并用 `v-for` 渲染一组数据。
## 你要练什么
- `v-if`
- `v-else`
- `v-show`
- `v-for`
- `:key`
## 任务
请基于页面结构完成以下操作:
- 切换“是否展开课程列表”
- 根据是否付费显示不同文案
-`v-for` 渲染课程数组
## 文件
- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/07-vue2/04-conditional-and-list-rendering/starter.html)
- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/07-vue2/04-conditional-and-list-rendering/starter.js)
- [answer.html](/Users/lijiaqing/home/wwwroot/front-end-example/07-vue2/04-conditional-and-list-rendering/answer.html)
- [answer.js](/Users/lijiaqing/home/wwwroot/front-end-example/07-vue2/04-conditional-and-list-rendering/answer.js)

View File

@@ -0,0 +1,32 @@
<!DOCTYPE html>
<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: #f5f8fc; }
.panel { max-width: 760px; margin: 0 auto; padding: 24px; border-radius: 18px; background: #fff; border: 1px solid #dbe4f4; }
ul { padding-left: 18px; }
button { padding: 10px 16px; border: 0; border-radius: 999px; background: #2f6ee5; color: #fff; cursor: pointer; }
</style>
</head>
<body>
<section id="app" class="panel">
<h1>Vue2 渲染练习</h1>
<button type="button" @click="showList = !showList">切换课程列表</button>
<p v-if="isPaid">你已经开通会员课程。</p>
<p v-else>你还没有开通会员课程。</p>
<ul v-show="showList">
<li v-for="course in courses" :key="course.id">
{{ course.title }} - {{ course.status }}
</li>
</ul>
</section>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script src="./answer.js"></script>
</body>
</html>

View File

@@ -0,0 +1,12 @@
new Vue({
el: "#app",
data: {
isPaid: true,
showList: true,
courses: [
{ id: 1, title: "Vue 实例", status: "已完成" },
{ id: 2, title: "指令系统", status: "学习中" },
{ id: 3, title: "组件通信", status: "未开始" },
],
},
});

View File

@@ -0,0 +1,32 @@
<!DOCTYPE html>
<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: #f5f8fc; }
.panel { max-width: 760px; margin: 0 auto; padding: 24px; border-radius: 18px; background: #fff; border: 1px solid #dbe4f4; }
ul { padding-left: 18px; }
button { padding: 10px 16px; border: 0; border-radius: 999px; background: #2f6ee5; color: #fff; cursor: pointer; }
</style>
</head>
<body>
<section id="app" class="panel">
<h1>Vue2 渲染练习</h1>
<button type="button" @click="showList = !showList">切换课程列表</button>
<p v-if="isPaid">你已经开通会员课程。</p>
<p v-else>你还没有开通会员课程。</p>
<ul v-show="showList">
<li v-for="course in courses" :key="course.id">
{{ course.title }} - {{ course.status }}
</li>
</ul>
</section>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script src="./starter.js"></script>
</body>
</html>

View File

@@ -0,0 +1,12 @@
new Vue({
el: "#app",
data: {
isPaid: false,
showList: true,
courses: [
{ id: 1, title: "Vue 实例", status: "已完成" },
{ id: 2, title: "指令系统", status: "学习中" },
{ id: 3, title: "组件通信", status: "未开始" },
],
},
});

View File

@@ -0,0 +1,27 @@
# 练习 5computed 和 watch
## 目标
学会区分“根据已有数据推导结果”和“监听变化做额外动作”。
## 你要练什么
- `computed`
- `watch`
- 数据推导
- 监听输入变化
## 任务
请基于页面结构完成以下操作:
- 根据关键字过滤课程列表
-`computed` 计算过滤结果数量
-`watch` 在关键字变化时输出日志
## 文件
- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/07-vue2/05-computed-and-watch/starter.html)
- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/07-vue2/05-computed-and-watch/starter.js)
- [answer.html](/Users/lijiaqing/home/wwwroot/front-end-example/07-vue2/05-computed-and-watch/answer.html)
- [answer.js](/Users/lijiaqing/home/wwwroot/front-end-example/07-vue2/05-computed-and-watch/answer.js)

View 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>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="./answer.js"></script>
</body>
</html>

View File

@@ -0,0 +1,30 @@
new Vue({
el: "#app",
data: {
keyword: "",
courses: [
{ id: 1, title: "Vue 实例入门" },
{ id: 2, title: "Vue 指令系统" },
{ id: 3, title: "组件通信实战" },
],
},
computed: {
filteredCourses() {
const value = this.keyword.trim().toLowerCase();
if (!value) {
return this.courses;
}
return this.courses.filter((item) => item.title.toLowerCase().includes(value));
},
matchedCount() {
return this.filteredCourses.length;
},
},
watch: {
keyword(newValue) {
console.log("关键字变化:", newValue);
},
},
});

View 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>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>

View File

@@ -0,0 +1,26 @@
new Vue({
el: "#app",
data: {
keyword: "",
courses: [
{ id: 1, title: "Vue 实例入门" },
{ id: 2, title: "Vue 指令系统" },
{ id: 3, title: "组件通信实战" },
],
},
computed: {
filteredCourses() {
// 任务:返回过滤后的课程列表
return this.courses;
},
matchedCount() {
// 任务:返回 filteredCourses 的数量
return 0;
},
},
watch: {
keyword(newValue) {
// 任务:在控制台输出关键字变化
},
},
});

View File

@@ -0,0 +1,27 @@
# 练习 6动态类名、样式和事件
## 目标
学会让组件状态直接影响样式和交互反馈。
## 你要练什么
- `:class`
- `:style`
- `v-on`
- 点击切换状态
## 任务
请基于页面结构完成以下操作:
- 点击卡片切换激活状态
- 根据激活状态切换类名
- 根据进度值动态改变进度条宽度
## 文件
- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/07-vue2/06-class-style-and-event/starter.html)
- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/07-vue2/06-class-style-and-event/starter.js)
- [answer.html](/Users/lijiaqing/home/wwwroot/front-end-example/07-vue2/06-class-style-and-event/answer.html)
- [answer.js](/Users/lijiaqing/home/wwwroot/front-end-example/07-vue2/06-class-style-and-event/answer.js)

View File

@@ -0,0 +1,31 @@
<!DOCTYPE html>
<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>
<script src="./answer.js"></script>
</body>
</html>

View File

@@ -0,0 +1,14 @@
new Vue({
el: "#app",
data: {
title: "动态样式练习",
isActive: false,
progress: 35,
},
methods: {
toggleCard() {
this.isActive = !this.isActive;
this.progress = this.isActive ? 80 : 35;
},
},
});

View File

@@ -0,0 +1,31 @@
<!DOCTYPE html>
<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>
<script src="./starter.js"></script>
</body>
</html>

View File

@@ -0,0 +1,16 @@
new Vue({
el: "#app",
data: {
title: "动态样式练习",
isActive: false,
progress: 35,
},
methods: {
toggleCard() {
// 任务:
// 1. 切换 isActive
// 2. 如果激活了,让 progress 增加到 80
// 3. 如果取消激活,让 progress 回到 35
},
},
});

View File

@@ -0,0 +1,31 @@
# 练习 7生命周期和异步更新
## 目标
学会在合适的生命周期里加载数据,并理解页面更新的时机。
## 你要练什么
- `created`
- `mounted`
- `updated`
- `beforeDestroy`
- `destroyed`
- 模拟异步请求
## 任务
请基于页面结构完成以下操作:
- 进入页面时显示 loading
-`mounted` 里模拟异步获取课程数据
- 数据返回后渲染课程列表
-`updated` 里输出更新日志
- 点击按钮时销毁当前实例,并观察销毁日志
## 文件
- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/07-vue2/07-lifecycle-and-async/starter.html)
- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/07-vue2/07-lifecycle-and-async/starter.js)
- [answer.html](/Users/lijiaqing/home/wwwroot/front-end-example/07-vue2/07-lifecycle-and-async/answer.html)
- [answer.js](/Users/lijiaqing/home/wwwroot/front-end-example/07-vue2/07-lifecycle-and-async/answer.js)

View 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>生命周期和异步更新</title>
<style>
body { margin: 0; padding: 32px; font-family: "PingFang SC", sans-serif; background: #f6f8fb; }
.panel { max-width: 760px; margin: 0 auto; padding: 24px; border-radius: 18px; background: #fff; border: 1px solid #dce4f1; }
</style>
</head>
<body>
<section id="app" class="panel">
<h1>生命周期练习</h1>
<button type="button" @click="destroyInstance">销毁当前实例</button>
<p v-if="loading">数据加载中...</p>
<ul v-else>
<li v-for="item in courses" :key="item.id">{{ item.title }}</li>
</ul>
</section>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script src="./answer.js"></script>
</body>
</html>

View File

@@ -0,0 +1,36 @@
new Vue({
el: "#app",
data: {
loading: true,
courses: [],
},
created() {
console.log("created: 实例已经创建");
},
mounted() {
console.log("mounted: 页面已经挂载");
setTimeout(() => {
this.courses = [
{ id: 1, title: "生命周期基础" },
{ id: 2, title: "异步数据渲染" },
{ id: 3, title: "组件更新时机" },
];
this.loading = false;
}, 1000);
},
updated() {
console.log("updated: 页面数据已经更新");
},
beforeDestroy() {
console.log("beforeDestroy: 实例即将销毁");
},
destroyed() {
console.log("destroyed: 实例已经销毁");
},
methods: {
destroyInstance() {
this.$destroy();
},
},
});

View 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>生命周期和异步更新</title>
<style>
body { margin: 0; padding: 32px; font-family: "PingFang SC", sans-serif; background: #f6f8fb; }
.panel { max-width: 760px; margin: 0 auto; padding: 24px; border-radius: 18px; background: #fff; border: 1px solid #dce4f1; }
</style>
</head>
<body>
<section id="app" class="panel">
<h1>生命周期练习</h1>
<button type="button" @click="destroyInstance">销毁当前实例</button>
<p v-if="loading">数据加载中...</p>
<ul v-else>
<li v-for="item in courses" :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>

View File

@@ -0,0 +1,30 @@
new Vue({
el: "#app",
data: {
loading: true,
courses: [],
},
created() {
console.log("created: 实例已经创建");
},
mounted() {
// 任务:
// 1. 模拟异步请求
// 2. 1 秒后给 courses 赋值
// 3. loading 改成 false
},
updated() {
// 任务:在控制台输出 updated 日志
},
beforeDestroy() {
// 任务:在控制台输出 beforeDestroy 日志
},
destroyed() {
// 任务:在控制台输出 destroyed 日志
},
methods: {
destroyInstance() {
// 任务:调用 this.$destroy()
},
},
});

View File

@@ -0,0 +1,33 @@
# 练习 8组件通信与综合练习
## 目标
把 Vue2 最核心的组件能力串起来,完成一个小型页面。
## 你要练什么
- 组件注册(全局 / 局部)
- `props`
- `props` 校验
- `$emit`
- `slot`
- `ref`
## 任务
请基于页面结构完成以下操作:
- 把课程卡片拆成子组件
- 注册一个全局角标组件
- 父组件通过 `props` 传课程数据
-`props` 补上类型、必填、默认值或校验规则
- 子组件点击按钮后通过 `$emit` 通知父组件切换状态
-`slot` 自定义按钮文案
-`ref` 聚焦输入框
## 文件
- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/07-vue2/08-components-communication-and-final/starter.html)
- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/07-vue2/08-components-communication-and-final/starter.js)
- [answer.html](/Users/lijiaqing/home/wwwroot/front-end-example/07-vue2/08-components-communication-and-final/answer.html)
- [answer.js](/Users/lijiaqing/home/wwwroot/front-end-example/07-vue2/08-components-communication-and-final/answer.js)

View File

@@ -0,0 +1,43 @@
<!DOCTYPE html>
<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: #f4f7fb; }
.wrap { max-width: 860px; margin: 0 auto; display: grid; gap: 16px; }
.toolbar, .card { padding: 20px; border-radius: 18px; background: #fff; border: 1px solid #d9e3f2; }
.list { display: grid; gap: 14px; }
button { padding: 10px 14px; border: 0; border-radius: 999px; background: #2d6cdf; color: #fff; cursor: pointer; }
input { width: 100%; padding: 12px; border-radius: 12px; border: 1px solid #cfd8e8; }
.done { color: #1f8f54; }
</style>
</head>
<body>
<section id="app" class="wrap">
<article class="toolbar">
<h1>Vue2 综合练习</h1>
<input ref="keywordInput" v-model="keyword" type="text" placeholder="输入关键字过滤课程" />
<button type="button" @click="focusInput">聚焦输入框</button>
</article>
<section class="list">
<course-card
v-for="course in filteredCourses"
:key="course.id"
:course="course"
theme="accent"
@toggle="toggleCourse"
>
<template v-slot:action>
切换完成状态
</template>
</course-card>
</section>
</section>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script src="./answer.js"></script>
</body>
</html>

View File

@@ -0,0 +1,80 @@
Vue.component("course-badge", {
props: {
label: {
type: String,
default: "默认角标",
},
},
template: `<span style="display:inline-block;margin-bottom:8px;padding:4px 10px;border-radius:999px;background:#eef4ff;color:#2d6cdf;">{{ label }}</span>`,
});
const CourseCard = {
props: {
course: {
type: Object,
required: true,
},
theme: {
type: String,
default: "normal",
validator(value) {
return ["normal", "accent"].includes(value);
},
},
},
template: `
<article class="card" :style="theme === 'accent' ? 'border-color:#2d6cdf;' : ''">
<course-badge :label="course.level"></course-badge>
<h2>{{ course.title }}</h2>
<p :class="{ done: course.finished }">
{{ course.finished ? "已完成" : "学习中" }}
</p>
<button type="button" @click="$emit('toggle', course.id)">
<slot name="action">操作</slot>
</button>
</article>
`,
};
new Vue({
el: "#app",
components: {
CourseCard,
},
data: {
keyword: "",
courses: [
{ id: 1, title: "Vue 实例", finished: true, level: "基础" },
{ id: 2, title: "模板语法", finished: false, level: "基础" },
{ id: 3, title: "组件通信", finished: false, level: "进阶" },
],
},
computed: {
filteredCourses() {
const value = this.keyword.trim().toLowerCase();
if (!value) {
return this.courses;
}
return this.courses.filter((course) => course.title.toLowerCase().includes(value));
},
},
methods: {
toggleCourse(courseId) {
this.courses = this.courses.map((course) => {
if (course.id === courseId) {
return {
...course,
finished: !course.finished,
};
}
return course;
});
},
focusInput() {
this.$refs.keywordInput.focus();
},
},
});

View File

@@ -0,0 +1,43 @@
<!DOCTYPE html>
<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: #f4f7fb; }
.wrap { max-width: 860px; margin: 0 auto; display: grid; gap: 16px; }
.toolbar, .card { padding: 20px; border-radius: 18px; background: #fff; border: 1px solid #d9e3f2; }
.list { display: grid; gap: 14px; }
button { padding: 10px 14px; border: 0; border-radius: 999px; background: #2d6cdf; color: #fff; cursor: pointer; }
input { width: 100%; padding: 12px; border-radius: 12px; border: 1px solid #cfd8e8; }
.done { color: #1f8f54; }
</style>
</head>
<body>
<section id="app" class="wrap">
<article class="toolbar">
<h1>Vue2 综合练习</h1>
<input ref="keywordInput" v-model="keyword" type="text" placeholder="输入关键字过滤课程" />
<button type="button" @click="focusInput">聚焦输入框</button>
</article>
<section class="list">
<course-card
v-for="course in filteredCourses"
:key="course.id"
:course="course"
theme="accent"
@toggle="toggleCourse"
>
<template v-slot:action>
切换完成状态
</template>
</course-card>
</section>
</section>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script src="./starter.js"></script>
</body>
</html>

View File

@@ -0,0 +1,66 @@
Vue.component("course-badge", {
props: {
label: {
type: String,
default: "默认角标",
},
},
template: `<span style="display:inline-block;margin-bottom:8px;padding:4px 10px;border-radius:999px;background:#eef4ff;color:#2d6cdf;">{{ label }}</span>`,
});
const CourseCard = {
props: {
course: {
type: Object,
required: true,
},
theme: {
type: String,
default: "normal",
validator(value) {
return ["normal", "accent"].includes(value);
},
},
},
template: `
<article class="card" :style="theme === 'accent' ? 'border-color:#2d6cdf;' : ''">
<course-badge :label="course.level"></course-badge>
<h2>{{ course.title }}</h2>
<p :class="{ done: course.finished }">
{{ course.finished ? "已完成" : "学习中" }}
</p>
<button type="button" @click="$emit('toggle', course.id)">
<slot name="action">操作</slot>
</button>
</article>
`,
};
new Vue({
el: "#app",
components: {
CourseCard,
},
data: {
keyword: "",
courses: [
{ id: 1, title: "Vue 实例", finished: true, level: "基础" },
{ id: 2, title: "模板语法", finished: false, level: "基础" },
{ id: 3, title: "组件通信", finished: false, level: "进阶" },
],
},
computed: {
filteredCourses() {
// 任务:根据 keyword 过滤课程列表
return this.courses;
},
},
methods: {
toggleCourse(courseId) {
// 任务:根据 courseId 切换 finished
},
focusInput() {
// 任务:通过 ref 聚焦输入框
},
},
});

View File

@@ -0,0 +1,44 @@
# 练习 9Vue2 综合课程管理面板
## 目标
把 Vue2 入门阶段最关键的能力串起来,完成一个带搜索、筛选、统计、组件通信的课程管理面板。
## 你要练什么
- `new Vue()`
- `data`
- `methods`
- `computed`
- `watch`
- `v-model`
- `v-if`
- `v-show`
- `v-for`
- `:key`
- `:class`
- `props`
- `props` 校验
- `$emit`
- `slot`
- `ref`
## 任务
请基于页面结构完成以下操作:
- 输入关键字过滤课程名称
- 用下拉框切换“全部 / 已完成 / 学习中”
- 点击按钮让搜索框获得焦点
- 用子组件渲染每一项课程
- 子组件通过 `$emit` 通知父组件切换完成状态
- 没有匹配结果时显示空状态
- 页面底部显示总数、已完成数、当前筛选数
-`watch` 里输出关键字变化日志
## 文件
- [starter.html](/Users/lijiaqing/home/wwwroot/front-end-example/07-vue2/09-final-course-dashboard/starter.html)
- [starter.js](/Users/lijiaqing/home/wwwroot/front-end-example/07-vue2/09-final-course-dashboard/starter.js)
- [answer.html](/Users/lijiaqing/home/wwwroot/front-end-example/07-vue2/09-final-course-dashboard/answer.html)
- [answer.js](/Users/lijiaqing/home/wwwroot/front-end-example/07-vue2/09-final-course-dashboard/answer.js)

View File

@@ -0,0 +1,66 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vue2 综合课程管理面板</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; }
.meta { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
.badge { display: inline-flex; padding: 4px 10px; border-radius: 999px; background: #edf4ff; color: #2d6cdf; font-size: 12px; }
.badge.advanced { background: #fff0e8; color: #d96a18; }
.stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; }
.empty { padding: 18px; border-radius: 16px; background: #fff6e8; color: #a06200; }
</style>
</head>
<body>
<section id="app" class="page">
<article class="panel">
<h1>Vue2 课程管理面板</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" class="empty">暂无匹配结果</div>
<div v-show="filteredCourses.length" class="list">
<course-item
v-for="course in filteredCourses"
:key="course.id"
:course="course"
@toggle="toggleCourse"
>
<template v-slot:action>
切换完成状态
</template>
</course-item>
</div>
</section>
<footer class="panel stats">
<div>总课程数:{{ totalCount }}</div>
<div>已完成数:{{ finishedCount }}</div>
<div>当前筛选数:{{ visibleCount }}</div>
</footer>
</section>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script src="./answer.js"></script>
</body>
</html>

View File

@@ -0,0 +1,99 @@
Vue.component("level-badge", {
props: {
level: {
type: String,
default: "基础",
},
},
computed: {
badgeClass() {
return this.level === "进阶" ? "badge advanced" : "badge";
},
},
template: `<span :class="badgeClass">{{ level }}</span>`,
});
const CourseItem = {
props: {
course: {
type: Object,
required: true,
},
},
template: `
<article class="course-card" :class="{ 'is-done': course.finished }">
<div class="meta">
<level-badge :level="course.level"></level-badge>
<strong>{{ course.title }}</strong>
</div>
<p>{{ course.finished ? "当前已完成" : "当前学习中" }}</p>
<button type="button" @click="$emit('toggle', course.id)">
<slot name="action">操作</slot>
</button>
</article>
`,
};
new Vue({
el: "#app",
components: {
CourseItem,
},
data: {
keyword: "",
statusFilter: "all",
courses: [
{ id: 1, title: "Vue 实例基础", finished: true, level: "基础" },
{ id: 2, title: "模板语法与指令", finished: false, level: "基础" },
{ id: 3, title: "组件通信实战", finished: false, level: "进阶" },
{ id: 4, title: "生命周期和异步", finished: true, level: "进阶" },
],
},
computed: {
filteredCourses() {
const keyword = this.keyword.trim().toLowerCase();
return this.courses.filter((course) => {
const matchStatus =
this.statusFilter === "all" ||
(this.statusFilter === "done" && course.finished) ||
(this.statusFilter === "doing" && !course.finished);
const matchKeyword = !keyword || course.title.toLowerCase().includes(keyword);
return matchStatus && matchKeyword;
});
},
totalCount() {
return this.courses.length;
},
finishedCount() {
return this.courses.filter((course) => course.finished).length;
},
visibleCount() {
return this.filteredCourses.length;
},
},
watch: {
keyword(newValue) {
console.log("搜索关键字变化:", newValue);
},
},
methods: {
toggleCourse(courseId) {
this.courses = this.courses.map((course) => {
if (course.id === courseId) {
return {
...course,
finished: !course.finished,
};
}
return course;
});
},
focusSearch() {
this.$refs.searchInput.focus();
},
},
});

View File

@@ -0,0 +1,65 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vue2 综合课程管理面板</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; }
.meta { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
.badge { display: inline-flex; padding: 4px 10px; border-radius: 999px; background: #edf4ff; color: #2d6cdf; font-size: 12px; }
.stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; }
.empty { padding: 18px; border-radius: 16px; background: #fff6e8; color: #a06200; }
</style>
</head>
<body>
<section id="app" class="page">
<article class="panel">
<h1>Vue2 课程管理面板</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" class="empty">暂无匹配结果</div>
<div v-show="filteredCourses.length" class="list">
<course-item
v-for="course in filteredCourses"
:key="course.id"
:course="course"
@toggle="toggleCourse"
>
<template v-slot:action>
切换完成状态
</template>
</course-item>
</div>
</section>
<footer class="panel stats">
<div>总课程数:{{ totalCount }}</div>
<div>已完成数:{{ finishedCount }}</div>
<div>当前筛选数:{{ visibleCount }}</div>
</footer>
</section>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script src="./starter.js"></script>
</body>
</html>

View File

@@ -0,0 +1,79 @@
Vue.component("level-badge", {
props: {
level: {
type: String,
default: "基础",
},
},
template: `<span class="badge">{{ level }}</span>`,
});
const CourseItem = {
props: {
course: {
type: Object,
required: true,
},
},
template: `
<article class="course-card" :class="{ 'is-done': course.finished }">
<div class="meta">
<level-badge :level="course.level"></level-badge>
<strong>{{ course.title }}</strong>
</div>
<p>{{ course.finished ? "当前已完成" : "当前学习中" }}</p>
<button type="button" @click="$emit('toggle', course.id)">
<slot name="action">操作</slot>
</button>
</article>
`,
};
new Vue({
el: "#app",
components: {
CourseItem,
},
data: {
keyword: "",
statusFilter: "all",
courses: [
{ id: 1, title: "Vue 实例基础", finished: true, level: "基础" },
{ id: 2, title: "模板语法与指令", finished: false, level: "基础" },
{ id: 3, title: "组件通信实战", finished: false, level: "进阶" },
{ id: 4, title: "生命周期和异步", finished: true, level: "进阶" },
],
},
computed: {
filteredCourses() {
// 任务:
// 1. 先按 statusFilter 过滤
// 2. 再按 keyword 过滤标题
return this.courses;
},
totalCount() {
return this.courses.length;
},
finishedCount() {
// 任务:返回已完成课程数量
return 0;
},
visibleCount() {
// 任务:返回当前筛选后的数量
return 0;
},
},
watch: {
keyword(newValue) {
// 任务:在控制台输出关键字变化
},
},
methods: {
toggleCourse(courseId) {
// 任务:根据 courseId 切换 finished
},
focusSearch() {
// 任务:通过 ref 聚焦输入框
},
},
});

158
07-vue2/README.md Normal file
View File

@@ -0,0 +1,158 @@
# Vue2理解框架思想
这部分只解决一个问题:你能不能从“手写 DOM”切换到“数据驱动视图”的思维方式并理解 Vue2 为什么能让页面开发更高效。
## 学完后你应该掌握
- Vue2 是什么,为什么说它是渐进式框架
- `new Vue()``el``data``methods` 的基础写法
- 模板插值 `{{ }}`
- `v-bind``v-on`
- `v-model` 做表单双向绑定,包括文本、单选和复选
- `v-if``v-show``v-for``:key`
- `computed``watch` 的基本区别
- `:class``:style` 的动态绑定
- 生命周期里的 `created``mounted``updated``beforeDestroy``destroyed`
- 组件、`props``props` 校验、`$emit`
- 插槽 `slot`
- `ref` 的基本用法
- 如何把这些能力组合成一个 Vue2 小页面
## 这一章在解决什么
Vue2 不是教你再多学一套“新语法”,而是帮你把页面开发的思路改掉。
它回答的是:
- 页面内容为什么应该由数据决定
- 一个点击事件为什么不一定要手动改 DOM
- 一段重复结构为什么应该交给 `v-for`
- 一块页面为什么应该拆成组件
- 父组件和子组件之间怎么传数据和传事件
## 必须建立的 6 个核心意识
### 1. 视图应该跟着数据走
不是先找 DOM 再改内容,而是先改数据,让页面自动更新。
### 2. 指令是模板和数据之间的桥
`v-bind``v-on``v-model``v-if``v-for` 这些指令,本质上是在描述“数据怎么影响页面”。
### 3. 计算属性更适合“基于已有数据得出结果”
如果一个值可以从别的数据推导出来,优先考虑 `computed`
### 4. `watch` 更适合“监听变化后做副作用”
比如发请求、写日志、同步别的状态。
### 5. 组件化不是拆文件,而是拆职责
一个组件只关心一块明确的界面和交互。
### 6. 父子通信是 Vue 开发主线
- 父组件通过 `props` 往下传数据
- 子组件通过 `$emit` 往上通知事件
## 全部知识点清单
### 基础认知
- Vue2 渐进式框架
- 数据驱动视图
- 声明式渲染
### 实例基础
- `new Vue()`
- `el`
- `data`
- `methods`
### 模板与指令
- `{{ }}`
- `v-bind`
- `v-on`
- `v-model`
- `radio`
- `checkbox`
- `v-if`
- `v-else`
- `v-show`
- `v-for`
- `:key`
### 响应式进阶
- `computed`
- `watch`
- `:class`
- `:style`
### 生命周期
- `created`
- `mounted`
- `updated`
- `beforeDestroy`
- `destroyed`
### 组件通信
- 组件注册(全局 / 局部)
- `props`
- `props` 校验
- `$emit`
- `slot`
- `ref`
- 综合页面组织能力
## 学习顺序
1. Vue 实例和数据绑定
2. 模板语法和事件绑定
3. 表单双向绑定
4. 条件渲染和列表渲染
5. `computed``watch`
6. 动态类名、样式和事件交互
7. 生命周期和异步更新
8. 组件通信
9. 综合课程管理面板
## 练习目录
- [01-vue-instance-and-data/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/07-vue2/01-vue-instance-and-data/README.md)
- [02-template-bindings/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/07-vue2/02-template-bindings/README.md)
- [03-v-model-and-form/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/07-vue2/03-v-model-and-form/README.md)
- [04-conditional-and-list-rendering/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/07-vue2/04-conditional-and-list-rendering/README.md)
- [05-computed-and-watch/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/07-vue2/05-computed-and-watch/README.md)
- [06-class-style-and-event/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/07-vue2/06-class-style-and-event/README.md)
- [07-lifecycle-and-async/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/07-vue2/07-lifecycle-and-async/README.md)
- [08-components-communication-and-final/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/07-vue2/08-components-communication-and-final/README.md)
- [09-final-course-dashboard/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/07-vue2/09-final-course-dashboard/README.md)
## 过关标准
如果你能独立做到下面这些,就说明这一章已经基本过关:
- 能写出一个基础 Vue2 实例
- 能用模板语法展示数据
- 能用 `v-model` 处理输入
- 能用 `v-model` 处理单选框和复选框
- 能用 `v-if``v-show``v-for` 组织页面
- 能分清 `computed``watch`
- 能写出基础组件并使用 `props` / `props` 校验 / `$emit`
- 能理解插槽和 `ref` 的常见用途
- 能知道 Vue 实例销毁前后会经过哪些生命周期
- 能完成一个带组件通信的小页面
## 学习建议
- 学 Vue2 时先不要急着记所有 API先抓住“数据变页面跟着变”这条主线
- 遇到页面不更新时,先看是不是数据没有改到
- 遇到组件通信问题时,先分清“数据往下传”还是“事件往上抛”
- 学组件时先从 2 个组件开始,不要一上来拆太细

View File

@@ -10,6 +10,7 @@
- `04-dom-events-async`:预留给 DOM + 事件 + 异步
- `05-es6-plus`:预留给 ES6+(现代 JS
- `06-typescript`:预留给 TypeScript
- `07-vue2`:预留给 Vue2
## 当前可学内容
@@ -56,7 +57,14 @@
- `answer.ts` 参考答案
- `Vite + TypeScript` 学习面板
前六部分现在已经补充到“核心主线 + 常见细分知识点”。
现在已经整理好 `07-vue2`,里面包含:
- Vue2理解框架思想讲义
- 分阶段练习
- `starter.html` / `starter.js` 起始代码
- `answer.html` / `answer.js` 参考答案
前七部分现在都已经补充到“核心主线 + 常见细分知识点”。
## 使用方式
@@ -66,9 +74,10 @@
4. 再阅读 [04-dom-events-async/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/04-dom-events-async/README.md)
5. 再阅读 [05-es6-plus/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/05-es6-plus/README.md)
6. 再阅读 [06-typescript/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript/README.md)
7. 按顺序完成每个练习目录
8. 先写 `starter.html``starter.css``starter.js``starter.ts`
9. 写完后再对照答案文件
10. `06-typescript` 时,也可以进入 [06-typescript](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript) 后执行 `npm install``npm run dev`
7. 再阅读 [07-vue2/README.md](/Users/lijiaqing/home/wwwroot/front-end-example/07-vue2/README.md)
8. 按顺序完成每个练习目录
9. 先写 `starter.html``starter.css``starter.js``starter.ts`
10. 写完后再对照答案文件
11.`06-typescript` 时,也可以进入 [06-typescript](/Users/lijiaqing/home/wwwroot/front-end-example/06-typescript) 后执行 `npm install``npm run dev`
如果你后面要继续学其他知识点,我可以按同样结构继续给你补更多工程化目录。