上一章你会用插值把数据「印」到页面上了,但真实页面里的数据从来不是一五一十地全铺出来:登录了才显示用户菜单、有数据才渲染列表、没数据要显示「暂无」、推荐的才加个角标。用 v-if / v-show 决定「显示什么」,用 v-for 决定「重复渲染什么」——这两组指令,撑起了「古典文学网」几乎所有页面的骨架。
<script setup>
import { ref } from 'vue'
// 三种状态:加载中 / 出错 / 加载完成
const loading = ref(true)
const error = ref('')
const article = ref(null)
// 真实项目里这里会请求接口,本章先手动把状态改好,专心看渲染
loading.value = false
article.value = { id: 1, title: '静夜思', author: '李白', dynasty: '唐' }
</script>
<template>
<p v-if="loading" class="loading">加载中……</p>
<p v-else-if="error" class="err">{{ error }}</p>
<div v-else class="detail">
<h2>{{ article.title }}</h2>
<p>{{ article.author }} · {{ article.dynasty }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue'
// 用户点一下切一次,切换非常频繁 —— 这种场景优先 v-show
const showBanner = ref(true)
</script>
<template>
<div v-show="showBanner" class="banner">首页轮播图</div>
<button @click="showBanner = !showBanner">显示 / 隐藏轮播图</button>
</template>
<script setup>
import { ref } from 'vue'
const showBanner = ref(true)
</script>
<template>
<div v-if="showBanner" class="banner">首页轮播图</div>
<button @click="showBanner = !showBanner">显示 / 隐藏轮播图</button>
</template>
<script setup>
import { ref } from 'vue'
const articles = ref([
{ id: 1, title: '静夜思', author: '李白' },
{ id: 2, title: '春望', author: '杜甫' },
{ id: 3, title: '陋室铭', author: '刘禹锡' }
])
// 对象也能遍历:(值, 键) —— 注意「值」在前,「键」在后
const category = ref({ id: 1, name: '唐诗', sort: 1 })
</script>
<template>
<h3>作品(共 {{ articles.length }} 篇)</h3>
<ul>
<li v-for="(item, index) in articles" :key="item.id">
{{ index + 1 }}. {{ item.title }} —— {{ item.author }}
</li>
</ul>
<h3>分类信息</h3>
<p v-for="(value, key) in category" :key="key">{{ key }}:{{ value }}</p>
</template>
<script setup>
import { ref } from 'vue'
const list = ref([
{ id: 101, title: '静夜思' },
{ id: 102, title: '春望' },
{ id: 103, title: '陋室铭' }
])
function removeFirst() {
list.value.splice(0, 1)
}
</script>
<template>
<!-- ❌ 用 index 当 key:数据一删,位置和身份的对应关系就断了 -->
<div v-for="(item, index) in list" :key="index" class="row">
<span>{{ item.title }}</span>
<input placeholder="写点读书笔记">
</div>
<button @click="removeFirst">删掉第一条</button>
</template>
<script setup>
import { ref, shallowRef } from 'vue'
const list = ref([
{ id: 1, title: '静夜思' },
{ id: 2, title: '春望' }
])
// ❌ 反例一:漏了 .value
// 这行不是"改数组第 0 项",而是给 ref 包装对象加了个叫 0 的属性,
// list.value 这个真数组一个字节都没变,界面当然不动
list[0] = { id: 3, title: '陋室铭' }
// ❌ 反例二:用了浅层响应式 shallowRef
// 它只跟踪 .value 整体被替换,数组内部的 push、下标赋值通通不触发更新
const tabs = shallowRef(['唐诗', '宋词'])
tabs.value.push('元曲')
</script>
<script setup>
import { ref } from 'vue'
const list = ref([
{ id: 1, title: '静夜思' },
{ id: 2, title: '春望' }
])
// ✅ 正例一:变更方法(会改动原数组,也能触发更新)
// push / pop / shift / unshift / splice / sort / reverse
list.value.push({ id: 3, title: '陋室铭' })
// ✅ 正例二:要替换某一项,就老老实实写 list.value[0]
list.value[0] = { id: 4, title: '声声慢' }
// ✅ 正例三:splice(起始下标, 删除几个, 要插入的值)——原地替换,语义最清楚
list.value.splice(1, 1, { id: 5, title: '天净沙·秋思' })
// ✅ 正例四:非变更方法返回新数组,整体重新赋值回去
// filter / map / concat / slice 都不会动原数组,所以要自己赋回去
list.value = list.value.filter(item => item.id !== 2)
// ✅ 正例五:想清空,「重新赋一个空数组」永远是最稳的写法
// list.value = []
</script>
现在把 v-if、v-show、v-for、:key 拼成一个「古典文学网」作品列表页:有数据就循环渲染、没数据显示「暂无作品」、推荐的加一个角标。这是你后面每一章都会反复打磨的那一个页面。
<template>
<div class="article-list">
<h2>作品列表</h2>
<!-- 空状态:一条数据都没有时给用户一句交代,别留一片白 -->
<p v-if="list.length === 0" class="empty">暂无作品,换个分类试试~</p>
<!-- 有数据:循环渲染,key 用后端的作品 id -->
<ul v-else>
<li v-for="item in list" :key="item.id" class="card">
<!-- 角标只在推荐作品上出现 -->
<span v-if="item.isRecommend" class="badge">推荐</span>
<b>{{ item.title }}</b>
<span class="meta">{{ item.author }} · {{ item.dynasty }}</span>
<span v-show="item.views > 300" class="hot">🔥 热门</span>
</li>
</ul>
</div>
</template>
再补上后半个「三态」——加载中、加载失败、空、有数据,四个分支一次写清。注意顺序:先判断加载中,再判断错误,再判断空,最后才是正常列表。
<template>
<p v-if="loading">加载中……</p>
<p v-else-if="error" class="err">{{ error }}</p>
<p v-else-if="list.length === 0">暂无作品,换个分类试试~</p>
<ul v-else>
<li v-for="item in list" :key="item.id">
<span v-if="item.isRecommend" class="badge">推荐</span>
{{ item.title }} —— {{ item.author }}
</li>
</ul>
</template>
<!-- 第 1 题 -->
<script setup>
import { ref } from 'vue'
const loading = ref(true)
const error = ref('')
const article = ref(null)
</script>
<template>
<p v-if="loading">加载中……</p>
<p v-else-if="error" class="err">{{ error }}</p>
<div v-else>{{ article.title }}</div>
</template>
<!-- 用 v-if 的理由:这三种状态是整块互斥的,同一时刻只该有一种存在。
而且详情数据没拿到之前,正常分支根本不该被渲染(连骨架都不该建)。
v-show 会先把三个分支都渲染出来再靠 display 藏,纯属浪费。 -->
<!-- 第 2 题 -->
<!-- 原因:index 是「位置」,不是「身份」。删掉第一条后,
原来的第 2 条 index 从 1 变成 0,Vue 以为还是同一行 → 复用旧 DOM,
输入框里已输入的内容就留在了原位置,于是串到了下一行。
正确写法:用后端给的业务主键当 key —— :key="item.id" -->
<!-- 第 3 题(红影赏析,把作品换成电影,套路完全一样) -->
<script setup>
import { ref, computed } from 'vue'
const movies = ref([
{ id: 1, title: '流浪地球', genre: '科幻', rating: 8.4 },
{ id: 2, title: '星际穿越', genre: '科幻', rating: 9.3 },
{ id: 3, title: '让子弹飞', genre: '剧情', rating: 9.0 }
])
// 排序用计算属性:不改原数组,只产出一份「排好序的副本」
const sorted = computed(() => [...movies.value].sort((a, b) => b.rating - a.rating))
</script>
<template>
<p v-if="movies.length === 0">暂无电影</p>
<ul v-else>
<li v-for="movie in sorted" :key="movie.id">
<span v-if="movie.rating >= 8.5" class="badge">高分</span>
{{ movie.title }} —— {{ movie.rating }}
</li>
</ul>
</template>
第 3 题的关键:换的是「数据字段和业务规则」,不变的是「v-if 判空 + v-for 渲染 + :key 用主键 + 角标用 v-if」这套骨架。另外注意 sort() 是变更方法,别直接对原数组下手。