Merge branch 'main' of https://gitea.lililibra.cn/rou/front-end-example
This commit is contained in:
@@ -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();
|
||||
|
||||
28
07-vue2/01-vue-instance-and-data/README.md
Normal file
28
07-vue2/01-vue-instance-and-data/README.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# 练习 1:Vue 实例和数据
|
||||
|
||||
## 目标
|
||||
|
||||
学会写一个最基础的 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)
|
||||
24
07-vue2/01-vue-instance-and-data/answer.html
Normal file
24
07-vue2/01-vue-instance-and-data/answer.html
Normal 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>
|
||||
14
07-vue2/01-vue-instance-and-data/answer.js
Normal file
14
07-vue2/01-vue-instance-and-data/answer.js
Normal 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);
|
||||
},
|
||||
},
|
||||
});
|
||||
24
07-vue2/01-vue-instance-and-data/starter.html
Normal file
24
07-vue2/01-vue-instance-and-data/starter.html
Normal 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>
|
||||
15
07-vue2/01-vue-instance-and-data/starter.js
Normal file
15
07-vue2/01-vue-instance-and-data/starter.js
Normal file
@@ -0,0 +1,15 @@
|
||||
new Vue({
|
||||
el: "#app",
|
||||
data: {
|
||||
title: "Vue2 基础入门",
|
||||
lessonCount: 8,
|
||||
learnerCount: 12,
|
||||
},
|
||||
methods: {
|
||||
increaseLearner() {
|
||||
// 任务:
|
||||
// 1. learnerCount 加 1
|
||||
// 2. 在控制台输出最新人数
|
||||
},
|
||||
},
|
||||
});
|
||||
27
07-vue2/02-template-bindings/README.md
Normal file
27
07-vue2/02-template-bindings/README.md
Normal 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)
|
||||
28
07-vue2/02-template-bindings/answer.html
Normal file
28
07-vue2/02-template-bindings/answer.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>模板绑定</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>
|
||||
16
07-vue2/02-template-bindings/answer.js
Normal file
16
07-vue2/02-template-bindings/answer.js
Normal 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 ? "已收藏" : "未收藏");
|
||||
},
|
||||
},
|
||||
});
|
||||
28
07-vue2/02-template-bindings/starter.html
Normal file
28
07-vue2/02-template-bindings/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>模板绑定</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>
|
||||
17
07-vue2/02-template-bindings/starter.js
Normal file
17
07-vue2/02-template-bindings/starter.js
Normal 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. 在控制台输出最新收藏状态
|
||||
},
|
||||
},
|
||||
});
|
||||
31
07-vue2/03-v-model-and-form/README.md
Normal file
31
07-vue2/03-v-model-and-form/README.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# 练习 3:v-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)
|
||||
59
07-vue2/03-v-model-and-form/answer.html
Normal file
59
07-vue2/03-v-model-and-form/answer.html
Normal 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>
|
||||
10
07-vue2/03-v-model-and-form/answer.js
Normal file
10
07-vue2/03-v-model-and-form/answer.js
Normal file
@@ -0,0 +1,10 @@
|
||||
new Vue({
|
||||
el: "#app",
|
||||
data: {
|
||||
nickname: "林晨",
|
||||
goal: "希望独立完成一个 Vue2 管理后台页面。",
|
||||
stage: "进阶",
|
||||
pace: "每周集中学习",
|
||||
skills: ["HTML", "CSS"],
|
||||
},
|
||||
});
|
||||
59
07-vue2/03-v-model-and-form/starter.html
Normal file
59
07-vue2/03-v-model-and-form/starter.html
Normal 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>
|
||||
10
07-vue2/03-v-model-and-form/starter.js
Normal file
10
07-vue2/03-v-model-and-form/starter.js
Normal file
@@ -0,0 +1,10 @@
|
||||
new Vue({
|
||||
el: "#app",
|
||||
data: {
|
||||
nickname: "",
|
||||
goal: "",
|
||||
stage: "入门",
|
||||
pace: "每天学习",
|
||||
skills: [],
|
||||
},
|
||||
});
|
||||
28
07-vue2/04-conditional-and-list-rendering/README.md
Normal file
28
07-vue2/04-conditional-and-list-rendering/README.md
Normal 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)
|
||||
32
07-vue2/04-conditional-and-list-rendering/answer.html
Normal file
32
07-vue2/04-conditional-and-list-rendering/answer.html
Normal 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>
|
||||
12
07-vue2/04-conditional-and-list-rendering/answer.js
Normal file
12
07-vue2/04-conditional-and-list-rendering/answer.js
Normal 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: "未开始" },
|
||||
],
|
||||
},
|
||||
});
|
||||
32
07-vue2/04-conditional-and-list-rendering/starter.html
Normal file
32
07-vue2/04-conditional-and-list-rendering/starter.html
Normal 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>
|
||||
12
07-vue2/04-conditional-and-list-rendering/starter.js
Normal file
12
07-vue2/04-conditional-and-list-rendering/starter.js
Normal 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: "未开始" },
|
||||
],
|
||||
},
|
||||
});
|
||||
27
07-vue2/05-computed-and-watch/README.md
Normal file
27
07-vue2/05-computed-and-watch/README.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# 练习 5:computed 和 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)
|
||||
28
07-vue2/05-computed-and-watch/answer.html
Normal file
28
07-vue2/05-computed-and-watch/answer.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>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>
|
||||
30
07-vue2/05-computed-and-watch/answer.js
Normal file
30
07-vue2/05-computed-and-watch/answer.js
Normal 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);
|
||||
},
|
||||
},
|
||||
});
|
||||
28
07-vue2/05-computed-and-watch/starter.html
Normal file
28
07-vue2/05-computed-and-watch/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>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>
|
||||
26
07-vue2/05-computed-and-watch/starter.js
Normal file
26
07-vue2/05-computed-and-watch/starter.js
Normal 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) {
|
||||
// 任务:在控制台输出关键字变化
|
||||
},
|
||||
},
|
||||
});
|
||||
27
07-vue2/06-class-style-and-event/README.md
Normal file
27
07-vue2/06-class-style-and-event/README.md
Normal 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)
|
||||
31
07-vue2/06-class-style-and-event/answer.html
Normal file
31
07-vue2/06-class-style-and-event/answer.html
Normal 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>
|
||||
14
07-vue2/06-class-style-and-event/answer.js
Normal file
14
07-vue2/06-class-style-and-event/answer.js
Normal 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;
|
||||
},
|
||||
},
|
||||
});
|
||||
31
07-vue2/06-class-style-and-event/starter.html
Normal file
31
07-vue2/06-class-style-and-event/starter.html
Normal 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>
|
||||
16
07-vue2/06-class-style-and-event/starter.js
Normal file
16
07-vue2/06-class-style-and-event/starter.js
Normal 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
|
||||
},
|
||||
},
|
||||
});
|
||||
31
07-vue2/07-lifecycle-and-async/README.md
Normal file
31
07-vue2/07-lifecycle-and-async/README.md
Normal 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)
|
||||
25
07-vue2/07-lifecycle-and-async/answer.html
Normal file
25
07-vue2/07-lifecycle-and-async/answer.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>生命周期和异步更新</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>
|
||||
36
07-vue2/07-lifecycle-and-async/answer.js
Normal file
36
07-vue2/07-lifecycle-and-async/answer.js
Normal 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();
|
||||
},
|
||||
},
|
||||
});
|
||||
25
07-vue2/07-lifecycle-and-async/starter.html
Normal file
25
07-vue2/07-lifecycle-and-async/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>生命周期和异步更新</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>
|
||||
30
07-vue2/07-lifecycle-and-async/starter.js
Normal file
30
07-vue2/07-lifecycle-and-async/starter.js
Normal 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()
|
||||
},
|
||||
},
|
||||
});
|
||||
33
07-vue2/08-components-communication-and-final/README.md
Normal file
33
07-vue2/08-components-communication-and-final/README.md
Normal 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)
|
||||
43
07-vue2/08-components-communication-and-final/answer.html
Normal file
43
07-vue2/08-components-communication-and-final/answer.html
Normal 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>
|
||||
80
07-vue2/08-components-communication-and-final/answer.js
Normal file
80
07-vue2/08-components-communication-and-final/answer.js
Normal 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();
|
||||
},
|
||||
},
|
||||
});
|
||||
43
07-vue2/08-components-communication-and-final/starter.html
Normal file
43
07-vue2/08-components-communication-and-final/starter.html
Normal 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>
|
||||
66
07-vue2/08-components-communication-and-final/starter.js
Normal file
66
07-vue2/08-components-communication-and-final/starter.js
Normal 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 聚焦输入框
|
||||
},
|
||||
},
|
||||
});
|
||||
44
07-vue2/09-final-course-dashboard/README.md
Normal file
44
07-vue2/09-final-course-dashboard/README.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# 练习 9:Vue2 综合课程管理面板
|
||||
|
||||
## 目标
|
||||
|
||||
把 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)
|
||||
66
07-vue2/09-final-course-dashboard/answer.html
Normal file
66
07-vue2/09-final-course-dashboard/answer.html
Normal 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>
|
||||
99
07-vue2/09-final-course-dashboard/answer.js
Normal file
99
07-vue2/09-final-course-dashboard/answer.js
Normal 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();
|
||||
},
|
||||
},
|
||||
});
|
||||
65
07-vue2/09-final-course-dashboard/starter.html
Normal file
65
07-vue2/09-final-course-dashboard/starter.html
Normal 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>
|
||||
79
07-vue2/09-final-course-dashboard/starter.js
Normal file
79
07-vue2/09-final-course-dashboard/starter.js
Normal 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
158
07-vue2/README.md
Normal 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 个组件开始,不要一上来拆太细
|
||||
19
README.md
19
README.md
@@ -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`
|
||||
|
||||
如果你后面要继续学其他知识点,我可以按同样结构继续给你补更多工程化目录。
|
||||
|
||||
Reference in New Issue
Block a user