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

前言

Hexohexo-douban 项目,可以添加观影数据到页面,更换 Hugo 后也想添加一个观影页面,查找资料后发现有一个 doumark-action 项目可以将观影数据保存为本地文件,然后可以通过 Hugo 的函数获取数据,再前端展示,前面的文章中已经添加了豆瓣条目的短代码,这篇文章再补充两个,一个是近期观影的短代码,另外是添加一个海报墙页面。 最终实现的样式布局参考了一些博主的文章和仓库代码:

保存本地数据

由于豆瓣 API 的限制,之前的一些在线获取数据方式已经失效了,可以通过在线爬取标记的数据到本地,然后操作本地的数据文件,再前端展示数据。 用到的项目是 doumark-action。 首先在博客仓库的根目录新建一个 Workflow,新建 .github/workflows/douban.yml 文件,在其中写入自己想要保存的数据,影音、书籍,也可以保存到 Notion,项目有详细的说明,这里贴一下我的配置,其中将 id 改为自己的豆瓣 ID

YAML
# .github/workflows/douban.yml
name: douban
on: 
  schedule:
  - cron: "30 * * * *"
  workflow_dispatch:

jobs:
  douban:
    name: Douban mark data sync
    runs-on: ubuntu-latest
    steps:
    - name: Checkout
      uses: actions/checkout@v2

    - name: movie
      uses: lizheming/doumark-action@master
      with:
        id: 222317686
        type: movie
        format: csv
        dir: ./assets/data/douban
    
    - name: book
      uses: lizheming/doumark-action@master
      with:
        id: 222317686
        type: book
        format: csv
        dir: ./assets/data/douban
  
    - name: Commit
      uses: EndBug/add-and-commit@v8
      with:
        message: 'chore: update douban data'
        add: './assets/data/douban'
点击展开查看更多

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,在其中添加模板元素:

HTML
{{ $type := .Get "type" }}
{{ $count := .Get "count" | default 4 }}
{{ $count = add $count 1 }}
{{ $items := slice }}
{{ $csvPath := "" }}
{{ if eq $type "movies" }}
    {{ $csvPath = "data/douban/movie.csv" }}
{{ else if eq $type "books" }}
    {{ $csvPath = "data/douban/book.csv" }}
{{ end }}

{{ with resources.Get $csvPath }}
    {{ $opts := dict "delimiter" "," }}
    {{ $items = . | transform.Unmarshal $opts }}
{{ else }}
    {{ errorf "无法获取资源 %q" $csvPath }}
{{ end }}

<div class="recent-items">
    {{ range $idx, $item := first $count $items }}
        {{ if ne $idx 0 }}
            {{ $rating := float (index $item 6) }}
            <div class="recent-item">
                <div class="recent-item-cover">
                    <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">
                </div>
                <div class="recent-douban-rating">
                    <div class="rating">
                        <span class="allstardark">
                            <span class="allstarlight" style="width:{{ mul 10 $rating }}%"></span>
                        </span>
                        <span class="rating_nums">{{ $rating }}</span>
                    </div>
                </div>
                <div class="recent-item-title">
                    <a rel="noreferrer" href="{{ index $item 5 }}" target="_blank">{{ index $item 1 }}</a>
                </div>
            </div>
        {{ end }}
    {{ end }}
</div>
点击展开查看更多

然后添加一下样式代码,这里和之前的文章保持一致,添加到 shortcodes.scss 中:

SCSS
.recent-items {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
    margin: 15px;
}
.recent-items .recent-item, .recent-items .recent-item img {
    margin-bottom: 10px;
}
点击展开查看更多

使用

使用方法如下:

PLAINTEXT
{\{< recent-douban type="movies" count=8 >}\}
{\{< recent-douban type="books" >}\}
点击展开查看更多

默认展示四条数据,可以通过 count 参数指定显示的数量。

观影页面

配置

添加一个海报墙页面展示所有的观影信息,默认展示 18 条信息。

首先在 ~/lauouts/_default/ 文件夹下新建模板文件,这里为 posterwall.html,然后在其中添加页面元素:

HTML
{{- define "title" -}}
  {{- title .Title -}}
  {{- if .Site.Params.withSiteTitle }} {{ .Site.Params.titleDelimiter }} {{ .Site.Title }}{{- end -}}
{{- end -}}

{{- define "content" -}}
  {{- $title := title .Title -}}
  {{- $params := partial "function/params.html" -}}
  {{- $toc := .Scratch.Get "toc" -}}
  {{- $tocEmpty := eq .TableOfContents `<nav id="TableOfContents"></nav>` -}}
<div class="posterwall-wrapper">
  <div class="posterwall-description">
    {{ .Params.description | markdownify }}
  </div>
  <div class="movie-wall">
    {{ $csvPath := "data/douban/movie.csv" }}
    {{ $items := slice }}
    {{ with resources.Get $csvPath }}
        {{ $opts := dict "delimiter" "," }}
        {{ $items = . | transform.Unmarshal $opts }}
    {{ else }}
        {{ errorf "无法获取资源 %q" $csvPath }}
    {{ end }}
    {{ range $idx, $item := $items }}
        {{ if ne $idx 0 }}
        {{ $rating := float (index $item 6) }}
        <div class="movie-item" style="display: none;">
            <div class="movie-cover">
            <img src="{{ index $item 3 }}" alt="{{ index $item 1 }}" loading="lazy" width="200" referrer-policy="no-referrer">
            <div class="movie-info">
                <div class="movie-title"><a rel="noreferrer" href="{{ index $item 5 }}" target="_blank">{{ index $item 1 }}</a></div>
                <div class="movie-rating">
                    <div class="rating">
                        <span class="allstardark">
                            <span class="allstarlight" style="width:{{ mul 10 $rating }}%"></span>
                        </span>
                        <span class="rating_nums">{{ index $item 6 }}</span>
                    </div>
                </div>
                <div class="movie-card">{{ index $item 12 }}</div>
                <div class="movie-comment">{{ index $item 9 }}</div>
            </div>
            </div>
        </div>
        {{ end }}
    {{ end }}
  </div>
  <button id="loadMore">加载更多</button>
</div>

<script>
  let visibleMovies = 20;
  const movieItems = document.querySelectorAll('.movie-item');
  const loadMoreButton = document.getElementById('loadMore');

  function updateVisibility() {
    movieItems.forEach((movie, idx) => {
      movie.style.display = idx < visibleMovies ? 'block' : 'none';
    });

    if (visibleMovies >= movieItems.length) {
      loadMoreButton.style.display = 'none';
    }
  }

  loadMoreButton.addEventListener('click', () => {
    visibleMovies += 20;
    updateVisibility();
  });

  updateVisibility();
</script>
{{ end }}
点击展开查看更多

完成后在 custom.scss 中添加样式代码:

SCSS
// 电影海报墙样式
.posterwall-description {
  margin: 10px;
  text-align: center;
}
.movie-wall {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-gap: 3px;
  padding: 3px;
}

.movie-item {
  width: 100%;
  margin-bottom: 0;
}

.movie-cover {
  position: relative;
  overflow: hidden;
  aspect-ratio: 2 / 3;
}

.movie-cover img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 0.3s ease;
}

.movie-info {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.8);
  color: #fff;
  padding: 10px;
  opacity: 0;
  transition: opacity 0.3s ease;
  overflow: auto;
  display: flex;
  flex-direction: column;
  justify-content: center;
}

.movie-cover:hover img {
  transform: scale(1.1);
}

.movie-cover:hover .movie-info {
  opacity: 1;
}

#loadMore {
  display: block;
  margin: 20px auto;
  padding: 10px 20px;
  background-color: lighten($color-accent,0.1);
  color: white;
  border: none;
  cursor: pointer;
}

#loadMore:hover {
  background-color: gray;
}

.movie-info .movie-rating {
  display: flex;
  align-items: center;
  margin-top: 3px;
  margin-bottom: 3px;
}
.movie-info .movie-rating .rating .rating_nums {
  font-size: 1.0em;
  color: #fff;
}

.movie-info .movie-card {
  margin: 5px 0;
  font-size: 0.7em;
  color: #fff;
}

.movie-info .movie-comment {
  margin: 5px 0;
  font-size: 0.7em;
  color: #fff;
}
点击展开查看更多

使用

content 目录下创建一个 posterwall.md 文件,Front Matter 信息填写如下,标题可描述可以自由更改:

YAML
---
title: 海报墙
layout: "posterwall"
description: "这里是我已观看电影的海报墙,数据来源于豆瓣。"
---
点击展开查看更多

添加后就可以在配置文件中添加一个菜单页面来指向海报墙,最终效果可以访问 👉 观影页面

图片地址替换

CSV 文件中的图片地址是 douban-action 作者反代的地址,近期图片挂了,而 Neodb 提供 API,所以这里将图片替换为 Neodb 地址,方法如下, 海报墙页面代码和位置都一致:

{{ $rating := float (index $item 6) }} 这行代码下添加下面两行:

HUGO
{{ $dbUrl := printf "https://neodb.social/api/catalog/fetch?url=%s" (index $item 5) }}
{{ $dbFetch := (resources.GetRemote $dbUrl) | transform.Unmarshal }}
点击展开查看更多

然后将图片地址更换:

DIFF
-src=" {{ index $item 3 }} " 
+src=" {{ $dbFetch.cover_image_url }} "
点击展开查看更多

观影页面更新

HTML
{{- define "title" -}}
  {{- title .Title -}}
  {{- if .Site.Params.withSiteTitle }} {{ .Site.Params.titleDelimiter }} {{ .Site.Title }}{{- end -}}
{{- end -}}

{{- define "main" -}}
  {{- $title := title .Title -}}
<div class="movies-wrapper">
  <div class="movies-description">
    {{ .Params.description | markdownify }}
  </div>
  
<!-- 修改筛选控制区HTML结构 -->
<div class="filter-sort-controls">
    <div class="filter-container">
      <!-- 年份筛选 -->
      <div class="filter-section">
        <button class="filter-button" id="toggleYearFilter">年份筛选 <span class="arrow-down"></span></button>
        <div class="filter-options" id="yearOptions">
          <div class="filter-options-grid" id="yearFilters"></div>
          <button class="reset-filters" data-type="year">重置年份</button>
        </div>
      </div>
      
      <!-- 类型筛选 -->
      <div class="filter-section">
        <button class="filter-button" id="toggleGenreFilter">类型筛选 <span class="arrow-down"></span></button>
        <div class="filter-options" id="genreOptions">
          <div class="filter-options-grid" id="genreFilters"></div>
          <button class="reset-filters" data-type="genre">重置类型</button>
        </div>
      </div>
  
      <!-- 排序 -->
      <div class="sort-section">
        <button class="sort-button" id="toggleSort">排序方式 <span class="arrow-down"></span></button>
        <div class="sort-options" id="sortOptions">
            <button class="sort-option" data-sort="time" data-order="desc">观影时间 (倒序)</button>
            <button class="sort-option" data-sort="time" data-order="asc">观影时间 (正序)</button>
            <button class="sort-option" data-sort="rating" data-order="desc">评分 (高→低)</button>
            <button class="sort-option" data-sort="rating" data-order="asc">评分 (低→高)</button>
        </div>
      </div>
    </div>
  </div>
  
  <div class="movies-wall">
    {{ $csvPath := "data/douban/movie.csv" }}
    {{ $items := slice }}
    {{ with resources.Get $csvPath }}
        {{ $opts := dict "delimiter" "," }}
        {{ $items = . | transform.Unmarshal $opts }}
    {{ else }}
        {{ errorf "无法获取资源 %q" $csvPath }}
    {{ end }}
    
    {{ range $idx, $item := $items }}
        {{ if ne $idx 0 }}
        {{ $id := index $item 0 }}
        {{ $title := index $item 1 }}
        {{ $poster := index $item 3 }}
        {{ $pubdate := index $item 4 }}
        {{ $url := index $item 5 }}
        {{ $rating := float (index $item 6) }}
        {{ $genres := index $item 7 }}
        {{ $comment := index $item 9 }}
        {{ $card := index $item 12 }}
        {{ $star_time := index $item 11 }}
        
        {{ $year := cond (findRE "\\d{4}" $pubdate) (index (findRE "\\d{4}" $pubdate) 0) "" }}
        
        <div class="movie-item" 
             data-id="{{ $id }}" 
             data-year="{{ $year }}" 
             data-genres="{{ $genres }}" 
             data-rating="{{ $rating }}" 
             data-time="{{ $star_time }}">
            <div class="movie-cover">
                <img src="{{ $poster }}" alt="{{ $title }}" loading="lazy" referrer-policy="no-referrer">
                <div class="movie-info">
                    <div class="movie-title"><a rel="noreferrer" href="{{ $url }}" target="_blank">{{ $title }}</a></div>
                    <div class="movie-rating">
                        <div class="rating">
                            <span class="allstardark">
                                <span class="allstarlight" style="width:{{ mul 10 $rating }}%"></span>
                            </span>
                            <span class="rating_nums">{{ $rating }}</span>
                        </div>
                    </div>
                    <div class="movie-card">{{ $card }}</div>
                    {{ if $comment }}
                    <div class="movie-comment">{{ $comment }}</div>
                    {{ end }}
                </div>
            </div>
        </div>
        {{ end }}
    {{ end }}
  </div>
  
  <!-- 分页控制 -->
  <div class="pagination-controls">
    <button id="prevPage" disabled>上一页</button>
    <span id="pageInfo"><span id="currentPage">1</span> 页,共 <span id="totalPages">1</span></span>
    <button id="nextPage">下一页</button>
  </div>
</div>

<script>
    document.addEventListener('DOMContentLoaded', function() {
      // 电影项元素
      const movieItems = document.querySelectorAll('.movie-item');
      // 筛选和排序元素
      const toggleYearFilter = document.getElementById('toggleYearFilter');
      const yearOptions = document.getElementById('yearOptions');
      const toggleGenreFilter = document.getElementById('toggleGenreFilter');
      const genreOptions = document.getElementById('genreOptions');
      const toggleSort = document.getElementById('toggleSort');
      const sortOptions = document.getElementById('sortOptions');
      const yearFilters = document.getElementById('yearFilters');
      const genreFilters = document.getElementById('genreFilters');
      const sortButtons = document.querySelectorAll('.sort-option');
      
      // 分页元素
      const prevPageBtn = document.getElementById('prevPage');
      const nextPageBtn = document.getElementById('nextPage');
      const currentPageEl = document.getElementById('currentPage');
      const totalPagesEl = document.getElementById('totalPages');
      
      // 分页配置
      let itemsPerRow = window.innerWidth < 768 ? 2 : (window.innerWidth < 992 ? 3 : 6);
      const rowsPerPage = 5;
      let itemsPerPage = itemsPerRow * rowsPerPage;
      
      // 筛选和排序的状态
      let activeFilters = {
        years: [],
        genres: []
      };
      let currentSort = {
        field: 'time',
        order: 'desc'
      };
      let currentPage = 1;
      
      // 初始化
      initializeFilters();
      updateDisplay();
      
      // 初始化筛选器(修改部分)
      function initializeFilters() {
        // 收集所有年份和类型
        const years = new Set();
        const genres = new Set();
        
        movieItems.forEach(item => {
          const year = item.dataset.year;
          if (year) years.add(year);
          
          const genreList = item.dataset.genres.split(',');
          genreList.forEach(genre => {
            if (genre) genres.add(genre.trim());
          });
        });
        
        // 生成年份按钮
        const sortedYears = Array.from(years).sort((a, b) => b - a);
        sortedYears.forEach(year => {
          const btn = document.createElement('button');
          btn.className = 'filter-option-btn';
          btn.textContent = year;
          btn.dataset.value = year;
          btn.addEventListener('click', function() {
            this.classList.toggle('active');
            const index = activeFilters.years.indexOf(year);
            if (index === -1) {
              activeFilters.years.push(year);
            } else {
              activeFilters.years.splice(index, 1);
            }
            currentPage = 1;
            updateDisplay();
          });
          yearFilters.appendChild(btn);
        });
  
        // 生成类型按钮 
        const sortedGenres = Array.from(genres).sort();
        sortedGenres.forEach(genre => {
          const btn = document.createElement('button');
          btn.className = 'filter-option-btn';
          btn.textContent = genre;
          btn.dataset.value = genre;
          btn.addEventListener('click', function() {
            this.classList.toggle('active');
            const index = activeFilters.genres.indexOf(genre);
            if (index === -1) {
              activeFilters.genres.push(genre);
            } else {
              activeFilters.genres.splice(index, 1);
            }
            currentPage = 1;
            updateDisplay();
          });
          genreFilters.appendChild(btn);
        });
  
        // 重置筛选逻辑(修改部分)
        document.querySelectorAll('.reset-filters').forEach(btn => {
          btn.addEventListener('click', function() {
            const type = this.dataset.type;
            if (type === 'year') {
              activeFilters.years = [];
              yearFilters.querySelectorAll('.active').forEach(b => b.classList.remove('active'));
            } else {
              activeFilters.genres = [];
              genreFilters.querySelectorAll('.active').forEach(b => b.classList.remove('active'));
            }
            currentPage = 1;
            updateDisplay();
          });
        });
      }
  
      // 筛选电影(保持原有逻辑)
      function filterMovies() {
        const filtered = Array.from(movieItems).filter(item => {
          const year = item.dataset.year;
          const genres = item.dataset.genres.split(',').map(g => g.trim());
          
          const yearMatch = activeFilters.years.length === 0 || activeFilters.years.includes(year);
          const genreMatch = activeFilters.genres.length === 0 || 
                            activeFilters.genres.some(g => genres.includes(g));
          
          return yearMatch && genreMatch;
        });
        
        return filtered;
      }
  
      // 排序电影(保持原有逻辑)
      function sortMovies(movies) {
        const sorted = movies.sort((a, b) => {
          let valueA, valueB;
          
          if (currentSort.field === 'time') {
            valueA = a.dataset.time;
            valueB = b.dataset.time;
          } else {
            valueA = parseFloat(a.dataset.rating);
            valueB = parseFloat(b.dataset.rating);
          }
          
          if (valueA === valueB) {
            return 0;
          }
          
          return currentSort.order === 'asc' ? 
            (valueA > valueB ? 1 : -1) : 
            (valueA < valueB ? 1 : -1);
        });
        
        
        return sorted;
      }
  
      // 更新显示(保持原有逻辑)
      function updateDisplay() {
        // 获取所有电影项目的原始引用
        const allMovieItems = Array.from(document.querySelectorAll('.movie-item'));
        
        // 应用筛选
        const filteredMovies = filterMovies();
        
        // 应用排序
        const sortedMovies = sortMovies(filteredMovies);
        
        // 更新分页信息
        const totalPages = Math.ceil(sortedMovies.length / itemsPerPage) || 1;
        currentPage = currentPage > totalPages ? totalPages : currentPage;
        currentPageEl.textContent = currentPage;
        totalPagesEl.textContent = totalPages;
        
        // 更新分页按钮状态
        prevPageBtn.disabled = currentPage <= 1;
        nextPageBtn.disabled = currentPage >= totalPages;
      
        // 计算当前页面应显示的范围
        const startIdx = (currentPage - 1) * itemsPerPage;
        const endIdx = startIdx + itemsPerPage;
        
        // 先隐藏所有电影
        allMovieItems.forEach(item => {
          item.style.display = 'none';
        });
        
        // 只显示当前页中筛选和排序后的电影
        sortedMovies.forEach((item, index) => {
          if (index >= startIdx && index < endIdx) {
            item.style.display = 'block';
          }
        });
        
        // 重新排序DOM元素 (仅处理筛选后的元素)
        const moviesWall = document.querySelector('.movies-wall');
        sortedMovies.forEach(item => {
          moviesWall.appendChild(item);
        });
      }
      
      // 修改 filterMovies 函数以确保它正确处理筛选逻辑
      function filterMovies() {
        return Array.from(movieItems).filter(item => {
          const year = item.dataset.year;
          const genres = item.dataset.genres.split(',').map(g => g.trim());
          
          // 年份筛选逻辑
          const yearMatch = activeFilters.years.length === 0 || 
                          activeFilters.years.includes(year);
          
          // 类型筛选逻辑
          const genreMatch = activeFilters.genres.length === 0 || 
                          activeFilters.genres.some(g => genres.includes(g));
          
          // 调试输出
          // console.log(`电影: ${item.querySelector('.movie-title')?.textContent} - 年份匹配: ${yearMatch}, 类型匹配: ${genreMatch}`);
          
          return yearMatch && genreMatch;
        });
      }



      // 事件监听(修改部分)
      toggleYearFilter.addEventListener('click', function() {
        yearOptions.classList.toggle('show');
        this.querySelector('.arrow-down').textContent = 
          yearOptions.classList.contains('show') ? '▲' : '▼';
      });
  
      toggleGenreFilter.addEventListener('click', function() {
        genreOptions.classList.toggle('show');
        this.querySelector('.arrow-down').textContent = 
          genreOptions.classList.contains('show') ? '▲' : '▼';
      });
  
      toggleSort.addEventListener('click', function() {
        sortOptions.classList.toggle('show');
        this.querySelector('.arrow-down').textContent = 
          sortOptions.classList.contains('show') ? '▲' : '▼';
      });
  
      // 保持原有排序逻辑
      sortButtons.forEach(button => {
        button.addEventListener('click', function() {
          currentSort.field = this.dataset.sort;
          currentSort.order = this.dataset.order;
          currentPage = 1;
          
          sortButtons.forEach(btn => btn.classList.remove('active'));
          this.classList.add('active');
          updateDisplay();
          
          sortOptions.classList.remove('show');
          toggleSort.querySelector('.arrow-down').textContent = '▼';
        });
      });
  
      // 分页控制(保持原有逻辑)
      prevPageBtn.addEventListener('click', () => {
        if (currentPage > 1) {
          currentPage--;
          updateDisplay();
        }
      });
  
      nextPageBtn.addEventListener('click', () => {
        const filteredMovies = filterMovies();
        const totalPages = Math.ceil(filteredMovies.length / itemsPerPage);
        if (currentPage < totalPages) {
          currentPage++;
          updateDisplay();
        }
      });
  
      // 点击外部关闭(修改部分)
      document.addEventListener('click', function(event) {
        const isFilter = event.target.closest('.filter-section');
        const isSort = event.target.closest('.sort-section'); // 新增排序区域判断
        
        if (!isFilter && !isSort) { // 同时检查两个区域
          [yearOptions, genreOptions, sortOptions].forEach(panel => {
            if (panel.classList.contains('show')) {
              panel.classList.remove('show');
              // 修复箭头方向更新逻辑
              const toggleBtn = document.querySelector(`#${panel.id.replace('Options','Filter')}`);
              if(toggleBtn) {
                toggleBtn.querySelector('.arrow-down').textContent = '▼';
              }
            }
          });
        }
      });
  
      // 响应式处理(保持原有逻辑)
      window.addEventListener('resize', function() {
        const newItemsPerRow = window.innerWidth < 768 ? 2 : (window.innerWidth < 992 ? 3 : 6);
        if (newItemsPerRow !== itemsPerRow) {
          itemsPerRow = newItemsPerRow;
          itemsPerPage = itemsPerRow * rowsPerPage;
          updateDisplay();
        }
      });
    });
  </script>

<style>
/* 电影墙容器 */
.movies-wrapper {
  margin: 20px auto;
  max-width: 1200px;
}

.movies-description {
  margin: 10px;
  text-align: center;
}

/* 电影墙网格布局 */
.movies-wall {
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  grid-gap: 15px;
  padding: 15px;
}

/* 响应式布局 */
@media (max-width: 992px) {
  .movies-wall {
    grid-template-columns: repeat(3, 1fr);
  }
}

@media (max-width: 768px) {
  .movies-wall {
    grid-template-columns: repeat(2, 1fr);
  }
}

/* 单个电影项目 */
.movie-item {
  width: 100%;
  margin-bottom: 10px;
}

.movie-cover {
  position: relative;
  overflow: hidden;
  aspect-ratio: 2 / 3;
  border-radius: 5px;
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}

.movie-cover img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 0.3s ease;
}

.movie-info {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.8);
  color: #fff;
  padding: 15px;
  opacity: 0;
  transition: opacity 0.3s ease;
  overflow: auto;
  display: flex;
  flex-direction: column;
  justify-content: center;
}

.movie-cover:hover img {
  transform: scale(1.1);
}

.movie-cover:hover .movie-info {
  opacity: 1;
}

/* 电影信息样式 */
.movie-title {
  font-size: 1.1em;
  font-weight: bold;
  margin-bottom: 10px;
}

.movie-title a {
  color: #fff;
  text-decoration: none;
}

.movie-title a:hover {
  text-decoration: underline;
}

/* 评分星星样式 */
.movie-rating {
  display: flex;
  align-items: center;
  margin: 8px 0;
}

.rating .allstardark {
  display: inline-block;
  position: relative;
  color: #f99b01;
  height: 16px;
  width: 80px;
  background-size: auto 100%;
  margin-right: 8px;
  background-repeat: repeat;
  background-image: url(data:image/svg+xml;base64,PHN2ZyBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iMzIiIGhlaWdodD0iMzIiPjxwYXRoIGQ9Ik05MDguMSAzNTMuMWwtMjUzLjktMzYuOUw1NDAuNyA4Ni4xYy0zLjEtNi4zLTguMi0xMS40LTE0LjUtMTQuNS0xNS44LTcuOC0zNS0xLjMtNDIuOSAxNC41TDM2OS44IDMxNi4ybC0yNTMuOSAzNi45Yy03IDEtMTMuNCA0LjMtMTguMyA5LjMtMTIuMyAxMi43LTEyLjEgMzIuOS42IDQ1LjNsMTgzLjcgMTc5LjEtNDMuNCAyNTIuOWMtMS4yIDYuOS0uMSAxNC4xIDMuMiAyMC4zIDguMiAxNS42IDI3LjYgMjEuNyA0My4yIDEzLjRMNTEyIDc1NGwyMjcuMSAxMTkuNGM2LjIgMy4zIDEzLjQgNC40IDIwLjMgMy4yIDE3LjQtMyAyOS4xLTE5LjUgMjYuMS0zNi45bC00My40LTI1Mi45IDE4My43LTE3OS4xYzUtNC45IDguMy0xMS4zIDkuMy0xOC4zIDIuNy0xNy41LTkuNS0zMy43LTI3LTM2LjN6TTY2NC44IDU2MS42bDM2LjEgMjEwLjNMNTEyIDY3Mi43IDMyMy4xIDc3MmwzNi4xLTIxMC4zLTE1Mi44LTE0OUw0MTcuNiAzODIgNTEyIDE5MC43IDYwNi40IDM4MmwyMTEuMiAzMC43LTE1Mi44IDE0OC45eiIgZmlsbD0iI2Y5OWIwMSIvPjwvc3ZnPg==);
}

.rating .allstarlight {
  position: absolute;
  left: 0;
  color: #f99b01;
  height: 16px;
  overflow: hidden;
  background-size: auto 100%;
  background-repeat: repeat;
  background-image: url(data:image/svg+xml;base64,PHN2ZyBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iMzIiIGhlaWdodD0iMzIiPjxwYXRoIGQ9Ik05MDguMSAzNTMuMWwtMjUzLjktMzYuOUw1NDAuNyA4Ni4xYy0zLjEtNi4zLTguMi0xMS40LTE0LjUtMTQuNS0xNS44LTcuOC0zNS0xLjMtNDIuOSAxNC41TDM2OS44IDMxNi4ybC0yNTMuOSAzNi45Yy03IDEtMTMuNCA0LjMtMTguMyA5LjMtMTIuMyAxMi43LTEyLjEgMzIuOS42IDQ1LjNsMTgzLjcgMTc5LjEtNDMuNCAyNTIuOWMtMS4yIDYuOS0uMSAxNC4xIDMuMiAyMC4zIDguMiAxNS42IDI3LjYgMjEuNyA0My4yIDEzLjRMNTEyIDc1NGwyMjcuMSAxMTkuNGM2LjIgMy4zIDEzLjQgNC40IDIwLjMgMy4yIDE3LjQtMyAyOS4xLTE5LjUgMjYuMS0zNi45bC00My40LTI1Mi45IDE4My43LTE3OS4xYzUtNC45IDguMy0xMS4zIDkuMy0xOC4zIDIuNy0xNy41LTkuNS0zMy43LTI3LTM2LjN6IiBmaWxsPSIjZjk5YjAxIi8+PC9zdmc+);
}

.rating .rating_nums {
  font-size: 1.0em;
  color: #fff;
}

.movie-card {
  margin: 8px 0;
  font-size: 0.8em;
  color: #ddd;
}

.movie-comment {
  margin: 8px 0;
  font-size: 0.8em;
  color: #ddd;
  font-style: italic;
}

/* 筛选和排序控制 */
.filter-sort-controls {
  margin: 15px;
  padding: 10px;
  background-color: #f5f5f5;
  border-radius: 5px;
}

.filter-container {
  display: flex;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 10px;
}

.filter-section, .sort-section {
  position: relative;
  flex: 1;
  min-width: 200px;
}

.filter-button, .sort-button {
  width: 100%;
  padding: 8px 12px;
  background-color: #3498db;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  text-align: left;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.filter-options, .sort-options {
  display: none;
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  background-color: white;
  border: 1px solid #ddd;
  border-radius: 4px;
  padding: 10px;
  z-index: 10;
  max-height: 300px;
  overflow-y: auto;
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}

.filter-options.show, .sort-options.show {
  display: block;
}

.filter-group {
  margin-bottom: 10px;
}

.filter-group h4 {
  margin: 0 0 5px 0;
  padding-bottom: 5px;
  border-bottom: 1px solid #eee;
}

.filter-checkbox {
  display: block;
  margin: 3px 0;
  cursor: pointer;
}

.sort-option {
  display: block;
  width: 100%;
  padding: 5px 10px;
  margin: 3px 0;
  text-align: left;
  background: none;
  border: none;
  cursor: pointer;
}

.sort-option:hover, .sort-option.active {
  background-color: #f0f0f0;
}

.reset-filters {
  display: block;
  width: 100%;
  padding: 5px 10px;
  margin-top: 10px;
  background-color: #f0f0f0;
  border: 1px solid #ddd;
  border-radius: 3px;
  cursor: pointer;
}

/* 分页控制 */
.pagination-controls {
  display: flex;
  justify-content: center;
  align-items: center;
  margin: 20px 0;
  gap: 15px;
}

.pagination-controls button {
  padding: 8px 15px;
  background-color: #3498db;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.pagination-controls button:hover:not([disabled]) {
  background-color: #2980b9;
}

.pagination-controls button[disabled] {
  background-color: #ccc;
  cursor: not-allowed;
}

#pageInfo {
  font-size: 0.9em;
  color: #666;
}

/* 新增筛选按钮样式 */
.filter-options-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
    gap: 8px;
    padding: 10px 0;
  }
  
  .filter-option-btn {
    padding: 6px 10px;
    border: 1px solid #ddd;
    border-radius: 15px;
    background: #f8f9fa;
    cursor: pointer;
    transition: all 0.2s;
    font-size: 0.9em;
    white-space: nowrap;
  }
  
  .filter-option-btn.active {
    background: #3498db;
    color: white;
    border-color: #3498db;
  }
</style>
{{- end -}}
点击展开查看更多

参考

版权声明

作者: Tom Almighty

链接: https://blog.grew.cc/posts/hugo-douban/

许可证: CC BY-NC-SA 4.0

本文采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

评论

开始搜索

输入关键词搜索文章内容

↑↓
ESC
⌘K 快捷键