Skip to content

Commit 3438083

Browse files
committed
Add workshop sorting tabs and cache results
1 parent 1ed5e5a commit 3438083

2 files changed

Lines changed: 143 additions & 12 deletions

File tree

.vitepress/theme/components/WorkshopList.vue

Lines changed: 137 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { ref, onMounted } from 'vue'
44
const mods = ref([])
55
const loading = ref(true)
66
const errorMessage = ref('')
7+
const activeSort = ref('trend')
78
89
const APP_ID = 457140
910
const PAGE_SIZE = 12
@@ -12,6 +13,28 @@ const WORKSHOP_API_BASE = (
1213
import.meta.env.VITE_WORKSHOP_API_BASE || DEFAULT_WORKSHOP_API_BASE
1314
).replace(/\/$/, '')
1415
const STEAM_WORKSHOP_HOME = `https://steamcommunity.com/app/${APP_ID}/workshop/`
16+
const SORT_OPTIONS = [
17+
{
18+
value: 'trend',
19+
label: '最热门',
20+
description: '过去 3 个月内评价最高'
21+
},
22+
{
23+
value: 'totaluniquesubscribers',
24+
label: '最多人订阅',
25+
description: '订阅人数最多'
26+
},
27+
{
28+
value: 'lastupdated',
29+
label: '最新更新',
30+
description: '最近更新的模组'
31+
},
32+
{
33+
value: 'mostrecent',
34+
label: '最新',
35+
description: '最新发布的模组'
36+
}
37+
]
1538
1639
function normalizeSteamImage(url) {
1740
if (!url) return 'https://placehold.co/400x240/1b1f29/d9dee7?text=Steam+Workshop'
@@ -81,15 +104,13 @@ function mapWorkshopFile(file) {
81104
}
82105
}
83106
84-
async function queryWorkshopFiles() {
107+
async function queryWorkshopFiles(sort = activeSort.value) {
85108
if (WORKSHOP_API_BASE) {
86109
const params = new URLSearchParams({
87110
appid: String(APP_ID),
88111
page: '1',
89112
limit: String(PAGE_SIZE),
90-
sort: 'trend',
91-
refresh: '1',
92-
t: String(Math.floor(Date.now() / (5 * 60 * 1000)))
113+
sort
93114
})
94115
95116
const res = await fetchWithTimeout(`${WORKSHOP_API_BASE}/workshop?${params}`)
@@ -103,7 +124,11 @@ async function queryWorkshopFiles() {
103124
appid: APP_ID,
104125
page: 1,
105126
numperpage: PAGE_SIZE,
106-
query_type: 3,
127+
query_type: sort === 'mostrecent' || sort === 'lastupdated'
128+
? 4
129+
: sort === 'totaluniquesubscribers'
130+
? 0
131+
: 3,
107132
return_metadata: true,
108133
return_vote_data: true,
109134
return_previews: true
@@ -122,9 +147,12 @@ async function queryWorkshopFiles() {
122147
return files.map(mapWorkshopFile).filter(mod => mod.link && mod.link !== '#')
123148
}
124149
125-
onMounted(async () => {
150+
async function loadWorkshopMods(sort = activeSort.value) {
151+
loading.value = true
152+
errorMessage.value = ''
153+
126154
try {
127-
mods.value = await queryWorkshopFiles()
155+
mods.value = await queryWorkshopFiles(sort)
128156
if (!mods.value.length) {
129157
errorMessage.value = 'Steam 暂时没有返回创意工坊条目。'
130158
}
@@ -134,11 +162,37 @@ onMounted(async () => {
134162
} finally {
135163
loading.value = false
136164
}
165+
}
166+
167+
function selectSort(sort) {
168+
if (sort === activeSort.value && mods.value.length) return
169+
activeSort.value = sort
170+
loadWorkshopMods(sort)
171+
}
172+
173+
onMounted(() => {
174+
loadWorkshopMods()
137175
})
138176
</script>
139177
140178
<template>
141179
<div class="workshop-container">
180+
<div class="workshop-sort" aria-label="创意工坊分类">
181+
<button
182+
v-for="option in SORT_OPTIONS"
183+
:key="option.value"
184+
type="button"
185+
class="workshop-sort__button"
186+
:class="{ 'workshop-sort__button--active': activeSort === option.value }"
187+
:aria-pressed="activeSort === option.value"
188+
:disabled="loading && activeSort === option.value"
189+
@click="selectSort(option.value)"
190+
>
191+
<span class="workshop-sort__label">{{ option.label }}</span>
192+
<span class="workshop-sort__description">{{ option.description }}</span>
193+
</button>
194+
</div>
195+
142196
<div v-if="loading" class="state-box">
143197
正在连接 Steam 创意工坊...
144198
</div>
@@ -187,6 +241,71 @@ onMounted(async () => {
187241
max-width: 920px;
188242
}
189243
244+
.workshop-sort {
245+
display: flex;
246+
flex-wrap: wrap;
247+
gap: 4px 22px;
248+
margin: 0 0 22px;
249+
border-bottom: 1px solid var(--vp-c-divider);
250+
}
251+
252+
.workshop-sort__button {
253+
position: relative;
254+
display: grid;
255+
gap: 3px;
256+
min-width: 0;
257+
padding: 0 0 12px;
258+
color: var(--vp-c-text-2);
259+
text-align: left;
260+
background: transparent;
261+
border: 0;
262+
cursor: pointer;
263+
transition: color 0.2s ease;
264+
}
265+
266+
.workshop-sort__button::after {
267+
position: absolute;
268+
right: 0;
269+
bottom: -1px;
270+
left: 0;
271+
height: 2px;
272+
background: var(--vp-c-brand-1);
273+
border-radius: 999px;
274+
content: '';
275+
opacity: 0;
276+
transform: scaleX(0.7);
277+
transition: opacity 0.2s ease, transform 0.2s ease;
278+
}
279+
280+
.workshop-sort__button:hover,
281+
.workshop-sort__button--active {
282+
color: var(--vp-c-text-1);
283+
}
284+
285+
.workshop-sort__button--active::after {
286+
opacity: 1;
287+
transform: scaleX(1);
288+
}
289+
290+
.workshop-sort__button:disabled {
291+
cursor: wait;
292+
}
293+
294+
.workshop-sort__label {
295+
font-size: 1rem;
296+
font-weight: 700;
297+
line-height: 1.3;
298+
white-space: nowrap;
299+
}
300+
301+
.workshop-sort__description {
302+
min-height: 1.3em;
303+
color: var(--vp-c-text-2);
304+
font-size: 0.82rem;
305+
line-height: 1.3;
306+
white-space: nowrap;
307+
}
308+
190309
.state-box {
191310
text-align: center;
192311
padding: 2rem;
@@ -219,6 +338,17 @@ onMounted(async () => {
219338
}
220339
221340
@media (max-width: 600px) {
341+
.workshop-sort {
342+
display: grid;
343+
grid-template-columns: repeat(2, minmax(0, 1fr));
344+
gap: 12px 16px;
345+
}
346+
347+
.workshop-sort__label,
348+
.workshop-sort__description {
349+
white-space: normal;
350+
}
351+
222352
.mod-grid {
223353
grid-template-columns: 1fr;
224354
}

workers/steam-workshop-worker.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ const DEFAULT_APP_ID = 457140
22
const DEFAULT_LIMIT = 6
33
const MAX_LIMIT = 20
44
const CACHE_VERSION = 'v2'
5+
const WORKSHOP_CACHE_TTL = 3600
56
const STEAM_COMMUNITY_ORIGIN = 'https://steamcommunity.com'
67
const STEAM_API_ORIGIN = 'https://api.steampowered.com'
78

@@ -92,7 +93,7 @@ async function handleWorkshop(url, env, ctx) {
9293
}
9394

9495
const response = json(body, 200, {
95-
'Cache-Control': items.length ? 'public, max-age=900' : 'no-store'
96+
'Cache-Control': items.length ? `public, max-age=${WORKSHOP_CACHE_TTL}` : 'no-store'
9697
})
9798

9899
if (items.length) {
@@ -366,7 +367,7 @@ async function postFormJson(url, body) {
366367
},
367368
body,
368369
cf: {
369-
cacheTtl: 900,
370+
cacheTtl: WORKSHOP_CACHE_TTL,
370371
cacheEverything: true
371372
}
372373
})
@@ -388,19 +389,19 @@ function steamRequestInit(accept) {
388389
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8'
389390
},
390391
cf: {
391-
cacheTtl: 900,
392+
cacheTtl: WORKSHOP_CACHE_TTL,
392393
cacheEverything: true
393394
}
394395
}
395396
}
396397

397398
function normalizeSort(sort) {
398-
const allowed = new Set(['trend', 'popular', 'mostrecent', 'totaluniquesubscribers', 'totaluniquevisitor'])
399+
const allowed = new Set(['trend', 'popular', 'mostrecent', 'lastupdated', 'totaluniquesubscribers', 'totaluniquevisitor'])
399400
return allowed.has(sort) ? sort : 'trend'
400401
}
401402

402403
function queryTypeForSort(sort) {
403-
if (sort === 'mostrecent') return 4
404+
if (sort === 'mostrecent' || sort === 'lastupdated') return 4
404405
if (sort === 'totaluniquesubscribers' || sort === 'popular') return 0
405406
return 3
406407
}

0 commit comments

Comments
 (0)