Hexo 魔改 - 创建 "我的游戏" 页面

一、效果预览

二、创建页面

/source/ 目录下创建 games 文件夹及 index.md 文件并修改

1
2
3
4
5
6
---
date: 2023-10-18 15:08:13
type: 'games'
comments: true
aside: false
---

三、创建数据文件

温馨提示:
请先准备好数据文件再执行脚本

source/_data/ 创建 games.yml 文件并修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
- class_name: 游戏世界
description: 我的游戏世界
tip: Pupper 一起探索世界
top_background: https://th.bing.com/th/id/R.13a97ef4830efa5e0b87134d622719f3?rik=G7RaJFpxg5PtkA&riu=http%3a%2f%2fupload.techweb.com.cn%2fs%2f640%2f2019%2f0530%2f1559208230699.jpg&ehk=j1G8rMX98TRX52EkLgI5jW1p7lIQp4I8Si1nqEggFRs%3d&risl=&pid=ImgRaw&r=0&sres=1&sresct=1
buttonText: Steam
buttonLink: https://steamcommunity.com/profiles/76561198159241291/
games_card: https://card.yuy1n.io/card/76561198159241291/dark,badge,group,bg-730
games:
- title: 游戏世界
description: 各种不要钱的单机游戏
games_list:
- name: 我的世界
en_name: Minecraft
link: https://www.minecraft.net/zh-hans
img: https://www.minecraft.net/content/dam/games/minecraft/key-art/Games_Subnav_Minecraft-300x465.jpg
totalTime: 999 小时
lastTime: 昨天
num: 1/1
achievement: 100%

steam:
- title: Steam
description: 打个折真难, G胖 学学隔壁 epic
games_list:
- name: 反恐精英2
en_name: Counter-Strike 2
link: https://pupper.cn
img: https://cdn.cloudflare.steamstatic.com/steam/apps/346110/header.jpg
totalTime: 921.3 小时
lastTime: 昨天
num: 1/1
achievement: 100%

3.1 steam 卡片获取

免费获取网址: Steam Card , 使用 steam 授权登录即可使用

3.2 脚本获取 steam 游戏库

获取 web key: https://steamcommunity.com/dev/apikey
获取 steamid: https://www.steamidfinder.com/lookup/

3.2.1 使用 python 脚本

温馨提示:
接口请求可能很慢,执行脚本时请耐心等待

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
import json
import re
import yaml
import time
import requests


key = "************"
steamid = "************"


def get_steam_data(key, steamid):
url = f"http://api.steampowered.com/IPlayerService/GetOwnedGames/v1?include_appinfo=true&key={key}&steamid={steamid}&skip_unvettend_apps=false&include_played_free_games%3D1=true&format=json"
response = requests.get(url)
return json.loads(response.text)["response"]["games"]


def get_achievement_data(appid, key, steamid):
achievement_url = f"https://api.steampowered.com/ISteamUserStats/GetPlayerAchievements/v0001/?appid={appid}&key={key}&steamid={steamid}&l=zh_CN"
res = requests.get(achievement_url)
return json.loads(res.text)


def load_yaml_file(file_path):
with open(file_path, "r", encoding="utf8") as file:
return yaml.safe_load(file)


def save_yaml_file(file_path, data):
with open(file_path, "w", encoding="utf8") as fw:
yaml.dump(data, fw, allow_unicode=True, sort_keys=False)


def baike(en_name):
url = f"https://baike.baidu.com/search/word?fromModule=lemma_search-box&word={en_name}"
res = requests.request("get", url)
pattern = r"<title>(.*?)_百度百科</title>"
match = re.search(pattern, res.text)
if match:
return match.group(1)
else:
return en_name


def main(file):
steam_data = get_steam_data(key, steamid)
file_data = load_yaml_file(file)
game_list = file_data[0]["steam"][0]["games_list"]

for i, game in enumerate(steam_data):
if i >= len(game_list):
game_list.append({})

game_list[i]["en_name"] = game.get("name", "")
game_list[i]["name"] = baike(game.get("name", ""))
appid = game.get('appid')
if appid:
game_list[i]["link"] = f"https://store.steampowered.com/app/{appid}"
game_list[i]["img"] = f"https://cdn.cloudflare.steamstatic.com/steam/apps/{appid}/header.jpg"
else:
print("appid 不存在")
break

game_list[i]["totalTime"] = f"{round(game.get('playtime_forever', 0) / 60, 2)} 小时"
game_list[i]["lastTime"] = time.strftime("%Y-%m-%d", time.localtime(game.get('rtime_last_played', 0))) if game.get(
'rtime_last_played') else "从未运行"

ach_res = get_achievement_data(appid, key, steamid)
ach_data = ach_res.get("playerstats", {}).get("achievements", [])
num = sum(1 for ach in ach_data if ach.get("achieved") == 1)
game_list[i]["num"] = f"{num}/{len(ach_data)}"
game_list[i]["achievement"] = f"{round(num / len(ach_data) * 100, 2) if ach_data else 0}%"

print(f"完成进度: {i + 1} / {len(steam_data)}, 游戏名:{game.get('name')}")

save_yaml_file(file, file_data)


if __name__ == '__main__':
os.system("pip3 freeze > requirements.txt")
main("source/_data/games.yml")

3.2.2 使用 js 脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
const axios = require('axios');
const fs = require('fs');
const yaml = require('js-yaml');
const cheerio = require('cheerio');

const key = "********";
const steamid = "********";

async function getSteamData(key, steamid) {
const url = `http://api.steampowered.com/IPlayerService/GetOwnedGames/v1?include_appinfo=true&key=${key}&steamid=${steamid}&skip_unvettend_apps=false&include_played_free_games%3D1=true&format=json`;
const response = await axios.get(url);
return response.data.response.games;
}

async function getAchievementData(appid, key, steamid) {
const achievementUrl = `https://api.steampowered.com/ISteamUserStats/GetPlayerAchievements/v0001/?appid=${appid}&key=${key}&steamid=${steamid}&l=zh_CN`;
const res = await axios.get(achievementUrl);
return res.data;
}

function loadYamlFile(filePath) {
return yaml.load(fs.readFileSync(filePath, 'utf8'));
}

function saveYamlFile(filePath, data) {
fs.writeFileSync(filePath, yaml.safeDump(data, { 'noRefs': true, 'indent': ' ' }), 'utf8');
}

async function baike(enName) {
const url = `https://baike.baidu.com/search/word?fromModule=lemma_search-box&word=${enName}`;
const res = await axios.get(url);
const $ = cheerio.load(res.data);
const title = $("title").text();
const match = title.split("_")[0];
return match ? match : enName;
}

async function main(file) {
const steamData = await getSteamData(key, steamid);
const fileData = loadYamlFile(file);
let gameList = fileData[0].steam[0].games_list;

for (let i = 0; i < steamData.length; i++) {
const game = steamData[i];
if (i >= gameList.length) {
gameList.push({});
}

gameList[i].en_name = game.name || "";
gameList[i].name = await baike(game.name || "");
const appid = game.appid;
if (appid) {
gameList[i].link = `https://store.steampowered.com/app/${appid}`;
gameList[i].img = `https://cdn.cloudflare.steamstatic.com/steam/apps/${appid}/header.jpg`;
} else {
console.log("appid 不存在");
break;
}

gameList[i].totalTime = `${Math.round(game.playtime_forever / 60 * 100) / 100} 小时`;
gameList[i].lastTime = game.rtime_last_played ? new Date(game.rtime_last_played * 1000).toISOString().split('T')[0] : "从未运行";

const achRes = await getAchievementData(appid, key, steamid);
const achData = achRes.playerstats ? achRes.playerstats.achievements : [];
const num = achData.filter(ach => ach.achieved === 1).length;
gameList[i].num = `${num}/${achData.length}`;
gameList[i].achievement = `${achData.length > 0 ? Math.round(num / achData.length * 100 * 100) / 100 : 0}%`;

console.log(`完成进度: ${i + 1} / ${steamData.length}, 游戏名:${game.name}`);
}

saveYamlFile(file, fileData);
}

main("source/_data/games.yml");

四、修改路由

修改 themes/anzhiyu/layout/page.pug 文件

1
2
3
4
5
6
7
8
      when 'equipment'
include includes/page/equipment.pug
when 'books'
include includes/page/books.pug
+ when 'games'
+ include includes/page/games.pug
default
include includes/page/default-page.pug

五、修改页面

5.1 修改 .pug 文件

themes/anzhiyu/layout/includes/page/ 创建 games.pug 文件并修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#games
each i in site.data.games
.author-content.author-content-item.gamesPage.single(style=`background: url(${i.top_background}) left 37% / cover no-repeat !important;`)
.card-content
.author-content-item-tips= i.class_name
span.author-content-item-title= i.description
.content-bottom
.tips= i.tip
.games-card
img.card-img(src= i.games_card)
.banner-button-group
a.banner-button(href= i.buttonLink)
i.anzhiyufont.anzhiyu-icon-arrow-circle-right(style='font-size: 1.3rem')
span.banner-button-text= i.buttonText
each item in i.games.concat(i.steam)
.games-item
h2.games-title= item.title
.team-item-description= item.description
.games-item
.games-item-content
each iten, index in item.games_list
.games-game
a.game-img(href=iten.link)
img(src= iten.img, alt= iten.name)
span.game-name= iten.name
span.en-name= iten.en_name
.play-time
span.total-time 总游戏时间
span= iten.totalTime
span.last-time 最后运行日期
span= iten.lastTime
.game-achievement
span.achievement-text 成就
span.achievement-num= iten.num
.progress(style=`width: ${iten.achievement}`)

5.2 修改 .styl 文件

themes/anzhiyu/source/css/_page/ 创建 games.styl 并修改

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

.games-item
.games-item-content
display: flex
flex-direction: row
flex-wrap: wrap
margin: 0 -8px
.games-item-content-item
width: calc(25% - 12px)
border-radius: 12px
border: var(--style-border-always)
overflow: hidden
margin: 8px 6px
background: var(--anzhiyu-card-bg)
box-shadow: var(--anzhiyu-shadow-border)
min-height: 400px
position: relative
+maxWidth1200()
width: calc(50% - 12px)
+maxWidth768()
width: 100%
min-height: auto // 适应手机屏幕,可以考虑减少最小高度
.games-item-content-item-info
padding: 8px 16px 16px 16px
margin-top: 12px
.games-item-content-item-name
font-size: 18px
font-weight: bold
line-height: 1
margin-bottom: 8px
white-space: nowrap
overflow: visible
text-overflow: ellipsis
width: fit-content
cursor pointer
&:hover
color: var(--anzhiyu-main)
.games-item-content-item-specification
font-size: 12px
color: var(--anzhiyu-secondtext)
line-height: 16px
margin-bottom: 5px
white-space: nowrap
overflow: hidden
text-overflow: ellipsis
.games-item-content-item-description
line-height: 20px
color: var(--anzhiyu-secondtext)
height: 60px
display: -webkit-box
overflow: hidden
-webkit-line-clamp: 3
-webkit-box-orient: vertical
font-size: 14px
+maxWidth768()
font-size: 12px // 适应手机屏幕,可以考虑减少字体大小
a.games-item-content-item-link
font-size: 12px
background: var(--anzhiyu-gray-op)
padding: 4px 8px
border-radius: 8px
cursor: pointer
&:hover
background: var(--anzhiyu-main)
color: var(--anzhiyu-white)
.games-item-content-item-cover
width: 100%
height: 200px
background: var(--anzhiyu-secondbg)
display: flex
justify-content: center
align-items: center
+maxWidth768()
height: 150px // 适应手机屏幕,可以考虑减少高度
img.games-item-content-item-image
object-fit: cover
height: 100%
width: 100%
// border-radius: 0
// 若需要去除图片圆角可以将这里的注释去掉
.games-item-content-item-toolbar
display: flex
justify-content: space-between
position: absolute
bottom: 12px
left: 0
width: 100%
padding: 0 16px

body[data-type="games"] #web_bg
background: var(--anzhiyu-background);
body[data-type="games"] #page
border: 0;
box-shadow: none !important;
padding: 0 !important;
background: 0 0 !important;
body[data-type="games"] #page .page-title
display: none;

#games
.card-content
.games-card
position: absolute
right: 15px
img.card-img
width: 500px
.goodsteam-item
.games-item
.playtime_forever
position: absolute
left: 15px
top: 15px
color: #fff
.rtime_last_played
position: absolute
right: 15px
top: 15px
color: #fff
.achievement
position: absolute

#header_canvas
z-index: 99

.games-game
box-sizing: border-box
height: 140px
width: 32%
padding: 10px
margin: 8px
background-color: #16202D
color: #fff
box-shadow: 3px 5px 11px #333333
border-radius: 10px
+maxWidth768()
width: 100% // 适应手机屏幕,可以考虑增加宽度
.en-name
font-weight: 300
font-size: 10px
position: absolute
margin-top: 35px
.game-img img
height: 100%
border-radius: 10px
width: 50%
.game-name
white-space: nowrap
font-size: 14px
display: inline-flex
flex-direction: column
justify-content: center
align-items: flex-start
height: 18px
font-weight: 900
position: absolute
margin: 1px 10px 10px
width: 200px
.play-time
flex-direction: row
white-space: nowrap
color: #B8BCBF
position: absolute
margin: 50px 10px 1px
display: inline-flex
.play-time .total-time, .last-time
display: inline-flex
flex-direction: column
font-size: 0.75rem
color: rgba(209, 211, 212, 0.8)
line-height: 1rem
width: 80px
.game-achievement
width: 200px
color: #B8BCBF
display: inline-block
margin-left: 8px
.achievement-text
font-weight: 700
font-size: 0.75rem
margin-right: 120px
color: inherit
letter-spacing: .03em
.achievement-num
font-size: 0.75rem
text-align: end
min-width: 2.5rem
.progress
height: 6px
border-radius: 2px
overflow: hidden
.progress::after
content: ""
display: block
height: 100%
background-color: #1A9FFF

六、 完结 撒花~~~

参考文献:

  1. Steam Card 获取 - steam 个人资料卡片, 需要 steam 授权登录.
  2. Steam id finder - 获取 steam 登录 id
  3. Steam Web API 密钥
  4. Steam Web API简易使用介绍
  5. Valvesoftware - steam 官方 api 文档