44 <div class =" app-bar-left" >
55 <div class =" app-bar-logo" >
66 <router-link to =" /" class =" logo-link" >
7- <span class =" logo-text" >蛇王争霸</span >
7+ <span class =" logo-text" @click.stop.prevent = " onLogoTap " >蛇王争霸</span >
88 </router-link >
99 </div >
1010
2828 </div >
2929
3030 <div class =" app-bar-user" >
31+ <button
32+ v-if =" showExport"
33+ class =" export-zip-button"
34+ :disabled =" exporting"
35+ @click =" downloadLatestSources"
36+ title =" 导出最新源码Zip"
37+ >
38+ {{ exporting ? '导出中…' : '导出源码' }}
39+ </button >
3140 <template v-if =" isAuthenticated " >
3241 <div class =" user-info" >
3342 <router-link to =" /profile" class =" user-profile-link" title =" 编辑个人资料" >
5564import { computed , ref , onMounted } from ' vue' ;
5665import { useRoute , useRouter } from ' vue-router' ;
5766import { useAuth } from ' ../stores/auth' ;
58- import { avatarService } from ' ../services/api' ;
67+ import { avatarService , sandboxService } from ' ../services/api' ;
68+ import { RateLimitError } from ' ../types/RateLimitError' ;
5969
6070const route = useRoute ();
6171const router = useRouter ();
@@ -105,6 +115,12 @@ const getAvatarUrl = (avatarId: number) => {
105115// 在组件挂载时获取默认头像ID
106116onMounted (() => {
107117 fetchDefaultAvatarId ();
118+ // 读取隐藏入口解锁状态
119+ try {
120+ if (localStorage .getItem (' exportUnlocked' ) === ' 1' ) {
121+ showExport .value = true ;
122+ }
123+ } catch {}
108124});
109125
110126// 生成头像颜色(用于没有头像时的占位符)
@@ -130,6 +146,53 @@ const handleLogout = async () => {
130146 await logout ();
131147 router .push (' /login' );
132148};
149+
150+ // 隐藏入口:连续点击 Logo 若干次显示导出按钮
151+ const tapCount = ref (0 );
152+ const showExport = ref (false );
153+ const exporting = ref (false );
154+ let tapResetTimer: number | undefined ;
155+
156+ const onLogoTap = () => {
157+ tapCount .value += 1 ;
158+ // 3 秒不再点击则重置计数,避免误触
159+ if (tapResetTimer ) window .clearTimeout (tapResetTimer );
160+ tapResetTimer = window .setTimeout (() => (tapCount .value = 0 ), 3000 );
161+
162+ if (tapCount .value >= 7 ) {
163+ showExport .value = true ;
164+ try { localStorage .setItem (' exportUnlocked' , ' 1' ); } catch {}
165+ }
166+ };
167+
168+ const downloadLatestSources = async () => {
169+ if (exporting .value ) return ;
170+ exporting .value = true ;
171+ try {
172+ const { blob, filename } = await sandboxService .downloadLatestSourcesZip ();
173+ const url = URL .createObjectURL (blob );
174+ const a = document .createElement (' a' );
175+ a .href = url ;
176+ a .download = filename || ' latest-sources.zip' ;
177+ document .body .appendChild (a );
178+ a .click ();
179+ a .remove ();
180+ URL .revokeObjectURL (url );
181+ } catch (err : any ) {
182+ if (err instanceof RateLimitError ) {
183+ const wait = err .retryAfterSeconds ? ` 请在 ${err .retryAfterSeconds }s 后重试。 ` : ' ' ;
184+ alert (' 导出频率过高。' + wait );
185+ } else if (err ?.response ?.status === 401 ) {
186+ alert (' 未授权或登录已过期,请重新登录。' );
187+ router .push (' /login' );
188+ } else {
189+ console .error (' 导出失败:' , err );
190+ alert (' 导出失败,请稍后重试。' );
191+ }
192+ } finally {
193+ exporting .value = false ;
194+ }
195+ };
133196 </script >
134197
135198<style scoped>
@@ -210,6 +273,30 @@ const handleLogout = async () => {
210273 align-items : center ;
211274}
212275
276+ .export-zip-button {
277+ margin-right : 8px ;
278+ background-color : var (--accent-color );
279+ color : #000 ;
280+ border : none ;
281+ padding : 6px 10px ;
282+ border-radius : 4px ;
283+ font-size : 12px ;
284+ font-family : ' Press Start 2P' , monospace ;
285+ cursor : pointer ;
286+ transition : all 0.2s ;
287+ }
288+
289+ .export-zip-button :hover:not (:disabled ) {
290+ background-color : var (--button-hover );
291+ transform : translateY (-1px );
292+ box-shadow : 0 2px 0 rgba (0 , 0 , 0 , 0.3 );
293+ }
294+
295+ .export-zip-button :disabled {
296+ opacity : 0.6 ;
297+ cursor : not-allowed ;
298+ }
299+
213300.user-info {
214301 display : flex ;
215302 align-items : center ;
0 commit comments