@@ -4,6 +4,7 @@ import { ref, onMounted } from 'vue'
44const mods = ref ([])
55const loading = ref (true )
66const errorMessage = ref (' ' )
7+ const activeSort = ref (' trend' )
78
89const APP_ID = 457140
910const 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 (/ \/ $ / , ' ' )
1415const 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
1639function 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 }
0 commit comments