Hugo系列(五):添加豆瓣观影页面

通过 GitHub doumark-action 保存豆瓣观影数据,并给 Hugo 博客添加豆瓣观影页面。

Note

2025.03.29 更新了一下模板文件,添加了筛选排序功能。

已将 data.GetCSV 函数替换,Hugo 会在 0.136 版本移除,目前最新版本是 0.135。 取而代之的是 resources.Gettransform.Unmarshal,由于 resource 函数获取全局文件路径是 ~/assets/,因此也替换了对应文件的存放路径。

前言

Hexohexo-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.csvbook.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