通过 GitHub doumark-action 保存豆瓣观影数据,并给 Hugo 博客添加豆瓣观影页面。
注意
2025.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
。
# .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'
提示
如果使用 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
,在其中添加模板元素:
{{ $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
中:
.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;
}
重要
之前的豆瓣条目短代码样式文件已经添加了评分星级的样式 allstarlight
,所以这里不添加,如果之前没添加过的话需要添加相关的样式,在之前的文章可以找到。
使用
使用方法如下:
{\{< recent-douban type="movies" count=8 >}\}
{\{< recent-douban type="books" >}\}
默认展示四条数据,可以通过 count
参数指定显示的数量。
观影页面
配置
添加一个海报墙页面展示所有的观影信息,默认展示 18
条信息。
首先在 ~/lauouts/_default/
文件夹下新建模板文件,这里为 posterwall.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 }}
重要
- 代码中包括了页面元素和加载更多数据的
js
代码,并且在页面标题下添加了一个页面描述,方便在 markdown 文件中更改描述信息,也便于调整样式。 - 同样页面中评分星级的命名
allstarlight
也是与之前豆瓣条目短代码一致,方便一起调整。 - 顶部的元素适应主题
Fixit
,其他主题自行更改。
完成后在 custom.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
信息填写如下,标题可描述可以自由更改:
---
title: 海报墙
layout: "posterwall"
description: "这里是我已观看电影的海报墙,数据来源于豆瓣。"
---
添加后就可以在配置文件中添加一个菜单页面来指向海报墙,最终效果可以访问 👉 观影页面
图片地址替换
CSV
文件中的图片地址是 douban-action
作者反代的地址,近期图片挂了,而 Neodb
提供 API
,所以这里将图片替换为 Neodb
地址,方法如下, 海报墙页面代码和位置都一致:
在 {{ $rating := float (index $item 6) }}
这行代码下添加下面两行:
{{ $dbUrl := printf "https://neodb.social/api/catalog/fetch?url=%s" (index $item 5) }}
{{ $dbFetch := (resources.GetRemote $dbUrl) | transform.Unmarshal }}
然后将图片地址更换:
-src=" {{ index $item 3 }} "
+src=" {{ $dbFetch.cover_image_url }} "
观影页面更新
{{- 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 -}}
评论