Hugo系列(五):添加豆瓣观影页面
通过 GitHub doumark-action 保存豆瓣观影数据,并给 Hugo 博客添加豆瓣观影页面。
Note2025.03.29 更新了一下模板文件,添加了筛选排序功能。
已将
data.GetCSV
函数替换,Hugo 会在0.136
版本移除,目前最新版本是0.135
。 取而代之的是resources.Get
和transform.Unmarshal
,由于resource
函数获取全局文件路径是~/assets/
,因此也替换了对应文件的存放路径。
前言
Hexo
有 hexo-douban 项目,可以添加观影数据到页面,更换 Hugo
后也想添加一个观影页面,查找资料后发现有一个 doumark-action
项目可以将观影数据保存为本地文件,然后可以通过 Hugo 的函数获取数据,再前端展示,前面的文章中已经添加了豆瓣条目的短代码,这篇文章再补充两个,一个是近期观影的短代码,另外是添加一个海报墙页面。
最终实现的样式布局参考了一些博主的文章和仓库代码:
保存本地数据
由于豆瓣 API
的限制,之前的一些在线获取数据方式已经失效了,可以通过在线爬取标记的数据到本地,然后操作本地的数据文件,再前端展示数据。
用到的项目是 doumark-action。
首先在博客仓库的根目录新建一个 Workflow
,新建 .github/workflows/douban.yml
文件,在其中写入自己想要保存的数据,影音、书籍,也可以保存到 Notion
,项目有详细的说明,这里贴一下我的配置,其中将 id
改为自己的豆瓣 ID
。
1# .github/workflows/douban.yml
2name: douban
3on:
4 schedule:
5 - cron: "30 * * * *"
6 workflow_dispatch:
7
8jobs:
9 douban:
10 name: Douban mark data sync
11 runs-on: ubuntu-latest
12 steps:
13 - name: Checkout
14 uses: actions/checkout@v2
15
16 - name: movie
17 uses: lizheming/doumark-action@master
18 with:
19 id: 222317686
20 type: movie
21 format: csv
22 dir: ./assets/data/douban
23
24 - name: book
25 uses: lizheming/doumark-action@master
26 with:
27 id: 222317686
28 type: book
29 format: csv
30 dir: ./assets/data/douban
31
32 - name: Commit
33 uses: EndBug/add-and-commit@v8
34 with:
35 message: 'chore: update douban data'
36 add: './assets/data/douban'
Tip如果使用
data.GetCSV
函数,不能使用站点根目录下的data
目录,之前也有人出现同类型的错误,更改目录后就正常了。
When working with local data, the file path is relative to the working directory. You must not place CSV files in the project’s data directory. 来自 Hugo 文档
之后可以在仓库手动执行一下 action
,完成后会在 ~/assets/data/douban/
生成 movie.csv
和 book.csv
,这就是用到的数据文件。
近期观影短代码
配置
要添加短代码需要在 ~/layouts/shortcodes/
目录下新建短代码模板文件,这里为 recent-douban.html
,在其中添加模板元素:
1{{ $type := .Get "type" }}
2{{ $count := .Get "count" | default 4 }}
3{{ $count = add $count 1 }}
4{{ $items := slice }}
5{{ $csvPath := "" }}
6{{ if eq $type "movies" }}
7 {{ $csvPath = "data/douban/movie.csv" }}
8{{ else if eq $type "books" }}
9 {{ $csvPath = "data/douban/book.csv" }}
10{{ end }}
11
12{{ with resources.Get $csvPath }}
13 {{ $opts := dict "delimiter" "," }}
14 {{ $items = . | transform.Unmarshal $opts }}
15{{ else }}
16 {{ errorf "无法获取资源 %q" $csvPath }}
17{{ end }}
18
19<div class="recent-items">
20 {{ range $idx, $item := first $count $items }}
21 {{ if ne $idx 0 }}
22 {{ $rating := float (index $item 6) }}
23 <div class="recent-item">
24 <div class="recent-item-cover">
25 <img class="avatar" src="{{ index $item 3 }}" referrer-policy="no-referrer" loading="lazy" alt="{{ index $item 9 }}" title="{{ index $item 1 }}" width="150" height="220">
26 </div>
27 <div class="recent-douban-rating">
28 <div class="rating">
29 <span class="allstardark">
30 <span class="allstarlight" style="width:{{ mul 10 $rating }}%"></span>
31 </span>
32 <span class="rating_nums">{{ $rating }}</span>
33 </div>
34 </div>
35 <div class="recent-item-title">
36 <a rel="noreferrer" href="{{ index $item 5 }}" target="_blank">{{ index $item 1 }}</a>
37 </div>
38 </div>
39 {{ end }}
40 {{ end }}
41</div>
然后添加一下样式代码,这里和之前的文章保持一致,添加到 shortcodes.scss
中:
1.recent-items {
2 display: flex;
3 flex-wrap: wrap;
4 justify-content: space-between;
5 margin: 15px;
6}
7.recent-items .recent-item, .recent-items .recent-item img {
8 margin-bottom: 10px;
9}
Important之前的豆瓣条目短代码样式文件已经添加了评分星级的样式
allstarlight
,所以这里不添加,如果之前没添加过的话需要添加相关的样式,在之前的文章可以找到。
使用
使用方法如下:
1{\{< recent-douban type="movies" count=8 >}\}
2{\{< recent-douban type="books" >}\}
默认展示四条数据,可以通过 count
参数指定显示的数量。
观影页面
配置
添加一个海报墙页面展示所有的观影信息,默认展示 18
条信息。
首先在 ~/lauouts/_default/
文件夹下新建模板文件,这里为 posterwall.html
,然后在其中添加页面元素:
1{{- define "title" -}}
2 {{- title .Title -}}
3 {{- if .Site.Params.withSiteTitle }} {{ .Site.Params.titleDelimiter }} {{ .Site.Title }}{{- end -}}
4{{- end -}}
5
6{{- define "content" -}}
7 {{- $title := title .Title -}}
8 {{- $params := partial "function/params.html" -}}
9 {{- $toc := .Scratch.Get "toc" -}}
10 {{- $tocEmpty := eq .TableOfContents `<nav id="TableOfContents"></nav>` -}}
11<div class="posterwall-wrapper">
12 <div class="posterwall-description">
13 {{ .Params.description | markdownify }}
14 </div>
15 <div class="movie-wall">
16 {{ $csvPath := "data/douban/movie.csv" }}
17 {{ $items := slice }}
18 {{ with resources.Get $csvPath }}
19 {{ $opts := dict "delimiter" "," }}
20 {{ $items = . | transform.Unmarshal $opts }}
21 {{ else }}
22 {{ errorf "无法获取资源 %q" $csvPath }}
23 {{ end }}
24 {{ range $idx, $item := $items }}
25 {{ if ne $idx 0 }}
26 {{ $rating := float (index $item 6) }}
27 <div class="movie-item" style="display: none;">
28 <div class="movie-cover">
29 <img src="{{ index $item 3 }}" alt="{{ index $item 1 }}" loading="lazy" width="200" referrer-policy="no-referrer">
30 <div class="movie-info">
31 <div class="movie-title"><a rel="noreferrer" href="{{ index $item 5 }}" target="_blank">{{ index $item 1 }}</a></div>
32 <div class="movie-rating">
33 <div class="rating">
34 <span class="allstardark">
35 <span class="allstarlight" style="width:{{ mul 10 $rating }}%"></span>
36 </span>
37 <span class="rating_nums">{{ index $item 6 }}</span>
38 </div>
39 </div>
40 <div class="movie-card">{{ index $item 12 }}</div>
41 <div class="movie-comment">{{ index $item 9 }}</div>
42 </div>
43 </div>
44 </div>
45 {{ end }}
46 {{ end }}
47 </div>
48 <button id="loadMore">加载更多</button>
49</div>
50
51<script>
52 let visibleMovies = 20;
53 const movieItems = document.querySelectorAll('.movie-item');
54 const loadMoreButton = document.getElementById('loadMore');
55
56 function updateVisibility() {
57 movieItems.forEach((movie, idx) => {
58 movie.style.display = idx < visibleMovies ? 'block' : 'none';
59 });
60
61 if (visibleMovies >= movieItems.length) {
62 loadMoreButton.style.display = 'none';
63 }
64 }
65
66 loadMoreButton.addEventListener('click', () => {
67 visibleMovies += 20;
68 updateVisibility();
69 });
70
71 updateVisibility();
72</script>
73{{ end }}
Important
- 代码中包括了页面元素和加载更多数据的
js
代码,并且在页面标题下添加了一个页面描述,方便在 markdown 文件中更改描述信息,也便于调整样式。- 同样页面中评分星级的命名
allstarlight
也是与之前豆瓣条目短代码一致,方便一起调整。- 顶部的元素适应主题
Fixit
,其他主题自行更改。
完成后在 custom.scss
中添加样式代码:
1// 电影海报墙样式
2.posterwall-description {
3 margin: 10px;
4 text-align: center;
5}
6.movie-wall {
7 display: grid;
8 grid-template-columns: repeat(3, 1fr);
9 grid-gap: 3px;
10 padding: 3px;
11}
12
13.movie-item {
14 width: 100%;
15 margin-bottom: 0;
16}
17
18.movie-cover {
19 position: relative;
20 overflow: hidden;
21 aspect-ratio: 2 / 3;
22}
23
24.movie-cover img {
25 width: 100%;
26 height: 100%;
27 object-fit: cover;
28 transition: transform 0.3s ease;
29}
30
31.movie-info {
32 position: absolute;
33 top: 0;
34 left: 0;
35 right: 0;
36 bottom: 0;
37 background: rgba(0, 0, 0, 0.8);
38 color: #fff;
39 padding: 10px;
40 opacity: 0;
41 transition: opacity 0.3s ease;
42 overflow: auto;
43 display: flex;
44 flex-direction: column;
45 justify-content: center;
46}
47
48.movie-cover:hover img {
49 transform: scale(1.1);
50}
51
52.movie-cover:hover .movie-info {
53 opacity: 1;
54}
55
56#loadMore {
57 display: block;
58 margin: 20px auto;
59 padding: 10px 20px;
60 background-color: lighten($color-accent,0.1);
61 color: white;
62 border: none;
63 cursor: pointer;
64}
65
66#loadMore:hover {
67 background-color: gray;
68}
69
70.movie-info .movie-rating {
71 display: flex;
72 align-items: center;
73 margin-top: 3px;
74 margin-bottom: 3px;
75}
76.movie-info .movie-rating .rating .rating_nums {
77 font-size: 1.0em;
78 color: #fff;
79}
80
81.movie-info .movie-card {
82 margin: 5px 0;
83 font-size: 0.7em;
84 color: #fff;
85}
86
87.movie-info .movie-comment {
88 margin: 5px 0;
89 font-size: 0.7em;
90 color: #fff;
91}
使用
在 content
目录下创建一个 posterwall.md
文件,Front Matter
信息填写如下,标题可描述可以自由更改:
1---
2title: 海报墙
3layout: "posterwall"
4description: "这里是我已观看电影的海报墙,数据来源于豆瓣。"
5---
添加后就可以在配置文件中添加一个菜单页面来指向海报墙,最终效果可以访问 👉 观影页面
图片地址替换
CSV
文件中的图片地址是 douban-action
作者反代的地址,近期图片挂了,而 Neodb
提供 API
,所以这里将图片替换为 Neodb
地址,方法如下, 海报墙页面代码和位置都一致:
在 {{ $rating := float (index $item 6) }}
这行代码下添加下面两行:
1{{ $dbUrl := printf "https://neodb.social/api/catalog/fetch?url=%s" (index $item 5) }}
2{{ $dbFetch := (resources.GetRemote $dbUrl) | transform.Unmarshal }}
然后将图片地址更换:
1-src=" {{ index $item 3 }} "
2+src=" {{ $dbFetch.cover_image_url }} "
观影页面更新
1{{- define "title" -}}
2 {{- title .Title -}}
3 {{- if .Site.Params.withSiteTitle }} {{ .Site.Params.titleDelimiter }} {{ .Site.Title }}{{- end -}}
4{{- end -}}
5
6{{- define "main" -}}
7 {{- $title := title .Title -}}
8<div class="movies-wrapper">
9 <div class="movies-description">
10 {{ .Params.description | markdownify }}
11 </div>
12
13<!-- 修改筛选控制区HTML结构 -->
14<div class="filter-sort-controls">
15 <div class="filter-container">
16 <!-- 年份筛选 -->
17 <div class="filter-section">
18 <button class="filter-button" id="toggleYearFilter">年份筛选 <span class="arrow-down">▼</span></button>
19 <div class="filter-options" id="yearOptions">
20 <div class="filter-options-grid" id="yearFilters"></div>
21 <button class="reset-filters" data-type="year">重置年份</button>
22 </div>
23 </div>
24
25 <!-- 类型筛选 -->
26 <div class="filter-section">
27 <button class="filter-button" id="toggleGenreFilter">类型筛选 <span class="arrow-down">▼</span></button>
28 <div class="filter-options" id="genreOptions">
29 <div class="filter-options-grid" id="genreFilters"></div>
30 <button class="reset-filters" data-type="genre">重置类型</button>
31 </div>
32 </div>
33
34 <!-- 排序 -->
35 <div class="sort-section">
36 <button class="sort-button" id="toggleSort">排序方式 <span class="arrow-down">▼</span></button>
37 <div class="sort-options" id="sortOptions">
38 <button class="sort-option" data-sort="time" data-order="desc">观影时间 (倒序)</button>
39 <button class="sort-option" data-sort="time" data-order="asc">观影时间 (正序)</button>
40 <button class="sort-option" data-sort="rating" data-order="desc">评分 (高→低)</button>
41 <button class="sort-option" data-sort="rating" data-order="asc">评分 (低→高)</button>
42 </div>
43 </div>
44 </div>
45 </div>
46
47 <div class="movies-wall">
48 {{ $csvPath := "data/douban/movie.csv" }}
49 {{ $items := slice }}
50 {{ with resources.Get $csvPath }}
51 {{ $opts := dict "delimiter" "," }}
52 {{ $items = . | transform.Unmarshal $opts }}
53 {{ else }}
54 {{ errorf "无法获取资源 %q" $csvPath }}
55 {{ end }}
56
57 {{ range $idx, $item := $items }}
58 {{ if ne $idx 0 }}
59 {{ $id := index $item 0 }}
60 {{ $title := index $item 1 }}
61 {{ $poster := index $item 3 }}
62 {{ $pubdate := index $item 4 }}
63 {{ $url := index $item 5 }}
64 {{ $rating := float (index $item 6) }}
65 {{ $genres := index $item 7 }}
66 {{ $comment := index $item 9 }}
67 {{ $card := index $item 12 }}
68 {{ $star_time := index $item 11 }}
69
70 {{ $year := cond (findRE "\\d{4}" $pubdate) (index (findRE "\\d{4}" $pubdate) 0) "" }}
71
72 <div class="movie-item"
73 data-id="{{ $id }}"
74 data-year="{{ $year }}"
75 data-genres="{{ $genres }}"
76 data-rating="{{ $rating }}"
77 data-time="{{ $star_time }}">
78 <div class="movie-cover">
79 <img src="{{ $poster }}" alt="{{ $title }}" loading="lazy" referrer-policy="no-referrer">
80 <div class="movie-info">
81 <div class="movie-title"><a rel="noreferrer" href="{{ $url }}" target="_blank">{{ $title }}</a></div>
82 <div class="movie-rating">
83 <div class="rating">
84 <span class="allstardark">
85 <span class="allstarlight" style="width:{{ mul 10 $rating }}%"></span>
86 </span>
87 <span class="rating_nums">{{ $rating }}</span>
88 </div>
89 </div>
90 <div class="movie-card">{{ $card }}</div>
91 {{ if $comment }}
92 <div class="movie-comment">{{ $comment }}</div>
93 {{ end }}
94 </div>
95 </div>
96 </div>
97 {{ end }}
98 {{ end }}
99 </div>
100
101 <!-- 分页控制 -->
102 <div class="pagination-controls">
103 <button id="prevPage" disabled>上一页</button>
104 <span id="pageInfo">第 <span id="currentPage">1</span> 页,共 <span id="totalPages">1</span> 页</span>
105 <button id="nextPage">下一页</button>
106 </div>
107</div>
108
109<script>
110 document.addEventListener('DOMContentLoaded', function() {
111 // 电影项元素
112 const movieItems = document.querySelectorAll('.movie-item');
113 // 筛选和排序元素
114 const toggleYearFilter = document.getElementById('toggleYearFilter');
115 const yearOptions = document.getElementById('yearOptions');
116 const toggleGenreFilter = document.getElementById('toggleGenreFilter');
117 const genreOptions = document.getElementById('genreOptions');
118 const toggleSort = document.getElementById('toggleSort');
119 const sortOptions = document.getElementById('sortOptions');
120 const yearFilters = document.getElementById('yearFilters');
121 const genreFilters = document.getElementById('genreFilters');
122 const sortButtons = document.querySelectorAll('.sort-option');
123
124 // 分页元素
125 const prevPageBtn = document.getElementById('prevPage');
126 const nextPageBtn = document.getElementById('nextPage');
127 const currentPageEl = document.getElementById('currentPage');
128 const totalPagesEl = document.getElementById('totalPages');
129
130 // 分页配置
131 let itemsPerRow = window.innerWidth < 768 ? 2 : (window.innerWidth < 992 ? 3 : 6);
132 const rowsPerPage = 5;
133 let itemsPerPage = itemsPerRow * rowsPerPage;
134
135 // 筛选和排序的状态
136 let activeFilters = {
137 years: [],
138 genres: []
139 };
140 let currentSort = {
141 field: 'time',
142 order: 'desc'
143 };
144 let currentPage = 1;
145
146 // 初始化
147 initializeFilters();
148 updateDisplay();
149
150 // 初始化筛选器(修改部分)
151 function initializeFilters() {
152 // 收集所有年份和类型
153 const years = new Set();
154 const genres = new Set();
155
156 movieItems.forEach(item => {
157 const year = item.dataset.year;
158 if (year) years.add(year);
159
160 const genreList = item.dataset.genres.split(',');
161 genreList.forEach(genre => {
162 if (genre) genres.add(genre.trim());
163 });
164 });
165
166 // 生成年份按钮
167 const sortedYears = Array.from(years).sort((a, b) => b - a);
168 sortedYears.forEach(year => {
169 const btn = document.createElement('button');
170 btn.className = 'filter-option-btn';
171 btn.textContent = year;
172 btn.dataset.value = year;
173 btn.addEventListener('click', function() {
174 this.classList.toggle('active');
175 const index = activeFilters.years.indexOf(year);
176 if (index === -1) {
177 activeFilters.years.push(year);
178 } else {
179 activeFilters.years.splice(index, 1);
180 }
181 currentPage = 1;
182 updateDisplay();
183 });
184 yearFilters.appendChild(btn);
185 });
186
187 // 生成类型按钮
188 const sortedGenres = Array.from(genres).sort();
189 sortedGenres.forEach(genre => {
190 const btn = document.createElement('button');
191 btn.className = 'filter-option-btn';
192 btn.textContent = genre;
193 btn.dataset.value = genre;
194 btn.addEventListener('click', function() {
195 this.classList.toggle('active');
196 const index = activeFilters.genres.indexOf(genre);
197 if (index === -1) {
198 activeFilters.genres.push(genre);
199 } else {
200 activeFilters.genres.splice(index, 1);
201 }
202 currentPage = 1;
203 updateDisplay();
204 });
205 genreFilters.appendChild(btn);
206 });
207
208 // 重置筛选逻辑(修改部分)
209 document.querySelectorAll('.reset-filters').forEach(btn => {
210 btn.addEventListener('click', function() {
211 const type = this.dataset.type;
212 if (type === 'year') {
213 activeFilters.years = [];
214 yearFilters.querySelectorAll('.active').forEach(b => b.classList.remove('active'));
215 } else {
216 activeFilters.genres = [];
217 genreFilters.querySelectorAll('.active').forEach(b => b.classList.remove('active'));
218 }
219 currentPage = 1;
220 updateDisplay();
221 });
222 });
223 }
224
225 // 筛选电影(保持原有逻辑)
226 function filterMovies() {
227 const filtered = Array.from(movieItems).filter(item => {
228 const year = item.dataset.year;
229 const genres = item.dataset.genres.split(',').map(g => g.trim());
230
231 const yearMatch = activeFilters.years.length === 0 || activeFilters.years.includes(year);
232 const genreMatch = activeFilters.genres.length === 0 ||
233 activeFilters.genres.some(g => genres.includes(g));
234
235 return yearMatch && genreMatch;
236 });
237
238 return filtered;
239 }
240
241 // 排序电影(保持原有逻辑)
242 function sortMovies(movies) {
243 const sorted = movies.sort((a, b) => {
244 let valueA, valueB;
245
246 if (currentSort.field === 'time') {
247 valueA = a.dataset.time;
248 valueB = b.dataset.time;
249 } else {
250 valueA = parseFloat(a.dataset.rating);
251 valueB = parseFloat(b.dataset.rating);
252 }
253
254 if (valueA === valueB) {
255 return 0;
256 }
257
258 return currentSort.order === 'asc' ?
259 (valueA > valueB ? 1 : -1) :
260 (valueA < valueB ? 1 : -1);
261 });
262
263
264 return sorted;
265 }
266
267 // 更新显示(保持原有逻辑)
268 function updateDisplay() {
269 // 获取所有电影项目的原始引用
270 const allMovieItems = Array.from(document.querySelectorAll('.movie-item'));
271
272 // 应用筛选
273 const filteredMovies = filterMovies();
274
275 // 应用排序
276 const sortedMovies = sortMovies(filteredMovies);
277
278 // 更新分页信息
279 const totalPages = Math.ceil(sortedMovies.length / itemsPerPage) || 1;
280 currentPage = currentPage > totalPages ? totalPages : currentPage;
281 currentPageEl.textContent = currentPage;
282 totalPagesEl.textContent = totalPages;
283
284 // 更新分页按钮状态
285 prevPageBtn.disabled = currentPage <= 1;
286 nextPageBtn.disabled = currentPage >= totalPages;
287
288 // 计算当前页面应显示的范围
289 const startIdx = (currentPage - 1) * itemsPerPage;
290 const endIdx = startIdx + itemsPerPage;
291
292 // 先隐藏所有电影
293 allMovieItems.forEach(item => {
294 item.style.display = 'none';
295 });
296
297 // 只显示当前页中筛选和排序后的电影
298 sortedMovies.forEach((item, index) => {
299 if (index >= startIdx && index < endIdx) {
300 item.style.display = 'block';
301 }
302 });
303
304 // 重新排序DOM元素 (仅处理筛选后的元素)
305 const moviesWall = document.querySelector('.movies-wall');
306 sortedMovies.forEach(item => {
307 moviesWall.appendChild(item);
308 });
309 }
310
311 // 修改 filterMovies 函数以确保它正确处理筛选逻辑
312 function filterMovies() {
313 return Array.from(movieItems).filter(item => {
314 const year = item.dataset.year;
315 const genres = item.dataset.genres.split(',').map(g => g.trim());
316
317 // 年份筛选逻辑
318 const yearMatch = activeFilters.years.length === 0 ||
319 activeFilters.years.includes(year);
320
321 // 类型筛选逻辑
322 const genreMatch = activeFilters.genres.length === 0 ||
323 activeFilters.genres.some(g => genres.includes(g));
324
325 // 调试输出
326 // console.log(`电影: ${item.querySelector('.movie-title')?.textContent} - 年份匹配: ${yearMatch}, 类型匹配: ${genreMatch}`);
327
328 return yearMatch && genreMatch;
329 });
330 }
331
332
333
334 // 事件监听(修改部分)
335 toggleYearFilter.addEventListener('click', function() {
336 yearOptions.classList.toggle('show');
337 this.querySelector('.arrow-down').textContent =
338 yearOptions.classList.contains('show') ? '▲' : '▼';
339 });
340
341 toggleGenreFilter.addEventListener('click', function() {
342 genreOptions.classList.toggle('show');
343 this.querySelector('.arrow-down').textContent =
344 genreOptions.classList.contains('show') ? '▲' : '▼';
345 });
346
347 toggleSort.addEventListener('click', function() {
348 sortOptions.classList.toggle('show');
349 this.querySelector('.arrow-down').textContent =
350 sortOptions.classList.contains('show') ? '▲' : '▼';
351 });
352
353 // 保持原有排序逻辑
354 sortButtons.forEach(button => {
355 button.addEventListener('click', function() {
356 currentSort.field = this.dataset.sort;
357 currentSort.order = this.dataset.order;
358 currentPage = 1;
359
360 sortButtons.forEach(btn => btn.classList.remove('active'));
361 this.classList.add('active');
362 updateDisplay();
363
364 sortOptions.classList.remove('show');
365 toggleSort.querySelector('.arrow-down').textContent = '▼';
366 });
367 });
368
369 // 分页控制(保持原有逻辑)
370 prevPageBtn.addEventListener('click', () => {
371 if (currentPage > 1) {
372 currentPage--;
373 updateDisplay();
374 }
375 });
376
377 nextPageBtn.addEventListener('click', () => {
378 const filteredMovies = filterMovies();
379 const totalPages = Math.ceil(filteredMovies.length / itemsPerPage);
380 if (currentPage < totalPages) {
381 currentPage++;
382 updateDisplay();
383 }
384 });
385
386 // 点击外部关闭(修改部分)
387 document.addEventListener('click', function(event) {
388 const isFilter = event.target.closest('.filter-section');
389 const isSort = event.target.closest('.sort-section'); // 新增排序区域判断
390
391 if (!isFilter && !isSort) { // 同时检查两个区域
392 [yearOptions, genreOptions, sortOptions].forEach(panel => {
393 if (panel.classList.contains('show')) {
394 panel.classList.remove('show');
395 // 修复箭头方向更新逻辑
396 const toggleBtn = document.querySelector(`#${panel.id.replace('Options','Filter')}`);
397 if(toggleBtn) {
398 toggleBtn.querySelector('.arrow-down').textContent = '▼';
399 }
400 }
401 });
402 }
403 });
404
405 // 响应式处理(保持原有逻辑)
406 window.addEventListener('resize', function() {
407 const newItemsPerRow = window.innerWidth < 768 ? 2 : (window.innerWidth < 992 ? 3 : 6);
408 if (newItemsPerRow !== itemsPerRow) {
409 itemsPerRow = newItemsPerRow;
410 itemsPerPage = itemsPerRow * rowsPerPage;
411 updateDisplay();
412 }
413 });
414 });
415 </script>
416
417<style>
418/* 电影墙容器 */
419.movies-wrapper {
420 margin: 20px auto;
421 max-width: 1200px;
422}
423
424.movies-description {
425 margin: 10px;
426 text-align: center;
427}
428
429/* 电影墙网格布局 */
430.movies-wall {
431 display: grid;
432 grid-template-columns: repeat(6, 1fr);
433 grid-gap: 15px;
434 padding: 15px;
435}
436
437/* 响应式布局 */
438@media (max-width: 992px) {
439 .movies-wall {
440 grid-template-columns: repeat(3, 1fr);
441 }
442}
443
444@media (max-width: 768px) {
445 .movies-wall {
446 grid-template-columns: repeat(2, 1fr);
447 }
448}
449
450/* 单个电影项目 */
451.movie-item {
452 width: 100%;
453 margin-bottom: 10px;
454}
455
456.movie-cover {
457 position: relative;
458 overflow: hidden;
459 aspect-ratio: 2 / 3;
460 border-radius: 5px;
461 box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
462}
463
464.movie-cover img {
465 width: 100%;
466 height: 100%;
467 object-fit: cover;
468 transition: transform 0.3s ease;
469}
470
471.movie-info {
472 position: absolute;
473 top: 0;
474 left: 0;
475 right: 0;
476 bottom: 0;
477 background: rgba(0, 0, 0, 0.8);
478 color: #fff;
479 padding: 15px;
480 opacity: 0;
481 transition: opacity 0.3s ease;
482 overflow: auto;
483 display: flex;
484 flex-direction: column;
485 justify-content: center;
486}
487
488.movie-cover:hover img {
489 transform: scale(1.1);
490}
491
492.movie-cover:hover .movie-info {
493 opacity: 1;
494}
495
496/* 电影信息样式 */
497.movie-title {
498 font-size: 1.1em;
499 font-weight: bold;
500 margin-bottom: 10px;
501}
502
503.movie-title a {
504 color: #fff;
505 text-decoration: none;
506}
507
508.movie-title a:hover {
509 text-decoration: underline;
510}
511
512/* 评分星星样式 */
513.movie-rating {
514 display: flex;
515 align-items: center;
516 margin: 8px 0;
517}
518
519.rating .allstardark {
520 display: inline-block;
521 position: relative;
522 color: #f99b01;
523 height: 16px;
524 width: 80px;
525 background-size: auto 100%;
526 margin-right: 8px;
527 background-repeat: repeat;
528 background-image: url(data:image/svg+xml;base64,PHN2ZyBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iMzIiIGhlaWdodD0iMzIiPjxwYXRoIGQ9Ik05MDguMSAzNTMuMWwtMjUzLjktMzYuOUw1NDAuNyA4Ni4xYy0zLjEtNi4zLTguMi0xMS40LTE0LjUtMTQuNS0xNS44LTcuOC0zNS0xLjMtNDIuOSAxNC41TDM2OS44IDMxNi4ybC0yNTMuOSAzNi45Yy03IDEtMTMuNCA0LjMtMTguMyA5LjMtMTIuMyAxMi43LTEyLjEgMzIuOS42IDQ1LjNsMTgzLjcgMTc5LjEtNDMuNCAyNTIuOWMtMS4yIDYuOS0uMSAxNC4xIDMuMiAyMC4zIDguMiAxNS42IDI3LjYgMjEuNyA0My4yIDEzLjRMNTEyIDc1NGwyMjcuMSAxMTkuNGM2LjIgMy4zIDEzLjQgNC40IDIwLjMgMy4yIDE3LjQtMyAyOS4xLTE5LjUgMjYuMS0zNi45bC00My40LTI1Mi45IDE4My43LTE3OS4xYzUtNC45IDguMy0xMS4zIDkuMy0xOC4zIDIuNy0xNy41LTkuNS0zMy43LTI3LTM2LjN6TTY2NC44IDU2MS42bDM2LjEgMjEwLjNMNTEyIDY3Mi43IDMyMy4xIDc3MmwzNi4xLTIxMC4zLTE1Mi44LTE0OUw0MTcuNiAzODIgNTEyIDE5MC43IDYwNi40IDM4MmwyMTEuMiAzMC43LTE1Mi44IDE0OC45eiIgZmlsbD0iI2Y5OWIwMSIvPjwvc3ZnPg==);
529}
530
531.rating .allstarlight {
532 position: absolute;
533 left: 0;
534 color: #f99b01;
535 height: 16px;
536 overflow: hidden;
537 background-size: auto 100%;
538 background-repeat: repeat;
539 background-image: url(data:image/svg+xml;base64,PHN2ZyBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iMzIiIGhlaWdodD0iMzIiPjxwYXRoIGQ9Ik05MDguMSAzNTMuMWwtMjUzLjktMzYuOUw1NDAuNyA4Ni4xYy0zLjEtNi4zLTguMi0xMS40LTE0LjUtMTQuNS0xNS44LTcuOC0zNS0xLjMtNDIuOSAxNC41TDM2OS44IDMxNi4ybC0yNTMuOSAzNi45Yy03IDEtMTMuNCA0LjMtMTguMyA5LjMtMTIuMyAxMi43LTEyLjEgMzIuOS42IDQ1LjNsMTgzLjcgMTc5LjEtNDMuNCAyNTIuOWMtMS4yIDYuOS0uMSAxNC4xIDMuMiAyMC4zIDguMiAxNS42IDI3LjYgMjEuNyA0My4yIDEzLjRMNTEyIDc1NGwyMjcuMSAxMTkuNGM2LjIgMy4zIDEzLjQgNC40IDIwLjMgMy4yIDE3LjQtMyAyOS4xLTE5LjUgMjYuMS0zNi45bC00My40LTI1Mi45IDE4My43LTE3OS4xYzUtNC45IDguMy0xMS4zIDkuMy0xOC4zIDIuNy0xNy41LTkuNS0zMy43LTI3LTM2LjN6IiBmaWxsPSIjZjk5YjAxIi8+PC9zdmc+);
540}
541
542.rating .rating_nums {
543 font-size: 1.0em;
544 color: #fff;
545}
546
547.movie-card {
548 margin: 8px 0;
549 font-size: 0.8em;
550 color: #ddd;
551}
552
553.movie-comment {
554 margin: 8px 0;
555 font-size: 0.8em;
556 color: #ddd;
557 font-style: italic;
558}
559
560/* 筛选和排序控制 */
561.filter-sort-controls {
562 margin: 15px;
563 padding: 10px;
564 background-color: #f5f5f5;
565 border-radius: 5px;
566}
567
568.filter-container {
569 display: flex;
570 justify-content: space-between;
571 flex-wrap: wrap;
572 gap: 10px;
573}
574
575.filter-section, .sort-section {
576 position: relative;
577 flex: 1;
578 min-width: 200px;
579}
580
581.filter-button, .sort-button {
582 width: 100%;
583 padding: 8px 12px;
584 background-color: #3498db;
585 color: white;
586 border: none;
587 border-radius: 4px;
588 cursor: pointer;
589 text-align: left;
590 display: flex;
591 justify-content: space-between;
592 align-items: center;
593}
594
595.filter-options, .sort-options {
596 display: none;
597 position: absolute;
598 top: 100%;
599 left: 0;
600 right: 0;
601 background-color: white;
602 border: 1px solid #ddd;
603 border-radius: 4px;
604 padding: 10px;
605 z-index: 10;
606 max-height: 300px;
607 overflow-y: auto;
608 box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
609}
610
611.filter-options.show, .sort-options.show {
612 display: block;
613}
614
615.filter-group {
616 margin-bottom: 10px;
617}
618
619.filter-group h4 {
620 margin: 0 0 5px 0;
621 padding-bottom: 5px;
622 border-bottom: 1px solid #eee;
623}
624
625.filter-checkbox {
626 display: block;
627 margin: 3px 0;
628 cursor: pointer;
629}
630
631.sort-option {
632 display: block;
633 width: 100%;
634 padding: 5px 10px;
635 margin: 3px 0;
636 text-align: left;
637 background: none;
638 border: none;
639 cursor: pointer;
640}
641
642.sort-option:hover, .sort-option.active {
643 background-color: #f0f0f0;
644}
645
646.reset-filters {
647 display: block;
648 width: 100%;
649 padding: 5px 10px;
650 margin-top: 10px;
651 background-color: #f0f0f0;
652 border: 1px solid #ddd;
653 border-radius: 3px;
654 cursor: pointer;
655}
656
657/* 分页控制 */
658.pagination-controls {
659 display: flex;
660 justify-content: center;
661 align-items: center;
662 margin: 20px 0;
663 gap: 15px;
664}
665
666.pagination-controls button {
667 padding: 8px 15px;
668 background-color: #3498db;
669 color: white;
670 border: none;
671 border-radius: 4px;
672 cursor: pointer;
673}
674
675.pagination-controls button:hover:not([disabled]) {
676 background-color: #2980b9;
677}
678
679.pagination-controls button[disabled] {
680 background-color: #ccc;
681 cursor: not-allowed;
682}
683
684#pageInfo {
685 font-size: 0.9em;
686 color: #666;
687}
688
689/* 新增筛选按钮样式 */
690.filter-options-grid {
691 display: grid;
692 grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
693 gap: 8px;
694 padding: 10px 0;
695 }
696
697 .filter-option-btn {
698 padding: 6px 10px;
699 border: 1px solid #ddd;
700 border-radius: 15px;
701 background: #f8f9fa;
702 cursor: pointer;
703 transition: all 0.2s;
704 font-size: 0.9em;
705 white-space: nowrap;
706 }
707
708 .filter-option-btn.active {
709 background: #3498db;
710 color: white;
711 border-color: #3498db;
712 }
713</style>
714{{- end -}}
参考
文章标题: Hugo系列(五):添加豆瓣观影页面
文章链接: https://blog.grew.cc/posts/hugo-douban/
版权声明: 本博客所有文章除特别声明外,均采用CC BY-NC-SA 4.0。