Skip to content

路由传递参数(重点)与二级路由(Lesson10 3/3) ​

本页对应 Lesson10,共 5 段视频,约 1 小时 37 分。

学习目标 ​

  • 能在路由配置中使用 children 定义二级路由,并在父级组件中通过 <router-view> 正确渲染子路由内容。
  • 能实现二级路由的跳转与传参,包括完整路径、相对路径、命名路由、对象传参,并能通过 $route.params 读取参数。
  • 能按“无导航页面作为一级路由、需导航页面作为地址为 / 的二级路由”的结构搭建卖座基本路由,并配置默认重定向。
  • 能实现“正在热映”和“即将上映”的切换、列表渲染及点击电影跳转详情页,并根据路由参数请求对应详情数据。
  • 能通过路由 meta 配置控制导航链接的显示与隐藏,并验证详情页等无导航页面不显示导航。
  • 能使用过滤器或映射方法处理接口数据中的演员列表和秒级时间戳,完成上映日期等内容的格式化渲染。

知识点 ​

二级路由:先把“谁里面再出导航”想清楚 ​

为什么需要:一级路由只能在 APP 里切换大页面;当“新闻列表”里还要有“体育/娱乐/财经”,或卖座里“正在热映/即将上映”还要共用一块导航时,就需要子路由。转写稿里反复强调:二级路由也叫子路由,概念和之前学的路由一致。

怎么想:先找“父级路由组件”,再在它里面放 router-view。一级路由的 router-view 在 APP 里;二级路由的 router-view 要放到父级组件里,否则子组件没有渲染位置。

怎么做:在父路由配置里加 children,子路由 path 一般不写开头的 /,它会自动拼到父路径后面。示意代码按转写稿整理:

js
// router/index.js
const routes = [
  {
    path: '/news',
    component: NewsList,
    children: [
      { path: 'sports', name: 'sports', component: Sports },
      { path: 'ent', name: 'ent', component: Ent },
      { path: 'finance', name: 'finance', component: Finance }
    ]
  }
]
html
<!-- NewsList.vue -->
<router-link to="/news/sports">体育新闻</router-link>
<router-link to="/news/ent">娱乐新闻</router-link>
<router-link to="/news/finance">财经新闻</router-link>
<router-view></router-view>

易错点:子路由 path 写成 /sports 会按根路径理解;父组件里忘写 router-view,地址变了但内容不出来;router-link 地址要写完整或正确使用相对地址,转写稿明确说相对地址“可行但不建议”,容易拼错层级。

默认选中:重定向或同地址加 exact ​

为什么需要:点“新闻列表”进入父路由时,下面没有默认子项,用户看不到内容,也不知道当前选中谁。

怎么想:转写稿给了两种方案。方案一:让默认子路由地址与父路由相同,并配合 exact 避免所有子链接都高亮。方案二:父路由重定向到默认子路由。

怎么做:

js
// 方案二:重定向
{ path: '/news', redirect: '/news/sports' }
html
<!-- 方案一思路:默认项地址与父级相同,并加 exact -->
<router-link to="/news" exact>体育新闻</router-link>

易错点:不加 exact 时,/news 会模糊匹配到多个子项,导致全都高亮;重定向后地址会变成默认子路由地址,这是正常现象。

详情页传参:动态路由 :filmId 与 $route.params ​

为什么需要:卖座列表点某部电影,详情页要知道“点的是哪一部”,所以要把 ID 通过路由传过去。

怎么想:转写稿里详情地址形如 /film/6445,路由配置用动态段接住,组件里用 $route.params 取。点击时用 router-link 的 to 拼接 ID。

怎么做:

js
{ path: '/film/:filmId', component: Detail }
html
<router-link :to="'/film/' + item.filmId">详情</router-link>
js
// Detail.vue
this.filmId = this.$route.params.filmId

易错点:转写稿调试时出现过“详情空白”,先查地址是 /film 还是 /film/xxx,再查路由参数名和 $route.params 里的名字是否一致;详情接口的请求头也可能和列表接口不同,不能直接照搬。列表渲染还出现过图片字段用 poster、主演是数组需 map 或过滤器拼接的问题,说明“能拿到数据”不等于“字段路径写对”。

隐藏导航:meta 判断与二级路由设计法 ​

为什么需要:正在热映、即将上映有导航;点详情后不要导航。转写稿先用 v-if 判断路由名,再改成 meta 标记,最后提出更高级的做法:把“没有导航的详情/登录”作为一级路由,把“需要导航的正在热映/即将上映”作为某个一级路由下的二级路由。

怎么想:APP 只挂载一次,切换路由时执行 updated,说明视图会更新,可以在 APP 里根据当前路由决定导航显示。用 $route.meta 比写死数组更稳。

怎么做:

js
{ path: '/film/:filmId', component: Detail, meta: { isHide: true } }
html
<nav v-if="!$route.meta.isHide">...</nav>

二级路由设计法示意:

js
{
  path: '/index',
  component: Index,
  children: [
    { path: 'noPlay', component: NoPlay },
    { path: 'comingSoon', component: ComingSoon }
  ]
}

易错点:meta.isHide 为 true 表示隐藏,判断时要取反;不要每加一个页面就改多处 v-if,转写稿建议把是否隐藏收敛到路由配置里。children 下的子路由要在父组件 Index 的 router-view 中渲染,不要误放到 APP 的 router-view 里。

二级路由是什么:先理解“子路由”的匹配顺序 ​

为什么需要:一级路由只能解决“首页、新闻、商品、我的”这种大页面切换;但新闻列表下面还要分体育、娱乐、财经,卖座下面还要分正在热映、即将上映,这时就需要二级路由,也叫子路由。

怎么想:二级路由不是独立存在的,必须先匹配到一级路由,再在一级路由下面匹配子路由。例如 /news/sport,先找到 /news 对应的 NewsList,再在它下面找 sport。因此一级组件里必须留一个渲染位置。

怎么做:在 router/index.js 中给一级路由加 children:

js
{
  path: '/news',
  component: NewsList,
  children: [
    { path: 'sport', component: Sport },
    { path: 'ent', component: Ent },
    { path: 'finance', component: Finance }
  ]
}

然后在 NewsList.vue 里写导航和二级出口:

vue
<template>
  <div>
    <router-link to="/news/sport">体育新闻</router-link>
    <router-link to="/news/ent">娱乐新闻</router-link>
    <router-link to="/news/finance">财经新闻</router-link>
    <router-view></router-view>
  </div>
</template>

易错点:子路由 path 不要随手写成 /sport,斜线开头会被当成根级地址;二级路由的 router-view 要放在一级组件里,不要只放在 App.vue 里,否则二级内容没有出口。转写稿里点击新闻后下面不显示,就是因为还没在一级组件里放渲染位置。

默认子路由与重定向:解决“点新闻后下面空白” ​

为什么需要:访问 /news 时,三个子路由都不匹配,页面会只显示一级导航,下面空白。需要默认进入体育新闻或做重定向。

怎么想:转写稿给了两种方案。方案一是让默认子路由地址与上一级相同,并处理高亮精确匹配;方案二更直观:访问 /news 时重定向到 /news/sport。

怎么做:用 redirect:

js
{
  path: '/news',
  component: NewsList,
  redirect: '/news/sport',
  children: [
    { path: 'sport', component: Sport },
    { path: 'ent', component: Ent },
    { path: 'finance', component: Finance }
  ]
}

易错点:重定向要写在父级路由上;如果父级链接一直高亮,转写稿里用 exact 做精确匹配,实际写法要按你使用的 Vue Router 版本确认。默认子路由不要和别的子路由地址冲突。

地址写法:完整路径、相对路径、命名路由 ​

为什么需要:to 写不好,点击后会跳到错误地址。转写稿专门试了完整地址、相对地址和对象写法。

怎么想:完整地址最直观;相对地址相对当前路由,容易多一层或少一层;命名路由最稳,改路径时不用到处改链接。

怎么做:

vue
<router-link to="/news/sport">体育新闻</router-link>
<router-link to="sport">体育新闻</router-link>
<router-link :to="{ name: 'sport' }">体育新闻</router-link>

对应路由加 name:

js
{ path: 'sport', name: 'sport', component: Sport }

易错点:转写稿里相对地址能试出效果,但不建议;斜线代表根目录,去掉斜线可能变成当前目录下的相对地址。命名路由要保证 name 不重复。

卖座案例:用层级设计隐藏详情页导航 ​

为什么需要:正在热映、即将上映有顶部导航;点进电影详情后不要导航。转写稿先试了 v-if 判断路由名或 meta,最后推荐用路由层级解决。

怎么想:把“没有导航的详情页”作为一级路由;把“需要导航的正在热映、即将上映”作为 / 下的二级路由。这样详情页直接在 App.vue 的 router-view 渲染,不经过带导航的 Index 组件。

怎么做:

js
const routes = [
  { path: '/film/:filmId', component: FilmDetail },
  {
    path: '/',
    component: Index,
    redirect: '/now-playing',
    children: [
      { path: 'now-playing', component: NowPlaying },
      { path: 'coming-soon', component: ComingSoon }
    ]
  }
]

Index.vue 里放导航和二级出口:

vue
<template>
  <div>
    <router-link to="/now-playing">正在热映</router-link>
    <router-link to="/coming-soon">即将上映</router-link>
    <router-view></router-view>
  </div>
</template>

易错点:详情路由不要误放进 children,否则它会继承 Index 的导航;子路由 path 不要再写 /;redirect 要指向真实存在的子路由。转写稿反复调试“点详情还有导航”,本质就是层级放错了。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

代码示例 ​

示例一:二级路由(子路由)+ 默认子路由 ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <title>二级路由(子路由)</title>
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue-router.js"></script>
  <style>
    body { font-family: sans-serif; }
    .nav a, .sub-nav a { margin-right: 12px; text-decoration: none; color: #333; }
    .router-link-active { color: red; }
    .sub-nav { padding: 8px 0; border-bottom: 1px dashed #ccc; }
  </style>
</head>
<body>
  <div id="app">
    <!-- 一级路由的导航 -->
    <div class="nav">
      <router-link to="/home" exact>首页</router-link>
      <router-link to="/news">新闻列表</router-link>
      <router-link to="/goods">商品列表</router-link>
    </div>
    <hr />
    <!-- 一级路由的出口 -->
    <router-view></router-view>
  </div>

  <script>
    const Home  = { template: '<h3>首页</h3>' };
    const Goods = { template: '<h3>商品列表</h3>' };

    // 一级路由组件:内部再放一个 router-view 作为“二级路由”的出口
    const NewsList = {
      template: `
        <div>
          <div class="sub-nav">
            <router-link to="/news/sport">体育新闻</router-link>
            <router-link to="/news/ent">娱乐新闻</router-link>
            <router-link to="/news/finance">财经新闻</router-link>
          </div>
          <router-view></router-view>
        </div>
      `
    };

    const Sport   = { template: '<h3>体育新闻</h3>' };
    const Ent     = { template: '<h3>娱乐新闻</h3>' };
    const Finance = { template: '<h3>财经新闻</h3>' };

    const router = new VueRouter({
      routes: [
        { path: '/', redirect: '/home' },
        { path: '/home', component: Home },
        {
          path: '/news',
          component: NewsList,
          // 方案二:访问 /news 时重定向到默认子路由,解决“没有默认选中项”
          redirect: '/news/sport',
          children: [
            // 子路由 path 不带斜线,表示相对于父路由
            { path: 'sport',   component: Sport },
            { path: 'ent',     component: Ent },
            { path: 'finance', component: Finance }
          ]
        },
        { path: '/goods', component: Goods }
      ]
    });

    new Vue({ el: '#app', router });
  </script>
</body>
</html>

关键行与易错点

  • children: [...] 就是二级路由(子路由)的配置入口,它必须挂在某个一级路由配置项下面。
  • 子路由的 path 写成 'sport'(不带斜线)。加了斜线 /sport 就变成根目录下的路由,不再是 /news/sport。
  • 二级路由要显示出来,一级路由组件 NewsList 的模板里必须有 <router-view>,否则地址变了页面没反应。
  • 解决“进入 /news 不显示任何子页面”:用 redirect: '/news/sport' 重定向(转写稿里的方案二);方案一是把子路由 path 写成 '' 并配 exact,但容易和兄弟路由互相“高亮”,不如重定向干净。
  • 转写稿提到的另一个坑:子组件如果写了 scoped 样式,父级导航样式不会作用进去,router-link-active 高亮会“失效”——把公共样式放到全局,或去掉 scoped。

示例二:路由传递参数(动态路由 + 命名路由) ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <title>路由传递参数</title>
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue-router.js"></script>
  <style>
    body { font-family: sans-serif; }
    .nav a { margin-right: 12px; }
    li { margin: 6px 0; }
  </style>
</head>
<body>
  <div id="app">
    <div class="nav">
      <router-link to="/nowplaying">正在热映</router-link>
      <router-link to="/comingsoon">即将上映</router-link>
    </div>
    <hr />
    <router-view></router-view>
  </div>

  <script>
    const films = [
      { id: 6445, name: '长安三万里' },
      { id: 6446, name: '封神第一部' }
    ];

    const NowPlaying = {
      data() {
        return { films: films };
      },
      template: `
        <div>
          <h3>正在热映</h3>
          <ul>
            <li v-for="item in films" :key="item.id">
              <!-- 写法一:字符串拼接 -->
              <router-link :to="'/film/' + item.id">{{ item.name }}</router-link>
              &nbsp;|&nbsp;
              <!-- 写法二:对象写法(推荐,后面再带 query/params 都方便) -->
              <router-link :to="{ name: 'filmDetail', params: { id: item.id } }">
                对象写法看 {{ item.name }}
              </router-link>
            </li>
          </ul>
        </div>
      `
    };

    const ComingSoon = { template: '<h3>即将上映</h3>' };

    const FilmDetail = {
      template: `
        <div>
          <h3>电影详情</h3>
          <p>模板里取:{{ $route.params.id }}</p>
          <p>JS 里取:{{ filmId }}</p>
        </div>
      `,
      computed: {
        // 组件被复用时(6445 → 6446)也能拿到最新参数
        filmId() {
          return this.$route.params.id;
        }
      }
    };

    const router = new VueRouter({
      routes: [
        { path: '/', redirect: '/nowplaying' },
        { path: '/nowplaying', component: NowPlaying },
        { path: '/comingsoon', component: ComingSoon },
        // 动态路由参数:冒号后面的 id 会进入 $route.params
        { path: '/film/:id', name: 'filmDetail', component: FilmDetail }
      ]
    });

    new Vue({ el: '#app', router });
  </script>
</body>
</html>

关键行与易错点

  • 路由配置里 :id 是占位符,跳转时 /film/6445 的 6445 就进了 this.$route.params.id。
  • :to="'/film/' + item.id" 是字符串拼接写法;:to="{ name: 'filmDetail', params: { id } }" 是命名路由写法——对象写法必须给路由配 name,否则找不到。
  • 组件里取值用 this.$route.params.id($route 是“当前路由信息”),不是 $router($router 是路由实例,用来 push)。
  • 易错点:从 /film/6445 跳到 /film/6446 时组件会被复用,created 不会重新执行,所以要在 computed(或 watch: { '$route'(){} })里取参数,不要只在 created 里存一次。
  • 顺序易错点:/film/nowplaying 这类静态地址要写在 /film/:id 前面,否则会被当成 id = nowplaying 匹配掉。

示例三:用“一级 / 二级路由结构”控制导航显示与隐藏 ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <title>隐藏导航(一级/二级路由结构方案)</title>
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue-router.js"></script>
  <style>
    body { font-family: sans-serif; }
    .nav a { margin-right: 12px; text-decoration: none; color: #333; }
    .router-link-active { color: red; }
    li { margin: 6px 0; }
  </style>
</head>
<body>
  <div id="app">
    <!-- 只放一个出口:一级路由(详情页)直接在这里渲染,天然没有导航 -->
    <router-view></router-view>
  </div>

  <script>
    // 【一级路由】带导航的那一层:内部再开一个 router-view 给二级路由用
    const Index = {
      template: `
        <div>
          <div class="nav">
            <router-link to="/index/nowplaying">正在热映</router-link>
            <router-link to="/index/comingsoon">即将上映</router-link>
          </div>
          <hr />
          <router-view></router-view>
        </div>
      `
    };

    const NowPlaying = {
      template: `
        <div>
          <h3>正在热映</h3>
          <ul>
            <li><router-link to="/film/6445">长安三万里</router-link></li>
            <li><router-link to="/film/6446">封神第一部</router-link></li>
          </ul>
        </div>
      `
    };

    const ComingSoon = {
      template: `
        <div>
          <h3>即将上映</h3>
          <ul>
            <li><router-link to="/film/7001">某部待映影片</router-link></li>
          </ul>
        </div>
      `
    };

    // 【一级路由】没有导航:直接渲染在 App 的 router-view 里
    const FilmDetail = {
      template: '<h3>电影详情,id = {{ $route.params.id }}</h3>'
    };

    const router = new VueRouter({
      routes: [
        { path: '/', redirect: '/index/nowplaying' },
        {
          // 需要导航的组件,作为“地址为 /index 的一级路由”的二级路由
          path: '/index',
          component: Index,
          redirect: '/index/nowplaying',
          children: [
            { path: 'nowplaying', component: NowPlaying },
            { path: 'comingsoon', component: ComingSoon }
          ]
        },
        // 不需要导航的组件,作为一级路由
        { path: '/film/:id', component: FilmDetail }
      ]
    });

    new Vue({ el: '#app', router });
  </script>
</body>
</html>

关键行与易错点

  • 转写稿里的三段式设计思想:① 没有导航的组件做一级路由(/film/:id);② 一级路由地址写成 /index 这种带斜线的路径;③ 需要导航的组件做成这个一级路由的二级路由(/index/nowplaying、/index/comingsoon)。
  • 详情页 /film/6445 匹配的是一级路由,渲染在 #app 的 <router-view> 里,它的模板里没有导航,所以导航自然消失——不用写任何 v-if。
  • 如果把详情页写成 /index/film/:id(挂成 Index 的 children),那它就会渲染在 Index 的 router-view 里,导航又回来了,这是最容易写错的地方。
  • 一级组件 Index 里忘了写 <router-view>,正在热映/即将上映就会“点了没反应”。
  • 备选方案(转写稿里也讲过):给路由加 meta: { isShow: true },在 App 里写 v-if="$route.meta.isShow" 控制导航——缺点是每加一个路由都要改配置,不如上面按结构划分“改一个地方就够”。

常见错误 ​

  1. 点了一级导航后,地址栏已经变成 /newsList/体育,但页面下半部分一片空白

    • 原因:父级组件(newsList)模板里只写了导航链接,没有留出二级路由的出口。
    • 修法:在父组件模板末尾补 <router-view/>,并确认子路由确实挂在它的 children 里。
  2. 进入 /newsList 时三个子导航全灰、没有任何默认选中项

    • 原因:当前地址与任何一条子路由的 path 都不匹配(子路由都是 /newsList/体育 这类完整路径)。
    • 修法:二选一——给父级加一条 redirect 指向默认子路由;或让默认子路由的 path 写成空串/与父级同路径并加 exact,避免它把其他子路由也“吃掉”。
  3. 一级导航“新闻列表”在点击任意子项后依然高亮,甚至多个链接同时高亮

    • 原因:父级 router-link 的 to 与子路由前缀相同,模糊匹配把子路由也算作父级激活;或者子路由 path 误写成以 / 开头的绝对路径,导致匹配范围扩大。
    • 修法:父级链接加 exact;子路由 path 去掉前导斜线,写成相对形式。
  4. 在子路由里写 to="体育",点击后跳到 /体育 而不是 /newsList/体育

    • 原因:to 不加前导斜线时按“相对当前路由”解析,层级被抬高了一级。
    • 修法:写完整绝对路径 /newsList/体育;或改用 :to="{name:'体育'}",用 name 跳转不受路径拼接影响。
  5. 用 name 跳转控制台报 No match for {name: "xxx"}

    • 原因:路由配置里没写 name,或 name 值与模板中的字符串大小写/拼写不一致。
    • 修法:逐条给路由记录补 name,模板统一用对象形式 :to="{name:'体育'}",改路径时不必再改模板。
  6. 详情页 /film/645 能显示,但 this.$route.params.id 是 undefined

    • 原因:path 里的动态段名(如 :filmId)与取值时用的键名(params.id)不一致。
    • 修法:命名一处定义、处处一致;取值前先 console.log(this.$route.params) 确认键名。
  7. 点进电影详情后,顶部“正在热映 / 即将上映”导航还在

    • 原因:详情被配成了带导航父路由的子路由;或者用 v-if 硬编码判断 $route.name === 'A' || 'B',每加一个页面就要改两处。
    • 修法:把无导航页面提升为一级路由、把有导航的页面统一挂到 / 的 children 下;若沿用显隐方案,则在 meta 上打标记统一判断,避免写死名单。
  8. 列表页正常,详情接口却 403 或返回空

    • 原因:详情接口要求的请求头与列表接口不同(多一项或少一项),直接复用了列表的 header。
    • 修法:对照 Network 面板逐项比对请求头,为详情单独设置;同时检查 URL 拼接的 id 是否为 undefined。
  9. 上映日期渲染成一长串数字,主演渲染成一堆 [object Object] 或带逗号

    • 原因:接口返回的是秒级时间戳和对象数组,直接插值输出。
    • 修法:时间戳用过滤器 new Date(v * 1000) 再拼“x月x日 周x”;数组先 map 取 name 再 join(' '),或封装成过滤器复用。
  10. 父组件加了 scoped 后,子路由里的导航样式全部失效

    • 原因:scoped 给样式加了作用域属性,子组件节点匹配不到。
    • 修法:把这段样式移到公共样式或父组件的非 scoped 区块。

练习 ​

  1. 在 newsList 下配置体育/娱乐/财经三条子路由,要求:直接访问 /newsList 时自动落到体育新闻,一级导航“新闻列表”在任意子页都精确高亮且不误伤其他链接。

  2. 把上面三条子路由的跳转全部改为 name 方式,并在体育新闻子组件里打印出当前路由的 name、path、params,验证跳转与取值一致。

  3. 改造卖座案例:把“正在热映 / 即将上映”做成 / 的二级路由(导航放在承载它们的父组件里),把电影详情做成一级路由,验证进入详情后顶部导航消失、返回后导航恢复。

  4. 编写一个日期过滤器,把接口返回的秒级时间戳格式化为“x月x日 周x”,并应用到即将上映列表;要求月份和日期不足两位时补零。

  5. 在正在热映/即将上映列表中,把 filmId 拼进 router-link 的 to 跳转到详情页,详情组件根据 id 请求数据并渲染片名、海报、简介;要求切换不同电影时详情内容随之更新。

原视频对照 ​

#视频时长
1二级路由的基本用法24:18
2搭建卖座基本路由11:00
3正在热映与即将上映22:58
4隐藏导航链接20:53
5二级路由的应用18:12

小结 ​

  • 二级路由能不能显示,第一件事永远是检查父组件里有没有 <router-view/>,而不是先怀疑配置。
  • 子路由 path 用相对写法、一级链接加 exact,能避免绝大多数“高亮错乱”和“跳错层级”问题。
  • 相对路径虽然能跑通,但一旦嵌套加深就极易出错;实际项目优先用 name + 对象形式跳转,路径改动不影响模板。
  • “默认选中某个子项”本质是让地址与某条子路由匹配,redirect 是比改 path 更清晰的做法。
  • 隐藏导航的两种方案里,meta + v-if 属于补丁式写法,每加页面要改多处;“无导航的做一级、有导航的挂到 / 下”属于结构式写法,扩展时只动路由表,更推荐。
  • 路由传参和接口请求是两件事:先确认 params 拿到手,再拿它去请求,否则报错会同时出现在两层,排查成本翻倍。
  • 详情页这类接口往往与列表接口请求头不同,直接复用是常见坑,接入前先比对一次 Network 面板。
  • 组件内展示数组和时间戳时,统一走 map/过滤器处理,别在模板里堆表达式——后期复用和改格式都省事。