anzhiyu主题新添文章卡片样式

前言

这份教程,记录了我为 AnZhiYu 主题添加横向卡片样式的全部过程。代码不多,但每一处都仔细调整过:斜切的视觉平衡、移动端的适配、分页后的稳定性……甚至强迫自己解决了偶数页布局错乱的 bug。

代码不是束缚,是自由的工具。愿你的博客,也能长出属于自己的姿态。

正文

文件路径分布

1
2
3
4
5
6
7
8
9
10
11
12
13
14
blog/                          # Hexo 博客根目录
├── themes/
│ └── anzhiyu/ # AnZhiYu 主题目录
│ ├── layout/ # 布局模板目录
│ │ ├── includes/ # 包含组件目录
│ │ │ └── mixins/ # mixins 组件目录
│ │ │ └── post-ui-horizontal.pug # ← 横向卡片 Pug 模板
│ │ └── index.pug # ← 首页布局文件(调用横向卡片)
│ └── ... # 其他主题文件
├── source/ # 源文件目录(会被复制到 public)
│ └── cdn/ # CDN 资源目录
│ └── css/
│ └── horizontal-card.css # ← 横向卡片样式文件(你提供的 CSS)
└── ... # 其他博客文件

新建post-ui-horizontal.pug

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
mixin postUIHorizontal()
- let maxDate = 0
each item in site.posts.data
if item.date > maxDate
- maxDate = item.date

.horizontal-post-list
each article, index in page.posts.data
- let link = article.link || article.path
- let title = article.title || _p('no_title')
- let post_cover = article.cover
- let desc = article.description || article.excerpt || ''
- let category = article.categories.data[0]
- let dateStr = date(article.date, 'MM-DD')
- let wc = wordcount(article.content)
- const new_post = is_current('/') && (maxDate === article.date)
// 判断左右位置:偶数左边,奇数右边
- let isLeft = index % 2 === 0

article.recent-post-item.horizontal-card(class=isLeft ? 'image-right' : 'image-left' onclick=`pjax.loadUrl('${url_for(link)}')`)
// 图片在左(奇数卡片)
if !isLeft && post_cover && theme.cover.index_enable
.post-cover
a(href=url_for(link) title=title)
img.post_bg(src=url_for(post_cover) onerror=`this.onerror=null;this.src='`+ url_for(theme.error_img.post_page) + `'` alt=title loading="lazy")

// 无图片占位(奇数卡片)
if !isLeft && !(post_cover && theme.cover.index_enable)
.post-cover.no-image
a(href=url_for(link) title=title)
.cover-placeholder

// 内容区
.post-content
.post-content-top
.post-tips
if (is_home() && (article.top || article.sticky > 0))
span.article-meta.sticky-warp
i.anzhiyufont.anzhiyu-icon-thumbtack.sticky
span.sticky= _p('sticky')

a.post-title(href=url_for(link) title=title)= title

.post-meta-wrap
if (theme.post_meta.page.date_type)
span.post-meta-date
if (theme.post_meta.page.date_type === 'both')
i.anzhiyufont.anzhiyu-icon-calendar-alt(style=`${theme.post_meta.page.date_format==="simple" ? "display:none":""}`)
span.article-meta-label=_p('post.created')
time.post-meta-date-created(datetime=date_xml(article.date) title=_p('post.created') + ' ' + full_date(article.date) time=full_date(article.date))=date(article.date, config.date_format)
span.article-meta-separator
i.anzhiyufont.anzhiyu-icon-history(style=`font-size: 15px; ${theme.post_meta.page.date_format==="simple" ? "display:none":""}`)
span.article-meta-label=_p('post.updated')
time.post-meta-date-updated(datetime=date_xml(article.updated) title=_p('post.updated') + ' ' + full_date(article.updated) time=full_date(article.updated))=date(article.updated, config.date_format)
else
- let data_type_updated = theme.post_meta.page.date_type === 'updated'
- let date_type = data_type_updated ? 'updated' : 'date'
- let date_type_other = data_type_updated ? 'date' : 'updated'
- let date_icon = data_type_updated ? 'anzhiyu-icon-history' :'anzhiyu-icon-calendar-days'
- let date_title = data_type_updated ? _p('post.updated') : _p('post.created')
- let date_title_other = data_type_updated ? _p('post.created') : _p('post.updated')
i.anzhiyufont(class=date_icon style=`font-size: 15px; ${theme.post_meta.page.date_format==="simple" ? "display:none":""}`)
span.article-meta-label=date_title
time(datetime=date_xml(article[date_type]) title=date_title + ' ' + full_date(article[date_type]) time=full_date(article[date_type]))=date(article[date_type], config.date_format)
time(datetime=date_xml(article[date_type_other]), class="time_hidden", title=date_title_other + ' ' + full_date(article[date_type_other]) time=full_date(article[date_type_other]))=date(article[date_type_other], config.date_format)

if (theme.post_meta.page.tags && article.tags.data.length > 0)
span.article-meta.tags
each item, index in article.tags.data
a(href=url_for(item.path) event.cancelbubble onclick="window.event.cancelBubble=true;").article-meta__tags
span
i.anzhiyufont.anzhiyu-icon-hashtag
=item.name

mixin countBlockHorizontal
- needLoadCountJs = true
span.article-meta
span.article-meta-separator
i.eucalyptus.icon-comments
if block
block
span.article-meta-label= ' ' + _p('card_post_count')

if theme.comments.card_post_count
case theme.comments.use[0]
when 'Valine'
+countBlockHorizontal
a(href=url_for(link) + '#post-comment')
span.valine-comment-count(data-xid=url_for(link))
i.anzhiyufont.anzhiyu-icon-spinner.anzhiyu-spin
when 'Waline'
+countBlockHorizontal
a(href=url_for(link) + '#post-comment')
span.waline-comment-count(id=url_for(link))
i.anzhiyufont.anzhiyu-icon-spinner.anzhiyu-spin
when 'Twikoo'
+countBlockHorizontal
a.twikoo-count(href=url_for(link) + '#post-comment' tabindex="-1")
i.anzhiyufont.anzhiyu-icon-spinner.anzhiyu-spin
when 'Artalk'
+countBlockHorizontal
a(href=url_for(link) + '#post-comment')
span.artalk-count(data-page-key=url_for(link))
i.anzhiyufont.anzhiyu-icon-spinner.anzhiyu-spin

case theme.index_post_content.method
when false
- break
when 1
.post-desc!= article.description
when 2
if article.description
.post-desc!= article.description
else
- const content = strip_html(article.content)
- let expert = content.substring(0, theme.index_post_content.length)
- content.length > theme.index_post_content.length ? expert += ' ...' : ''
.post-desc!= expert
default
- const content = strip_html(article.content)
- let expert = content.substring(0, theme.index_post_content.length)
- content.length > theme.index_post_content.length ? expert += ' ...' : ''
.post-desc!= expert

// 图片在右(偶数卡片)
if isLeft && post_cover && theme.cover.index_enable
.post-cover
a(href=url_for(link) title=title)
img.post_bg(src=url_for(post_cover) onerror=`this.onerror=null;this.src='`+ url_for(theme.error_img.post_page) + `'` alt=title loading="lazy")

// 无图片占位(偶数卡片)
if isLeft && !(post_cover && theme.cover.index_enable)
.post-cover.no-image
a(href=url_for(link) title=title)
.cover-placeholder

if theme.ad && theme.ad.index
if (index + 1) % 3 == 0
.horizontal-card.ads-wrap!=theme.ad.index

修改index.pug

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
extends includes/layout.pug

block content
include ./includes/mixins/post-ui.pug

// 关键:include 必须在顶层,不能放在 if 里面
include ./includes/mixins/post-ui-horizontal.pug

#recent-posts.recent-posts
include includes/categoryGroup.pug

if theme.home_card_style === 'horizontal'
+postUIHorizontal()
else
+postUI

include includes/pagination.pug

新建horizontal-card.css

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
/* ========== 变量定义 ========== */
:root {
--card-bg: #1a1a2e;
--card-bg-alt: #16213e;
--theme-color: #e94560;
--skew-width: 60px;
}

/* ========== 基础卡片样式 ========== */
.horizontal-card {
display: flex;
flex-direction: row;
width: 100%;
height: 260px;
border-radius: 16px;
overflow: hidden;
background: var(--card-bg);
box-shadow: 0 8px 32px rgba(0,0,0,0.3);
transition: all 0.4s ease;
cursor: pointer;
position: relative;
margin-bottom: 32px;
}

.horizontal-card:hover {
transform: translateY(-6px);
box-shadow: 0 16px 48px rgba(233, 69, 96, 0.15);
}

/* ========== 内容区域基础 ========== */
.horizontal-card .post-content {
width: calc(58% - 0px);
flex: 0 0 calc(58% - 0px);
max-width: calc(58% - 0px);
padding: 32px 40px;
display: flex;
flex-direction: column;
justify-content: space-between;
position: relative;
z-index: 2;
min-width: 0;
overflow: hidden;
box-sizing: border-box;
}

.horizontal-card .post-title {
font-size: 1.6rem;
font-weight: 700;
line-height: 1.3;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
margin-bottom: 4px;
word-break: break-word;
overflow-wrap: break-word;
}

.horizontal-card .post-desc {
font-size: 1rem;
line-height: 1.8;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
margin-top: 12px;
word-break: break-word;
overflow-wrap: break-word;
}

/* ========== 图片区域基础 ========== */
.horizontal-card .post-cover {
width: calc(42% - 0px);
flex: 0 0 calc(42% - 0px);
max-width: calc(42% - 0px);
min-width: 320px;
height: 100%;
position: relative;
overflow: hidden;
box-sizing: border-box;
}

.horizontal-card .post-cover img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.6s ease;
}

.horizontal-card:hover .post-cover img {
transform: scale(1.1);
}

/* ========== 图片在右边 + 斜切线 ========== */
.horizontal-card.image-right {
background: linear-gradient(135deg, var(--card-bg) 0%, var(--card-bg-alt) 100%);
}

.horizontal-card.image-right .post-content {
order: 1;
padding-right: 80px;
clip-path: polygon(
0 0,
calc(100% - var(--skew-width)) 0,
100% 100%,
0 100%
);
}

.horizontal-card.image-right .post-cover {
order: 2;
clip-path: polygon(
var(--skew-width) 0,
100% 0,
100% 100%,
0 100%
);
margin-left: calc(var(--skew-width) * -1);
margin-right: calc(var(--skew-width) * -1);
width: calc(42% + var(--skew-width)) !important;
flex: 0 0 calc(42% + var(--skew-width)) !important;
max-width: calc(42% + var(--skew-width)) !important;
}

/* ========== 图片在左边 + 斜切线 ========== */
.horizontal-card.image-left {
background: linear-gradient(225deg, var(--card-bg) 0%, var(--card-bg-alt) 100%);
}

.horizontal-card.image-left .post-content {
order: 2;
padding-left: 80px;
clip-path: polygon(
var(--skew-width) 0,
100% 0,
100% 100%,
0 100%
);
}

.horizontal-card.image-left .post-cover {
order: 1;
clip-path: polygon(
0 0,
calc(100% - var(--skew-width)) 0,
100% 100%,
0 100%
);
margin-right: calc(var(--skew-width) * -1);
margin-left: calc(var(--skew-width) * -1);
width: calc(42% + var(--skew-width)) !important;
flex: 0 0 calc(42% + var(--skew-width)) !important;
max-width: calc(42% + var(--skew-width)) !important;
}

/* 第一张卡片距离顶部增加间距 */
.horizontal-card:first-child {
margin-top: 32px;
}

/* ========== 响应式适配 ========== */
@media screen and (max-width: 768px) {
.horizontal-card,
.horizontal-card.image-left,
.horizontal-card.image-right {
flex-direction: column;
height: auto;
margin-bottom: 28px;
}

.horizontal-card .post-content,
.horizontal-card.image-left .post-content,
.horizontal-card.image-right .post-content {
width: 100%;
flex: none;
max-width: 100%;
padding: 24px;
order: 2;
clip-path: none;
}

.horizontal-card.image-left .post-cover,
.horizontal-card.image-right .post-cover {
width: 100% !important;
flex: none !important;
max-width: 100% !important;
min-width: auto;
height: 220px;
order: 1;
margin: 0 !important;
clip-path: polygon(0 0, 100% 0, 100% 85%, 0 100%);
}
}

_config.anzhiyu.yml调用自定义horizontal-card.css

1
2
3
4
5
inject:
head:
# 自定义css
...
- <link rel="stylesheet" href="/cdn/css/horizontal-card.css">

_config.anzhiyu.yml中使用新的文章卡片样式,建议添加article_double_row之后

1
2
3
4
5
6
7
8
9
...

# 首页双栏显示
article_double_row: true

# 文章卡片样式: default | horizontal
home_card_style: horizontal

...