将 Shoka 默认的线性归档、分类和标签列表改造成统一的卡片页面,加入数据概览、层级分类、最近文章、日期卡片、面包屑和移动端适配。
#前言
Shoka 默认归档页面使用时间轴展示文章,结构清晰,但当文章、分类和标签逐渐增多后,页面会显得较长,也不容易快速浏览。本次改造把归档、分类和标签统一为一套视觉系统,同时保留 Hexo 原有分页、分类层级和 PJAX 行为。
改造后的页面分为四类:
/archives/:按年份分组的文章卡片;/categories/分类名/:当前分类下的文章卡片;/tags/标签名/:当前标签下的文章卡片;/categories/和/tags/:分类索引卡片与标签索引卡片。
#开始前准备
这篇教程会修改主题模板、主题脚本和 Stylus 样式。第一次接触 Hexo 也不用担心,按下面的顺序操作即可。
- 找到博客根目录。能够看到
_config.yml、package.json、source和themes的文件夹,就是博客根目录。 - 复制一份
themes/shoka作为备份,或者先提交一次 Git。这样即使粘贴错误也可以快速恢复。 - 在博客根目录打开终端,执行
npm install,确保现有依赖已经安装。 - 后文出现的所有路径都从博客根目录开始计算,不要修改
public目录。public是 Hexo 每次生成时自动创建的成品目录,直接修改会被覆盖。 - 每完成一个大步骤,都可以执行一次
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 | 增加 categories 与 tags 两种索引页面分支;普通独立页和 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.njk 、 category.njk 或 tag.njk 本身,也不要删除原有分页、普通页面和 404 分支。
#设计思路
为了避免四个页面各写一套样式,先抽出三组公共组件:
collection-hero:页面顶部的数据概览卡;collection-breadcrumb:统一面包屑导航;collection-card:归档、分类和标签详情页共用的文章卡片。
分类索引和标签索引在此基础上分别使用 taxonomy-card 与 tag-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 步:创建公共文章卡片
公共卡片只编写一次,归档、分类和标签详情页都会调用它。
- 打开
themes/shoka/layout/_macro文件夹; - 新建
collection-card.njk,注意扩展名不是.html; - 把下面的代码完整粘贴进去并保存为 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.njk 、 category.njk 等真正主题文件时,必须使用正常的 { 和 } 。本篇教程源文件已经完成转义。
# /categories/ 或 /tags/ 是空白页
依次检查三处: pages.js 是否生成了 categories 和 tags 类型、 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 |
依次检查:
/archives/、年度归档和月度归档;/categories/与各级分类详情页;/tags/与标签详情页;- 分类层级、最近文章和数量是否正确;
- 移动端卡片是否变为单列;
- 日间与夜间模式下文字、背景和边框是否清晰;
- PJAX 切换、分页和面包屑链接是否正常。
#总结
这次改造的重点是 “复用” 而不是单独美化某一个页面。归档、分类和标签详情页共用文章卡片,分类和标签索引共用顶部概览与面包屑,因此后续修改圆角、阴影、间距或主题色时,只需要调整同一份 Stylus 样式。
#涉及改动的代码
代码卡片按属性名、函数名或带内容校验的具名片段定位,并显示当前源码行号。片段前后插行可自动重新定位;片段本身改变或位置不唯一时会停止生成,核对教程后再更新摘录,避免误引其他功能。
下面按文件集中展示本次改造的关键代码,只保留新增或修改的位置。卡片副标题会标注当前项目中的原始行号;区间之间出现的 “⋯ 未改动代码已省略 ⋯” 无需复制,也不要据此删除原文件中的其他内容。
行号会随以后修改而变化。如果你的主题版本与本文不同,请搜索卡片中的宏名称、区块名称、函数名或 CSS 选择器,再把代码合并到对应位置。
#模板文件
themes/shoka/layout/_macro/collection-card.njk改动位置:第 1–8、15–25、29–44 行
{% 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 行
{% 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 行
{% 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 行
{% 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 行
{%- 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完整文件
/* 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 行
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 行
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 行
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 行
.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'; 。
完成!