魔改教程 魔改 hexo Hexo 魔改 - 创建 "我的游戏" 页面 Pupper 2023-10-25 2024-10-30 一、效果预览
二、创建页面 在 /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 jsonimport reimport yamlimport timeimport requestskey = "************" 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
六、 完结 撒花~~~ 参考文献:
Steam Card 获取 - steam 个人资料卡片, 需要 steam 授权登录. Steam id finder - 获取 steam 登录 id Steam Web API 密钥 Steam Web API简易使用介绍 Valvesoftware - steam 官方 api 文档