Lavender's Blog

浩瀚众星,皆降为尘

好故事,值得慢一点

将 Shoka 默认的线性归档、分类和标签列表改造成统一的卡片页面,加入数据概览、层级分类、最近文章、日期卡片、面包屑和移动端适配。

#前言

Shoka 默认归档页面使用时间轴展示文章,结构清晰,但当文章、分类和标签逐渐增多后,页面会显得较长,也不容易快速浏览。本次改造把归档、分类和标签统一为一套视觉系统,同时保留 Hexo 原有分页、分类层级和 PJAX 行为。

改造后的页面分为四类:

  • /archives/ :按年份分组的文章卡片;
  • /categories/分类名/ :当前分类下的文章卡片;
  • /tags/标签名/ :当前标签下的文章卡片;
  • /categories//tags/ :分类索引卡片与标签索引卡片。

#开始前准备

这篇教程会修改主题模板、主题脚本和 Stylus 样式。第一次接触 Hexo 也不用担心,按下面的顺序操作即可。

  1. 找到博客根目录。能够看到 _config.ymlpackage.jsonsourcethemes 的文件夹,就是博客根目录。
  2. 复制一份 themes/shoka 作为备份,或者先提交一次 Git。这样即使粘贴错误也可以快速恢复。
  3. 在博客根目录打开终端,执行 npm install ,确保现有依赖已经安装。
  4. 后文出现的所有路径都从博客根目录开始计算,不要修改 public 目录。 public 是 Hexo 每次生成时自动创建的成品目录,直接修改会被覆盖。
  5. 每完成一个大步骤,都可以执行一次 hexo generate --bail 。如果某一步写错,终端会更容易指出具体文件。

本文本身也由 Hexo 生成。为了防止文章中的 Nunjucks 示例被当成真正的主题指令执行,教程源文件对花括号做了转义;网页中显示和复制出来的代码仍然是正常的 Nunjucks 语法。真正修改 *.njk 文件时,不要手动把花括号改成 HTML 实体。

#文件结构

themes/shoka/layout/_macro/collection-card.njk
themes/shoka/layout/archive.njk
themes/shoka/layout/category.njk
themes/shoka/layout/tag.njk
themes/shoka/layout/page.njk
themes/shoka/scripts/helpers/collection.js
themes/shoka/scripts/helpers/list_categories.js
themes/shoka/scripts/generaters/pages.js
themes/shoka/scripts/generaters/archive.js
themes/shoka/source/css/_common/components/pages/collapse.styl

#文件变更清单

下面先把文件级变化说清楚,再进入具体操作。这样即使是第一次修改主题,也能知道每一步应该新建文件、修改原文件,还是只做检查。

操作文件具体变化
新增themes/shoka/layout/_macro/collection-card.njk新建可复用的文章卡片宏,统一日期、标题、分类、字数、阅读时间和跳转箭头。
新增themes/shoka/scripts/helpers/collection.js自动注册完整集合计数帮助函数 _collection_total ,让分类、标签、年度和月度分页始终显示完整总数。
修改themes/shoka/layout/archive.njk删除原来的线性归档列表结构,改成概览卡、面包屑、年份分组和双列文章卡片;保留分页。
修改themes/shoka/layout/category.njk删除原来的分类时间轴列表,改成分类概览、面包屑和公共文章卡片。
修改themes/shoka/layout/tag.njk删除原来的标签时间轴列表,改成标签概览、面包屑和公共文章卡片。
修改themes/shoka/layout/page.njk增加 categoriestags 两种索引页面分支;普通独立页和 404 页面逻辑继续保留。
修改themes/shoka/scripts/helpers/list_categories.js重写分类列表辅助函数,加入三级分类、文章数量、最近两篇文章、空状态和卡片序号。
修改themes/shoka/scripts/generaters/pages.js新增 /categories//tags/ 索引路由;原有 404 路由继续保留。
修改themes/shoka/scripts/generaters/archive.js让总归档页不分页,以便一次完成所有年份分组;年度和月度页面仍按配置分页。
修改themes/shoka/source/css/_common/components/pages/collapse.styl在原有折叠组件样式后加入概览卡、文章卡、分类卡、标签卡、面包屑、暗色主题及移动端样式。
检查,通常无需修改themes/shoka/source/css/_common/components/pages/pages.styl确认已经导入 collapse.styl ;当前项目已有 @import 'collapse';

本次没有删除任何文件,只删除或替换了几个模板中的旧线性列表代码。不要删除 archive.njkcategory.njktag.njk 本身,也不要删除原有分页、普通页面和 404 分支。

#设计思路

为了避免四个页面各写一套样式,先抽出三组公共组件:

  1. collection-hero :页面顶部的数据概览卡;
  2. collection-breadcrumb :统一面包屑导航;
  3. collection-card :归档、分类和标签详情页共用的文章卡片。

分类索引和标签索引在此基础上分别使用 taxonomy-cardtag-index-card

#实现

#准备完整集合计数帮助函数

先新建 themes/shoka/scripts/helpers/collection.js ,将文末 “辅助函数与页面生成器” 中该文件的完整代码复制进去。Hexo 会自动加载主题 scripts/helpers 下的文件,注册 _collection_total ,不需要在页面中额外加载脚本。

分类、标签、年度和月度归档启用分页后, page.posts.length 只是当前页的篇数, page.total 则是页数,都不能当作文章总数。后面的三个详情模板在 block content 内先设置 collection_total ,概览文案和数字徽章使用它;正文列表仍遍历 page.posts ,分页逻辑保持不变:

{% set collection_total = _collection_total(page) %}
<p>{{ _p('counter.archive_posts', collection_total) }}</p>
<strong>{{ collection_total }}</strong>

合并下文示例时,将概览计数中的 page.posts.length 换成 collection_total ;文末当前模板代码卡片已包含这项调整。不要遗漏新增帮助函数,否则模板中的 _collection_total(page) 无法调用。

#第 1 步:创建公共文章卡片

公共卡片只编写一次,归档、分类和标签详情页都会调用它。

  1. 打开 themes/shoka/layout/_macro 文件夹;
  2. 新建 collection-card.njk ,注意扩展名不是 .html
  3. 把下面的代码完整粘贴进去并保存为 UTF-8 编码。
{% macro render(post, show_categories = true) %}
<article class="collection-card" itemscope itemtype="http://schema.org/Article">
  <div class="collection-card__date" aria-hidden="true">
    <span>{{ date(post.date, 'YYYY.MM') }}</span>
    <strong>{{ date(post.date, 'DD') }}</strong>
  </div>
  <div class="collection-card__content">
    {%- if show_categories and post.categories and post.categories.length %}
    <div class="collection-card__categories">
      {%- for cat in post.categories.toArray() %}
      <a href="{{ url_for(cat.path) }}">
        <i class="ic i-flag"></i>{{ cat.name }}
      </a>
      {%- endfor %}
    </div>
    {%- endif %}
    <h3 class="collection-card__title">
      <a href="{{ url_for(post.path) }}" itemprop="url">
        <span itemprop="name">{{ post.title or __('post.untitled') }}</span>
      </a>
    </h3>
    <div class="collection-card__meta">
      <time datetime="{{ moment(post.date).format() }}">
        <i class="ic i-calendar"></i>{{ date(post.date, config.date_format) }}
      </time>
      {%- if theme.post.count %}
      <span><i class="ic i-pen"></i>{{ symbolsCount(post) }} {{ __('symbols_count_time.word') }}</span>
      <span><i class="ic i-clock"></i>{{ symbolsTime(post, config.symbols_count_time.awl, config.symbols_count_time.wpm, __('symbols_count_time.time_minutes'), __('symbols_count_time.time_hours')) }}</span>
      {%- endif %}
    </div>
  </div>
  <a class="collection-card__arrow"
     href="{{ url_for(post.path) }}"
     aria-label="{{ post.title or __('post.untitled') }}">
    <i class="ic i-angle-right"></i>
  </a>
</article>
{% endmacro %}

参数 show_categories 用于控制是否显示分类。进入某个分类详情页时,可以隐藏重复的分类名称。

保存后不要直接双击这个文件预览。它是一个供其他模板调用的宏,必须经过 Hexo 生成后才会变成 HTML。

#第 2 步:改造归档页面

打开 themes/shoka/layout/archive.njk 并先备份原内容。这个文件负责 /archives/ 、年度归档和月度归档页面。

文件开头保留 extends ,然后紧接着导入刚才创建的卡片宏:

{% extends '_partials/layout.njk' %}
{% import '_macro/collection-card.njk' as collection with context %}

在模板的 content 区域中加入概览卡和面包屑。若原文件已经有 block content ,把下面内容放在该区块内部,不要重复写两组同名区块:

<div class="collection-page archive-page wrap">
  <section class="collection-hero">
    <div class="collection-hero__icon"><i class="ic i-archive"></i></div>
    <div class="collection-hero__copy">
      <span class="collection-hero__eyebrow">CHRONICLE</span>
      <h2>
        {%- if is_month() %}
          {{ page.year }}.{{ page.month }}
        {%- elif is_year() %}
          {{ page.year }}
        {%- else %}
          {{ __('title.archive') }}
        {%- endif %}
      </h2>
      <p>{{ _p('counter.archive_posts', page.posts.length) }}</p>
    </div>
    <div class="collection-hero__stat">
      <strong>{{ page.posts.length }}</strong>
      <span>{{ __('menu.posts') }}</span>
    </div>
  </section>
  <nav class="collection-breadcrumb" aria-label="breadcrumb">
    <a href="{{ url_for(site.path) }}">
      <i class="ic i-home"></i>{{ __('menu.home') }}
    </a>
    <i class="ic i-angle-right"></i>
    <span>{{ __('title.archive') }}</span>
  </nav>
</div>

紧接着加入文章列表。下面的代码会读取 page.posts ,遇到新年份时自动建立一组卡片:

<div class="archive-collections">
  {%- set current_year = '' %}
  {%- for post in page.posts.toArray() %}
    {%- set year = date(post.date, 'YYYY') %}
    {%- if year !== current_year %}
      {%- if current_year %}
          </div>
        </section>
      {%- endif %}
      <section class="archive-year" id="archive-{{ year }}">
        <header class="archive-year__header">
          <a class="archive-year__number"
             href="{{ url_for(config.archive_dir + '/' + year) }}">{{ year }}</a>
          <span class="archive-year__line"></span>
          <a class="archive-year__link"
             href="{{ url_for(config.archive_dir + '/' + year) }}">
            <i class="ic i-arrow-circle-right"></i>
          </a>
        </header>
        <div class="collection-grid">
      {%- set current_year = year %}
    {%- endif %}
    {{ collection.render(post) }}
  {%- endfor %}
  {%- if current_year %}
        </div>
      </section>
  {%- endif %}
</div>

文章列表后面保留主题原有分页,并确保最外层的 block content 最后仍有对应的 endblock

{% include '_partials/pagination.njk' %}

#第 3 步:让总归档页显示完整年份

如果总归档页仍被分成多页,就需要调整生成器。打开 themes/shoka/scripts/generaters/archive.js ,搜索 pagination(path, posts ,把该调用中的 perPage 改成下面的判断方式。不要删除函数中其余选项:

function generate (path, posts, options) {
  options = options || {}
  options.archive = true
  result = result.concat(pagination(path, posts, {
    perPage: path === archiveDir ? 0 : perPage,
    layout: ['archive', 'index'],
    format: paginationDir + '/%d/',
    data: options
  }))
}

这样 /archives/ 可以完整按年份分组,而 /archives/2026/ 等页面仍可以分页。

这里的 path === archiveDir 只匹配总归档路径;值为 0 表示总归档页不分页,并不会关闭年度、月度页面的分页。

#第 4 步:改造分类和标签详情页

先打开 themes/shoka/layout/category.njk 。与归档页相同,文件顶部导入 collection-card.njk ,再把原来的线性文章列表替换成下面的卡片结构:

{% extends '_partials/layout.njk' %}
{% import '_macro/collection-card.njk' as collection with context %}
{% block content %}
<div class="collection-page category-page wrap">
  <section class="collection-hero collection-hero--category">
    <div class="collection-hero__icon"><i class="ic i-flag"></i></div>
    <div class="collection-hero__copy">
      <span class="collection-hero__eyebrow">COLLECTION</span>
      <h2>{{ page.title }}</h2>
      <p>{{ _p('counter.archive_posts', page.posts.length) }}</p>
    </div>
    <div class="collection-hero__stat">
      <strong>{{ page.posts.length }}</strong>
      <span>{{ __('menu.posts') }}</span>
    </div>
  </section>
  <nav class="collection-breadcrumb" aria-label="breadcrumb">
    <a href="{{ url_for(site.path) }}"><i class="ic i-home"></i>{{ __('menu.home') }}</a>
    <i class="ic i-angle-right"></i>
    <a href="{{ url_for(config.category_dir) }}">{{ __('title.all') }}{{ __('title.category') }}</a>
    <i class="ic i-angle-right"></i>
    <span>{{ page.title }}</span>
  </nav>
  <div class="collection-grid category-posts-grid">
    {%- for post in page.posts.toArray() %}
      {{ collection.render(post, false) }}
    {%- endfor %}
  </div>
</div>
{% include '_partials/pagination.njk' %}
{% endblock %}

然后打开 themes/shoka/layout/tag.njk 。它的外层结构可以参考 category.njk ,但标题使用 page.tag ,文章卡片保留分类信息。把标签页的顶部概览和列表改成下面的内容:

<section class="collection-hero collection-hero--tag">
  <div class="collection-hero__icon"><i class="ic i-tag"></i></div>
  <div class="collection-hero__copy">
    <span class="collection-hero__eyebrow">TAG COLLECTION</span>
    <h2>{{ page.tag }}</h2>
    <p>{{ _p('counter.archive_posts', page.posts.length) }}</p>
  </div>
</section>
<div class="collection-grid tag-posts-grid">
  {%- for post in page.posts.toArray() %}
    {{ collection.render(post) }}
  {%- endfor %}
</div>

上面的标签页代码是关键片段,不是完整文件。 tag.njk 仍需保留文件开头的 extends 、宏导入、 block content ,以及文件末尾的分页和 endblock 。最稳妥的做法是复制已经完成的 category.njk ,再把分类图标、文案、 page.title 和卡片参数逐项改成标签版本。

#第 5 步:创建分类和标签索引路由

详情页和索引页不是同一个页面。 /categories/JavaScript/ 由 Hexo 的分类生成器产生,而 /categories/ 需要主题单独注册。

打开 themes/shoka/scripts/generaters/pages.js

  • 如果文件中已经注册了名为 pages 的 generator,就在原函数返回的数组中加入分类和标签两项;
  • 如果没有,再使用下面的完整代码;
  • 不要同时保留两个 hexo.extend.generator.register('pages', ...) ,否则会重复生成路由。
'use strict'
hexo.extend.generator.register('pages', function () {
  const config = hexo.config
  return [{
    path: config.category_dir + '/index.html',
    data: { type: 'categories' },
    layout: ['page']
  }, {
    path: config.tag_dir + '/index.html',
    data: { type: 'tags' },
    layout: ['page']
  }, {
    path: '404.html',
    data: { type: '404' },
    layout: ['page']
  }]
})

接着打开 themes/shoka/layout/page.njk 。这个模板根据 page.type 决定显示普通独立页、分类索引或标签索引。把分类和标签判断放在普通页面正文分支之前,避免它们被当成普通 Markdown 页面。

分类索引:

{% if page.type === 'categories' %}
<div class="taxonomy-page wrap">
  <section class="collection-hero collection-hero--category">
    <div class="collection-hero__icon"><i class="ic i-flag"></i></div>
    <div class="collection-hero__copy">
      <span class="collection-hero__eyebrow">INDEX</span>
      <h2>{{ __('title.all') }}{{ __('title.category') }}</h2>
      <p>{{ _p('counter.categories', site.categories.length) }}</p>
    </div>
    <div class="collection-hero__stat">
      <strong>{{ site.categories.length }}</strong>
      <span>{{ __('menu.categories') }}</span>
    </div>
  </section>
  <div class="taxonomy-grid">
    {{ _list_categories(3) }}
  </div>
</div>
{% endif %}

标签索引:

{% if page.type === 'tags' %}
<div class="taxonomy-page tag-index-page wrap">
  <section class="collection-hero collection-hero--tag">
    <div class="collection-hero__icon"><i class="ic i-tags"></i></div>
    <div class="collection-hero__copy">
      <span class="collection-hero__eyebrow">TAG INDEX</span>
      <h2>{{ __('title.all') }}{{ __('title.tag') }}</h2>
      <p>{{ _p('counter.tag_cloud', site.tags.length) }}</p>
    </div>
  </section>
  <div class="tag-index-grid">
    {%- for tag in site.tags.toArray() %}
    <a class="tag-index-card" href="{{ url_for(tag.path) }}" title="{{ tag.name }}">
      <span class="tag-index-card__icon"><i class="ic i-tag"></i></span>
      <span class="tag-index-card__copy">
        <strong class="tag-index-card__name">{{ tag.name }}</strong>
        <small><i class="ic i-feather"></i>专题标签 · 点击浏览</small>
      </span>
      <span class="tag-index-card__count">
        <strong>{{ tag.length }}</strong><small>文章</small>
      </span>
      <span class="tag-index-card__arrow"><i class="ic i-angle-right"></i></span>
      <span class="tag-index-card__watermark" aria-hidden="true">#{{ loop.index }}</span>
    </a>
    {%- endfor %}
  </div>
</div>
{% endif %}

完成后,检查两个条件的名称必须与 pages.js 完全一致: categories 对应分类索引, tags 对应标签索引。

#第 6 步:为分类卡片加入层级和最近文章

分类索引里的 _list_categories(3) 不是 Hexo 默认函数,而是主题辅助函数。打开 themes/shoka/scripts/helpers/list_categories.js ,先备份原文件,再用下面的逻辑递归读取分类关系,并为一级分类附加最近两篇文章:

const prepareQuery = function (categories, parent) {
  const query = parent ? { parent: parent } : { parent: { $exists: false } }
  return categories.find(query).sort('name', 1).filter(function (cat) {
    return cat.length
  })
}
hexo.extend.helper.register('_list_categories', function (depth = 0) {
  const hexo = this
  const categories = hexo.site.categories
  if (!categories || !categories.length) return ''
  const hierarchicalList = function (level, parent) {
    let result = ''
    prepareQuery(categories, parent).forEach(function (cat, index) {
      let children = ''
      if (level + 1 < depth) {
        children = hierarchicalList(level + 1, cat._id)
      }
      const url = hexo.url_for(cat.path)
      const recentPosts = cat.posts
        ? cat.posts.sort('date', -1).toArray().slice(0, 2)
        : []
      const recent = recentPosts.map(function (post) {
        return `<a href="${hexo.url_for(post.path)}" title="${post.title}">
          <i class="ic i-file"></i><span>${post.title}</span>
        </a>`
      }).join('')
      if (level === 0) {
        result += `<article class="taxonomy-card" style="--taxonomy-index:${index}">
          <a class="taxonomy-card__head" href="${url}">
            <span class="taxonomy-card__icon"><i class="ic i-archive"></i></span>
            <span class="taxonomy-card__name">${cat.name}</span>
            <span class="taxonomy-card__count"><strong>${cat.length}</strong><small>POSTS</small></span>
          </a>
          <div class="taxonomy-card__insight">
            <span><i class="ic i-feather"></i>分类专题</span>
            <span>持续收录 · ${cat.length} 篇文章</span>
          </div>
          <div class="taxonomy-card__recent">
            ${recent || '<span class="taxonomy-card__empty">等待新的文章加入</span>'}
          </div>
          ${children ? `<div class="taxonomy-card__children">${children}</div>` : ''}
          <a class="taxonomy-card__more" href="${url}"><i class="ic i-angle-right"></i></a>
          <span class="taxonomy-card__watermark">${String(index + 1).padStart(2, '0')}</span>
        </article>`
      } else if (level === 1) {
        result += `<div class="taxonomy-child">
          <a class="taxonomy-child__head" href="${url}">
            <span><i class="ic i-flag"></i>${cat.name}</span>
            <small>${cat.length}</small>
          </a>
          ${children ? `<div class="taxonomy-grandchildren">${children}</div>` : ''}
        </div>`
      } else {
        result += `<a class="taxonomy-grandchild" href="${url}">${cat.name}<small>${cat.length}</small></a>`
      }
    })
    return result
  }
  return hierarchicalList(0)
})

参数 3 表示最多展示三级分类。若只想显示一级和二级,可以在 page.njk 中改成 _list_categories(2) 。最近文章数量由 slice(0, 2) 控制,改成 slice(0, 3) 就会显示三篇,但卡片高度也需要相应增加。

#第 7 步:加入公共 Stylus 样式

打开 themes/shoka/source/css/_common/components/pages/collapse.styl 。保留文件原有的 .collapse 样式,把本节代码追加到文件末尾,不要整份覆盖。

再打开同目录的 pages.styl ,确认其中存在下面这一行;当前项目已经有这一行,如果你的主题版本没有,请手动加入:

@import 'collapse';

顶部概览卡:

.collection-page,
.taxonomy-page {
  position: relative;
  .collection-hero {
    position: relative;
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    align-items: center;
    gap: 1.5rem;
    min-height: 12rem;
    margin: 1rem 0 1.25rem;
    padding: 2rem 2.25rem;
    overflow: hidden;
    border: .0625rem solid var(--grey-2);
    border-radius: 1.375rem;
    background:
      radial-gradient(circle at 88% 18%, var(--color-red-a1), transparent 28%),
      linear-gradient(135deg, var(--grey-0), var(--grey-1) 52%, var(--grey-2));
    box-shadow: 0 1.5rem 3.75rem -2rem var(--box-bg-shadow);
    +mobile() {
      grid-template-columns: auto minmax(0, 1fr);
      min-height: 10rem;
      padding: 1.5rem;
    }
  }
}

文章卡片与悬浮效果:

.collection-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1rem;
  +mobile() {
    grid-template-columns: minmax(0, 1fr);
  }
}
.collection-card {
  position: relative;
  display: grid;
  grid-template-columns: 4rem minmax(0, 1fr) 2rem;
  align-items: center;
  gap: 1rem;
  min-height: 9.25rem;
  padding: 1.25rem 1.15rem;
  overflow: hidden;
  border: .0625rem solid var(--grey-2);
  border-radius: 1.125rem;
  background: var(--grey-0);
  box-shadow: 0 1rem 2.5rem -1.65rem var(--box-bg-shadow);
  the-transition(.28s, ease-out);
  &:hover {
    border-color: var(--color-red-a3);
    box-shadow: 0 1.35rem 3rem -1.5rem var(--box-bg-shadow);
    transform: translateY(-.3rem);
    .collection-card__date {
      color: #fff;
      border-color: transparent;
      background: linear-gradient(145deg, var(--color-pink), var(--color-orange));
    }
    .collection-card__arrow {
      color: #fff;
      background: var(--primary-color);
    }
  }
}

分类索引卡片:

.taxonomy-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1.25rem;
  +mobile() {
    grid-template-columns: minmax(0, 1fr);
  }
}
.taxonomy-card {
  position: relative;
  min-height: 13rem;
  padding: 1.25rem 1.25rem 3.25rem;
  overflow: hidden;
  border: .0625rem solid var(--grey-2);
  border-radius: 1.25rem;
  background:
    radial-gradient(circle at 100% 0, var(--color-red-a1), transparent 35%),
    var(--grey-0);
  box-shadow: 0 1rem 2.5rem -1.65rem var(--box-bg-shadow);
  the-transition(.28s, ease-out);
  &:hover {
    border-color: var(--color-red-a3);
    transform: translateY(-.3rem);
  }
}

标签索引使用三列布局,并在移动端依次切换为两列和一列:

.tag-index-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 1rem;
  +tablet-mobile() {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
  +mobile-small() {
    grid-template-columns: minmax(0, 1fr);
  }
}

所有颜色尽量使用 Shoka 已有 CSS 变量,例如:

color: var(--grey-7);
background: var(--grey-0);
border-color: var(--grey-2);
box-shadow: 0 1rem 2.5rem -1.65rem var(--box-bg-shadow);

这样无需额外编写整套夜间模式颜色,主题切换时会自动更新。

#常见问题

#教程里的代码突然变成了页面标题

这是 Nunjucks 指令被 Hexo 提前执行造成的。发布教程文章时,需要对代码示例中的模板花括号转义;但写入 archive.njkcategory.njk 等真正主题文件时,必须使用正常的 {} 。本篇教程源文件已经完成转义。

# /categories//tags/ 是空白页

依次检查三处: pages.js 是否生成了 categoriestags 类型、 page.njk 是否判断了相同的类型名、 list_categories.js 是否成功注册 _list_categories 。修改后必须先执行 hexo clean 再重新生成。

#详情页正常,但总归档页缺少部分年份

检查 archive.js 中总归档路径的 perPage 是否为 0 ,并确认 archive.njk 遍历的是 page.posts.toArray() ,而不是只取固定数量。

#卡片没有样式

确认样式写入的是 themes/shoka/source/css/_common/components/pages/collapse.styl ,并确认 pages.styl 中存在 @import 'collapse'; 。不要把样式写进 public/css ,因为下次生成时会被覆盖。

#手机端卡片横向溢出

检查所有网格是否使用 minmax(0, 1fr) ,移动端是否切换为单列,同时避免给卡片设置固定宽度。修改完成后在浏览器开发者工具中选择 375px 左右的手机宽度测试。

#测试

完成后执行:

hexo clean
hexo generate --bail
hexo server

依次检查:

  1. /archives/ 、年度归档和月度归档;
  2. /categories/ 与各级分类详情页;
  3. /tags/ 与标签详情页;
  4. 分类层级、最近文章和数量是否正确;
  5. 移动端卡片是否变为单列;
  6. 日间与夜间模式下文字、背景和边框是否清晰;
  7. PJAX 切换、分页和面包屑链接是否正常。

#总结

这次改造的重点是 “复用” 而不是单独美化某一个页面。归档、分类和标签详情页共用文章卡片,分类和标签索引共用顶部概览与面包屑,因此后续修改圆角、阴影、间距或主题色时,只需要调整同一份 Stylus 样式。

#涉及改动的代码

代码卡片按属性名、函数名或带内容校验的具名片段定位,并显示当前源码行号。片段前后插行可自动重新定位;片段本身改变或位置不唯一时会停止生成,核对教程后再更新摘录,避免误引其他功能。

下面按文件集中展示本次改造的关键代码,只保留新增或修改的位置。卡片副标题会标注当前项目中的原始行号;区间之间出现的 “⋯ 未改动代码已省略 ⋯” 无需复制,也不要据此删除原文件中的其他内容。

行号会随以后修改而变化。如果你的主题版本与本文不同,请搜索卡片中的宏名称、区块名称、函数名或 CSS 选择器,再把代码合并到对应位置。

#模板文件

themes/shoka/layout/_macro/collection-card.njk改动位置:第 1–8、15–25、29–44 行 35 行 · 1.6 KB
themes/shoka/layout/_macro/collection-card.njk
{% macro render(post, show_categories = true) %}
<article class="collection-card" itemscope itemtype="http://schema.org/Article">
  <div class="collection-card__date" aria-hidden="true">
    <span>{{ date(post.date, 'YYYY.MM') }}</span>
    <strong>{{ date(post.date, 'DD') }}</strong>
  </div>
 
  <div class="collection-card__content">
⋯ 未改动代码已省略 ⋯
    {%- endif %}
 
    <h3 class="collection-card__title">
      {%- if post.link %}
        {%- set postTitleIcon = '<i class="ic i-link-alt"></i>' %}
        {%- set postText = post.title or post.link %}
        {{ _url(post.link, postText + postTitleIcon, {class: 'external', itemprop: 'url'}) }}
      {% else %}
        <a href="{{ url_for(post.path) }}" itemprop="url">
          <span itemprop="name">{{ post.title or __('post.untitled') }}</span>
        </a>
⋯ 未改动代码已省略 ⋯
    <div class="collection-card__meta">
      <time itemprop="dateCreated datePublished" datetime="{{ moment(post.date).format() }}">
        <i class="ic i-calendar"></i>{{ date(post.date, config.date_format) }}
      </time>
      {%- if theme.post.count %}
      <span><i class="ic i-pen"></i>{{ symbolsCount(post) }} {{ __('symbols_count_time.word') }}</span>
      <span><i class="ic i-clock"></i>{{ symbolsTime(post, config.symbols_count_time.awl, config.symbols_count_time.wpm, __('symbols_count_time.time_minutes'), __('symbols_count_time.time_hours')) }}</span>
      {%- endif %}
    </div>
  </div>
 
  <a class="collection-card__arrow" href="{{ url_for(post.path) }}" aria-label="{{ post.title or __('post.untitled') }}">
    <i class="ic i-angle-right"></i>
  </a>
</article>
{% endmacro %}

themes/shoka/layout/archive.njk改动位置:第 1–2、25–55、64–94 行 64 行 · 2.5 KB
themes/shoka/layout/archive.njk
{% extends '_partials/layout.njk' %}
{% import '_macro/collection-card.njk' as collection with context %}
⋯ 未改动代码已省略 ⋯
{% block content %}
  {%- set collection_total = _collection_total(page) %}
  <div class="collection-page archive-page wrap">
    <section class="collection-hero">
      <div class="collection-hero__icon"><i class="ic i-archive"></i></div>
      <div class="collection-hero__copy">
        <span class="collection-hero__eyebrow">CHRONICLE</span>
        <h2>
          {%- if is_month() %}
            {{ page.year }}.{{ page.month }}
          {%- elif is_year() %}
            {{ page.year }}
          {%- else %}
            {{ __('title.archive') }}
          {%- endif %}
        </h2>
        <p>{{ _p('counter.archive_posts', collection_total) }}</p>
      </div>
      <div class="collection-hero__stat">
        <strong>{{ collection_total }}</strong>
        <span>{{ __('menu.posts') }}</span>
      </div>
    </section>
 
    <nav class="collection-breadcrumb" aria-label="breadcrumb">
      <a href="{{ url_for(site.path) }}"><i class="ic i-home"></i>{{ __('menu.home') }}</a>
      <i class="ic i-angle-right"></i>
      {%- if is_month() %}
        <a href="{{ url_for(config.archive_dir) }}">{{ __('title.all') }}</a>
        <i class="ic i-angle-right"></i>
        <a href="{{ url_for(config.archive_dir + '/' + page.year) }}">{{ page.year }}</a>
⋯ 未改动代码已省略 ⋯
      {%- endif %}
    </nav>
 
    <div class="archive-collections">
      {%- set current_year = '' %}
      {%- for post in page.posts.toArray() %}
        {%- set year = date(post.date, 'YYYY') %}
        {%- if year !== current_year %}
          {%- if current_year %}
            </div>
          </section>
          {%- endif %}
          <section class="archive-year" id="archive-{{ year }}">
            <header class="archive-year__header">
              <a class="archive-year__number" href="{{ url_for(config.archive_dir + '/' + year) }}">{{ year }}</a>
              <span class="archive-year__line"></span>
              <a class="archive-year__link" href="{{ url_for(config.archive_dir + '/' + year) }}" aria-label="{{ _p('title.archive_in', year + __('symbol.year')) | escape }}">
                <i class="ic i-arrow-circle-right" aria-hidden="true"></i>
              </a>
            </header>
            <div class="collection-grid">
          {%- set current_year = year %}
        {%- endif %}
        {{ collection.render(post) }}
      {%- endfor %}
      {%- if current_year %}
            </div>
          </section>
      {%- endif %}
    </div>
  </div>

themes/shoka/layout/category.njk改动位置:第 1–2、10–30、35–43 行 32 行 · 1.3 KB
themes/shoka/layout/category.njk
{% extends '_partials/layout.njk' %}
{% import '_macro/collection-card.njk' as collection with context %}
⋯ 未改动代码已省略 ⋯
{% block content %}
  {%- set collection_total = _collection_total(page) %}
  <div class="collection-page category-page wrap">
    <section class="collection-hero collection-hero--category">
      <div class="collection-hero__icon"><i class="ic i-flag"></i></div>
      <div class="collection-hero__copy">
        <span class="collection-hero__eyebrow">COLLECTION</span>
        <h2>{{ page.title }}</h2>
        <p>{{ _p('counter.archive_posts', collection_total) }}</p>
      </div>
      <div class="collection-hero__stat">
        <strong>{{ collection_total }}</strong>
        <span>{{ __('menu.posts') }}</span>
      </div>
    </section>
 
    <nav class="collection-breadcrumb" aria-label="breadcrumb">
      <a href="{{ url_for(site.path) }}"><i class="ic i-home"></i>{{ __('menu.home') }}</a>
      <i class="ic i-angle-right"></i>
      <a href="{{ url_for(config.category_dir) }}">{{ __('title.all') }}{{ __('title.category') }}</a>
      {%- set prev = _category_prev(page.category) %}
⋯ 未改动代码已省略 ⋯
      <span>{{ page.title }}</span>
    </nav>
 
    <div class="collection-grid category-posts-grid">
      {%- for post in page.posts.toArray() %}
        {{ collection.render(post, false) }}
      {%- endfor %}
    </div>
  </div>

themes/shoka/layout/tag.njk改动位置:第 1–2、10–28、33–39 行 28 行 · 1.1 KB
themes/shoka/layout/tag.njk
{% extends '_partials/layout.njk' %}
{% import '_macro/collection-card.njk' as collection with context %}
⋯ 未改动代码已省略 ⋯
{% block content %}
  {%- set collection_total = _collection_total(page) %}
  <div class="collection-page tag-page wrap">
    <section class="collection-hero collection-hero--tag">
      <div class="collection-hero__icon"><i class="ic i-tag"></i></div>
      <div class="collection-hero__copy">
        <span class="collection-hero__eyebrow">TAG COLLECTION</span>
        <h2>{{ page.tag }}</h2>
        <p>{{ _p('counter.archive_posts', collection_total) }}</p>
      </div>
      <div class="collection-hero__stat">
        <strong>{{ collection_total }}</strong>
        <span>{{ __('menu.posts') }}</span>
      </div>
    </section>
 
    <nav class="collection-breadcrumb" aria-label="breadcrumb">
      <a href="{{ url_for(site.path) }}"><i class="ic i-home"></i>{{ __('menu.home') }}</a>
      <i class="ic i-angle-right"></i>
⋯ 未改动代码已省略 ⋯
 
    <div class="collection-grid tag-posts-grid">
      {%- for post in page.posts.toArray() %}
        {{ collection.render(post) }}
      {%- endfor %}
    </div>
  </div>

themes/shoka/layout/page.njk改动位置:第 6–8、24–26、38–61、63–96 行 64 行 · 2.8 KB
themes/shoka/layout/page.njk
{%- if page.type === 'categories' %}
  {{- __('title.all') + __('title.category') }}
{%- elif page.type === 'tags' %}
⋯ 未改动代码已省略 ⋯
      {%- if page.type === 'categories' %}
        {{- __('title.all') + __('title.category') }}
      {%- elif page.type === 'tags' %}
⋯ 未改动代码已省略 ⋯
{%- if page.type === 'categories' %}
  <div class="taxonomy-page wrap">
    <section class="collection-hero collection-hero--category">
      <div class="collection-hero__icon"><i class="ic i-flag"></i></div>
      <div class="collection-hero__copy">
        <span class="collection-hero__eyebrow">INDEX</span>
        <h2>{{ __('title.all') }}{{ __('title.category') }}</h2>
        <p>{{ _p('counter.categories', site.categories.length) }}</p>
      </div>
      <div class="collection-hero__stat">
        <strong>{{ site.categories.length }}</strong>
        <span>{{ __('menu.categories') }}</span>
      </div>
    </section>
 
    <nav class="collection-breadcrumb" aria-label="breadcrumb">
      <a href="{{ url_for(site.path) }}"><i class="ic i-home"></i>{{ __('menu.home') }}</a>
      <i class="ic i-angle-right"></i>
      <span>{{ __('title.all') }}{{ __('title.category') }}</span>
    </nav>
 
    <div class="taxonomy-grid">
      {{ _list_categories(3) }}
    </div>
⋯ 未改动代码已省略 ⋯
{%- elif page.type === 'tags' %}
  <div class="taxonomy-page tag-index-page wrap">
    <section class="collection-hero collection-hero--tag">
      <div class="collection-hero__icon"><i class="ic i-tags"></i></div>
      <div class="collection-hero__copy">
        <span class="collection-hero__eyebrow">TAG INDEX</span>
        <h2>{{ __('title.all') }}{{ __('title.tag') }}</h2>
        <p>{{ _p('counter.tag_cloud', site.tags.length) }}</p>
      </div>
      <div class="collection-hero__stat">
        <strong>{{ site.tags.length }}</strong>
        <span>{{ __('menu.tags') }}</span>
      </div>
    </section>
 
    <nav class="collection-breadcrumb" aria-label="breadcrumb">
      <a href="{{ url_for(site.path) }}"><i class="ic i-home"></i>{{ __('menu.home') }}</a>
      <i class="ic i-angle-right"></i>
      <span>{{ __('title.all') }}{{ __('title.tag') }}</span>
    </nav>
 
    <div class="tag-index-grid">
      {%- for tag in site.tags.toArray() %}
      <a class="tag-index-card" href="{{ url_for(tag.path) }}" title="{{ tag.name }}">
        <span class="tag-index-card__icon"><i class="ic i-tag"></i></span>
        <span class="tag-index-card__copy">
          <strong class="tag-index-card__name">{{ tag.name }}</strong>
          <small><i class="ic i-feather"></i>专题标签 · 点击浏览</small>
        </span>
        <span class="tag-index-card__count"><strong>{{ tag.length }}</strong><small>文章</small></span>
        <span class="tag-index-card__arrow"><i class="ic i-angle-right"></i></span>
        <span class="tag-index-card__watermark" aria-hidden="true">#{{ loop.index }}</span>
      </a>
      {%- endfor %}

#辅助函数与页面生成器

新增 themes/shoka/scripts/helpers/collection.js :以下完整文件用于自动注册分页总数帮助函数,应先于引用它的模板一同保存。

themes/shoka/scripts/helpers/collection.js完整文件 21 行 · 969 B
themes/shoka/scripts/helpers/collection.js
/* global hexo */
'use strict';
 
// page.posts is only the pagination slice; page.total is a page count.
// Resolve the same complete collection used by Hexo's generators instead.
hexo.extend.helper.register('_collection_total', function(page = this.page) {
  const array = value => value && typeof value.toArray === 'function' ? value.toArray() : Array.isArray(value) ? value : [];
  const site = this.site || {};
  if(page.category || page.tag) {
    const rows = array(page.category ? site.categories : site.tags);
    const name = page.category || page.tag;
    const collection = rows.find(row => row.path === page.base) || rows.find(row => row.name === name);
    return collection ? collection.posts.length : 0;
  }
  return array(site.posts).filter(post =>
    (!page.year || post.date.year() === Number(page.year)) &&
    (!page.month || post.date.month() + 1 === Number(page.month)) &&
    (!page.day || post.date.date() === Number(page.day))
  ).length;
});
 

themes/shoka/scripts/helpers/list_categories.js改动位置:第 17–31、46–78、88–112 行 73 行 · 2.8 KB
themes/shoka/scripts/helpers/list_categories.js
hexo.extend.helper.register('_list_categories', function(depth = 0) {
  let hexo = this;
  let categories = hexo.site.categories;
 
  if (!categories || !categories.length) return '';
 
 
  const hierarchicalList = (level, parent) => {
    let result = '';
 
    prepareQuery(categories, parent).forEach((cat, i) => {
      let child = '';
 
      if (level + 1 < depth) {
        child = hierarchicalList(level + 1, cat._id);
⋯ 未改动代码已省略 ⋯
            <article class="taxonomy-card" style="--taxonomy-index:${i}">
              <a class="taxonomy-card__head" itemprop="url" href="${url}">
                <span class="taxonomy-card__icon"><i class="ic i-archive"></i></span>
                <span class="taxonomy-card__name">${name}</span>
                <span class="taxonomy-card__count"><strong>${count}</strong><small>POSTS</small></span>
              </a>
              <div class="taxonomy-card__insight">
                <span><i class="ic i-feather"></i>分类专题</span>
                <span>持续收录 · ${count} 篇文章</span>
              </div>
              <div class="taxonomy-card__recent">
                ${recent || '<span class="taxonomy-card__empty"><i class="ic i-file"></i>等待新的文章加入</span>'}
              </div>
              ${child ? `<div class="taxonomy-card__children">${child}</div>` : ''}
              <a class="taxonomy-card__more" href="${url}" aria-label="${name}"><i class="ic i-angle-right"></i></a>
              <span class="taxonomy-card__watermark" aria-hidden="true">${String(i + 1).padStart(2, '0')}</span>
            </article>`;
          break;
 
        case 1:
          result += `
            <div class="taxonomy-child">
              <a class="taxonomy-child__head" itemprop="url" href="${url}">
                <span><i class="ic i-flag"></i>${name}</span>
                <small>${count}</small>
              </a>
              ${child ? `<div class="taxonomy-grandchildren">${child}</div>` : ''}
            </div>`;
          break;
 
        case 2:
          result += `<a class="taxonomy-grandchild" itemprop="url" href="${url}">${name}<small>${count}</small></a>`;
          break;
⋯ 未改动代码已省略 ⋯
hexo.extend.helper.register('_categories', function() {
  let hexo = this;
  let categories = hexo.site.categories;
  if (!categories || !categories.length) return '';
 
  var pangu = hexo.theme.pangu ? require('pangu') : {
    spacing: data => {
      return data;
    }
  };
 
 
  let result = {};
 
  categories.forEach((cat, i) => {
    let child = prepareQuery(categories, cat._id);
    let cover = 'source/_posts' + cat.path.replace(hexo.config.category_dir, '') + 'cover.jpg'
 
    if (fs.existsSync(cover)) {
      let className = cat.slug.split('/');
      className.pop()
      cat.class = className.join(' ');
      cat.name = (pangu.spaceText || pangu.spacingText || pangu.spacing).call(pangu, cat.name);
 
      if (child.length != 0) {

themes/shoka/scripts/generaters/pages.js改动位置:第 6–16 行 11 行 · 237 B
themes/shoka/scripts/generaters/pages.js
  return [
    {
      path: config.category_dir + '/index.html',
      data: {type: 'categories'},
      layout: ['page']
    },
    {
      path: config.tag_dir + '/index.html',
      data: {type: 'tags'},
      layout: ['page']
    },

themes/shoka/scripts/generaters/archive.js改动位置:第 39–50 行 12 行 · 395 B
themes/shoka/scripts/generaters/archive.js
    if (archiveDir[archiveDir.length - 1] !== '/') archiveDir += '/';
 
    function generate(path, posts, options) {
      options = options || {};
      options.archive = true;
 
      result = result.concat(pagination(path, posts, {
        perPage: path === archiveDir? 0 : perPage,
        layout: ['archive', 'index'],
        format: paginationDir + '/%d/',
        data: options
      }));

#页面样式

themes/shoka/source/css/_common/components/pages/collapse.styl改动位置:第 124–155、297–331、387–448、585–630、755–805、862–909 行 274 行 · 6.2 KB
themes/shoka/source/css/_common/components/pages/collapse.styl
.collection-page,
.taxonomy-page {
  position: relative;
 
  .collection-hero {
    position: relative;
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    align-items: center;
    gap: 1.5rem;
    min-height: 12rem;
    margin: 1rem 0 1.25rem;
    padding: 2rem 2.25rem;
    overflow: hidden;
    border: .0625rem solid var(--grey-2);
    border-radius: 1.375rem;
    background:
      radial-gradient(circle at 88% 18%, var(--color-red-a1) 0, transparent 28%),
      linear-gradient(135deg, var(--grey-0) 0%, var(--grey-1) 52%, var(--grey-2) 100%);
    box-shadow: 0 1.5rem 3.75rem -2rem var(--box-bg-shadow);
 
    &::before {
      content: '';
      position: absolute;
      top: 0;
      left: 2.25rem;
      width: 6.5rem;
      height: .2rem;
      border-radius: 0 0 1rem 1rem;
      background: linear-gradient(90deg, var(--color-pink), var(--color-orange));
      box-shadow: 0 .2rem 1rem var(--color-pink-a3);
    }
⋯ 未改动代码已省略 ⋯
  .collection-breadcrumb {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: .45rem;
    width: fit-content;
    max-width: 100%;
    margin: 0 0 2rem;
    padding: .55rem .85rem;
    border: .0625rem solid var(--grey-2);
    border-radius: 5rem;
    color: var(--grey-5);
    background: var(--grey-0);
    box-shadow: 0 .55rem 1.5rem -.9rem var(--box-bg-shadow);
    font-size: $font-size-smallest;
 
    a {
      color: var(--grey-6);
      border: 0;
 
      &:hover {
        color: var(--primary-color);
      }
    }
 
    .i-home {
      margin-right: .35rem;
      color: var(--primary-color);
    }
 
    > .i-angle-right {
      color: var(--grey-4);
      font-size: .62rem;
    }
  }
⋯ 未改动代码已省略 ⋯
.collection-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1rem;
 
  +mobile() {
    grid-template-columns: minmax(0, 1fr);
  }
}
 
.collection-card {
  position: relative;
  display: grid;
  grid-template-columns: 4rem minmax(0, 1fr) 2rem;
  align-items: center;
  gap: 1rem;
  min-height: 9.25rem;
  padding: 1.25rem 1.15rem;
  overflow: hidden;
  border: .0625rem solid var(--grey-2);
  border-radius: 1.125rem;
  background: var(--grey-0);
  box-shadow: 0 1rem 2.5rem -1.65rem var(--box-bg-shadow);
  the-transition(.28s, ease-out);
 
  &::before {
    content: '';
    position: absolute;
    top: 0;
    right: 18%;
    left: 18%;
    height: .15rem;
    border-radius: 0 0 1rem 1rem;
    background: linear-gradient(90deg, transparent, var(--color-pink), var(--color-orange), transparent);
    opacity: 0;
    the-transition();
  }
 
  &:hover {
    border-color: var(--color-red-a3);
    box-shadow: 0 1.35rem 3rem -1.5rem var(--box-bg-shadow);
    transform: translateY(-.3rem);
 
    &::before {
      right: 8%;
      left: 8%;
      opacity: 1;
    }
 
    .collection-card__date {
      color: #fff;
      border-color: transparent;
      background: linear-gradient(145deg, var(--color-pink), var(--color-orange));
      box-shadow: 0 .7rem 1.5rem -.7rem var(--color-pink-a3);
    }
 
    .collection-card__arrow {
      color: #fff;
      background: var(--primary-color);
      transform: translateX(.15rem);
    }
  }
⋯ 未改动代码已省略 ⋯
.tag-index-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 1rem;
  margin-bottom: 2rem;
 
  +mobile() {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
 
  +mobile-small() {
    grid-template-columns: minmax(0, 1fr);
  }
}
 
.tag-index-card {
  position: relative;
  display: grid;
  grid-template-columns: 2.75rem minmax(0, 1fr) auto 1.5rem;
  align-items: center;
  gap: .75rem;
  min-height: 7.25rem;
  padding: 1rem;
  overflow: hidden;
  border: .0625rem solid var(--grey-2) !important;
  border-radius: 1rem;
  color: var(--grey-7);
  background:
    radial-gradient(circle at 100% 0, var(--color-red-a1), transparent 42%),
    var(--grey-0);
  box-shadow: 0 1rem 2.5rem -1.65rem var(--box-bg-shadow);
  the-transition(.28s, ease-out);
 
  &::before {
    content: '';
    position: absolute;
    top: 0;
    right: 15%;
    left: 15%;
    height: .15rem;
    border-radius: 0 0 1rem 1rem;
    background: linear-gradient(90deg, transparent, var(--color-pink), var(--color-orange), transparent);
    opacity: 0;
    the-transition();
  }
 
⋯ 未改动代码已省略 ⋯
.taxonomy-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1.25rem;
  margin-bottom: 2rem;
 
  +mobile() {
    grid-template-columns: minmax(0, 1fr);
  }
}
 
.taxonomy-card {
  position: relative;
  // Hover transforms must not be the only thing containing the arrow's layer.
  isolation: isolate;
  z-index: 0;
  min-height: 13rem;
  padding: 1.25rem 1.25rem 3.25rem;
  overflow: hidden;
  border: .0625rem solid var(--grey-2);
  border-radius: 1.25rem;
  background:
    radial-gradient(circle at 100% 0, var(--color-red-a1), transparent 35%),
    var(--grey-0);
  box-shadow: 0 1rem 2.5rem -1.65rem var(--box-bg-shadow);
  the-transition(.28s, ease-out);
 
  &::before {
    content: '';
    position: absolute;
    top: 0;
    right: 0;
    width: 5rem;
    height: .2rem;
    border-radius: 0 0 0 1rem;
    background: linear-gradient(90deg, var(--color-pink), var(--color-orange));
    opacity: .7;
  }
 
  &:hover {
    border-color: var(--color-red-a3);
    box-shadow: 0 1.4rem 3rem -1.5rem var(--box-bg-shadow);
    transform: translateY(-.3rem);
 
    .taxonomy-card__icon {
      color: #fff;
      background: linear-gradient(145deg, var(--color-pink), var(--color-orange));
      transform: rotate(-6deg) scale(1.05);
    }
 
    .taxonomy-card__more {
⋯ 未改动代码已省略 ⋯
  }
}
 
.taxonomy-card__insight {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .75rem;
  margin-top: 1rem;
  padding: .55rem .7rem;
  border: .0625rem solid var(--grey-2);
  border-radius: .75rem;
  color: var(--grey-5);
  background: var(--grey-1-a7);
  font-size: .65rem;
 
  span {
    min-width: 0;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
  }
 
  span:first-child {
    color: var(--primary-color);
    font-weight: 700;
    letter-spacing: .06em;
  }
 
  .ic {
    margin-right: .3rem;
    color: var(--color-orange);
  }
}
 
.taxonomy-card__recent {
  display: grid;
  gap: .35rem;
  margin-top: .65rem;
 
  > a,
  .taxonomy-card__empty {
    display: flex;
    align-items: center;
    min-width: 0;
    padding: .35rem .45rem;
    border: 0 !important;
    border-radius: .55rem;

themes/shoka/source/css/_common/components/pages/pages.styl 当前已经导入 collapse.styl ,因此本项目无需修改。如果你的版本没有导入,只需在页面样式导入区加入 @import 'collapse';

完成!

更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

Lavender 微信支付

微信支付

Lavender 支付宝

支付宝