11<script setup lang="ts">
22import { useData } from " vitepress" ;
3- import { computed , ref } from " vue" ;
3+ import { computed , ref , watch , onMounted } from " vue" ;
44import {
55 downloadTranslations ,
66 type DownloadKey ,
@@ -16,49 +16,170 @@ const t = computed(
1616const version = ref (ghdata .version );
1717const copied = ref <string | null >(null );
1818const useMirror = ref (false );
19+ const useNightly = ref (false );
20+
21+ // Nightly 配置
22+ const NIGHTLY_OWNER = " LanRhyme" ;
23+ const NIGHTLY_REPO = " MicYou" ;
24+ const NIGHTLY_WORKFLOW = " development.yml" ;
25+
26+ // 存储 nightly run 和 artifacts 信息
27+ const nightlyRunId = ref <number | null >(null );
28+ const nightlyArtifacts = ref <Map <string , string >>(new Map ());
29+ const nightlyLoading = ref (false );
30+ const nightlyError = ref <string | null >(null );
31+
32+ // GitHub API 获取最新成功 run
33+ async function fetchLatestNightlyRun() {
34+ if (! useNightly .value ) return ;
35+ nightlyLoading .value = true ;
36+ nightlyError .value = null ;
37+
38+ try {
39+ // 获取最新成功的 workflow run
40+ const runsUrl = ` https://api.github.com/repos/${NIGHTLY_OWNER }/${NIGHTLY_REPO }/actions/workflows/${NIGHTLY_WORKFLOW }/runs?status=success&per_page=1 ` ;
41+ const runsRes = await fetch (runsUrl );
42+ if (! runsRes .ok ) throw new Error (" Failed to fetch runs" );
43+ const runsData = await runsRes .json ();
44+
45+ if (! runsData .workflow_runs ?.length ) {
46+ throw new Error (" No successful runs found" );
47+ }
48+
49+ const runId = runsData .workflow_runs [0 ].id ;
50+ nightlyRunId .value = runId ;
51+
52+ // 获取该 run 的 artifacts
53+ const artifactsUrl = ` https://api.github.com/repos/${NIGHTLY_OWNER }/${NIGHTLY_REPO }/actions/runs/${runId }/artifacts ` ;
54+ const artifactsRes = await fetch (artifactsUrl );
55+ if (! artifactsRes .ok ) throw new Error (" Failed to fetch artifacts" );
56+ const artifactsData = await artifactsRes .json ();
57+
58+ // 构建 artifact 名称映射 (pattern prefix -> actual name)
59+ const map = new Map <string , string >();
60+ for (const artifact of artifactsData .artifacts || []) {
61+ const name = artifact .name ;
62+ map .set (name , name );
63+ }
64+ nightlyArtifacts .value = map ;
65+ } catch (e ) {
66+ nightlyError .value = e instanceof Error ? e .message : " Unknown error" ;
67+ console .error (" Failed to fetch nightly info:" , e );
68+ } finally {
69+ nightlyLoading .value = false ;
70+ }
71+ }
72+
73+ // 监听 nightly 模式切换
74+ watch (useNightly , (newVal ) => {
75+ if (newVal ) fetchLatestNightlyRun ();
76+ });
77+
78+ // 初始化时如果 nightly 已开启则加载
79+ onMounted (() => {
80+ if (useNightly .value ) fetchLatestNightlyRun ();
81+ });
82+
83+ // 正则匹配 artifact 名称
84+ function findArtifact(pattern : string ): string | null {
85+ const regex = new RegExp (
86+ ` ^${pattern .replace (" {version}" , " \\ d+\\ .\\ d+\\ .\\ d+" )}$ ` ,
87+ );
88+ for (const [name] of nightlyArtifacts .value ) {
89+ if (regex .test (name )) return name ;
90+ }
91+ return null ;
92+ }
1993
2094const platforms: {
2195 name: string ;
2296 icon: string ;
2397 desc: DownloadKey ;
24- files: { name: DownloadKey ; pattern? : string ; copy? : string }[];
98+ files: {
99+ name: DownloadKey ;
100+ pattern? : string ;
101+ copy? : string ;
102+ nightlyPattern? : string ;
103+ }[];
25104}[] = [
26105 {
27106 name: " Windows" ,
28107 icon: " simple-icons:windows" ,
29108 desc: " windowsDesc" ,
30109 files: [
31- { name: " installer" , pattern: " MicYou-Win-{version}-installer.exe" },
32- { name: " portableJRE" , pattern: " MicYou-Win-{version}.zip" },
33- { name: " portableNoJRE" , pattern: " MicYou-Win-NoJRE-{version}.zip" },
110+ {
111+ name: " installer" ,
112+ pattern: " MicYou-Win-{version}-installer.exe" ,
113+ nightlyPattern: " MicYou-Win-{version}" ,
114+ },
115+ {
116+ name: " portableJRE" ,
117+ pattern: " MicYou-Win-{version}.zip" ,
118+ nightlyPattern: " MicYou-Win-{version}" ,
119+ },
120+ {
121+ name: " portableNoJRE" ,
122+ pattern: " MicYou-Win-NoJRE-{version}.zip" ,
123+ nightlyPattern: " MicYou-Win-NoJRE-{version}" ,
124+ },
34125 ],
35126 },
36127 {
37128 name: " macOS" ,
38129 icon: " simple-icons:macos" ,
39130 desc: " macOSDesc" ,
40131 files: [
41- { name: " dmgArm" , pattern: " MicYou-macOS-{version}-arm64.dmg" },
42- { name: " dmgIntel" , pattern: " MicYou-macOS-{version}-x64.dmg" },
43- { name: " portableNoJRE" , pattern: " MicYou-macOS-NoJRE-{version}.tar.gz" },
132+ {
133+ name: " dmgArm" ,
134+ pattern: " MicYou-macOS-{version}-arm64.dmg" ,
135+ nightlyPattern: " MicYou-macOS-arm64-{version}" ,
136+ },
137+ {
138+ name: " dmgIntel" ,
139+ pattern: " MicYou-macOS-{version}-x64.dmg" ,
140+ nightlyPattern: " MicYou-macOS-x64-{version}" ,
141+ },
142+ {
143+ name: " portableNoJRE" ,
144+ pattern: " MicYou-macOS-NoJRE-{version}.tar.gz" ,
145+ nightlyPattern: " MicYou-macOS-NoJRE-arm64-{version}" ,
146+ },
44147 ],
45148 },
46149 {
47150 name: " Linux" ,
48151 icon: " simple-icons:linux" ,
49152 desc: " linuxDesc" ,
50153 files: [
51- { name: " deb" , pattern: " MicYou-Linux-{version}.deb" },
52- { name: " rpm" , pattern: " MicYou-Linux-{version}.rpm" },
154+ {
155+ name: " deb" ,
156+ pattern: " MicYou-Linux-{version}.deb" ,
157+ nightlyPattern: " MicYou-Linux-{version}" ,
158+ },
159+ {
160+ name: " rpm" ,
161+ pattern: " MicYou-Linux-{version}.rpm" ,
162+ nightlyPattern: " MicYou-Linux-{version}" ,
163+ },
53164 { name: " arch" , copy: " paru -S micyou-bin" },
54- { name: " portableNoJRE" , pattern: " MicYou-Linux-NoJRE-{version}.tar.gz" },
165+ {
166+ name: " portableNoJRE" ,
167+ pattern: " MicYou-Linux-NoJRE-{version}.tar.gz" ,
168+ nightlyPattern: " MicYou-Linux-NoJRE-{version}" ,
169+ },
55170 ],
56171 },
57172 {
58173 name: " Android" ,
59174 icon: " simple-icons:android" ,
60175 desc: " androidDesc" ,
61- files: [{ name: " apk" , pattern: " MicYou-Android-{version}.apk" }],
176+ files: [
177+ {
178+ name: " apk" ,
179+ pattern: " MicYou-Android-{version}.apk" ,
180+ nightlyPattern: " MicYou-Android-{version}" ,
181+ },
182+ ],
62183 },
63184];
64185
@@ -68,8 +189,26 @@ const githubUrl = (pattern: string) =>
68189const mirrorUrl = (pattern : string ) =>
69190 ` https://atomgit.com/gh_mirrors/mi/MicYou/releases/download/v${version .value }/${pattern .replace (" {version}" , version .value )} ` ;
70191
71- const getUrl = (pattern : string ) =>
72- useMirror .value ? mirrorUrl (pattern ) : githubUrl (pattern );
192+ // nightly.link URL - 需要动态获取 artifact 名称
193+ const getNightlyUrl = (pattern : string ): string | null => {
194+ if (! nightlyRunId .value ) return null ;
195+ const artifactName = findArtifact (pattern );
196+ if (! artifactName ) return null ;
197+ return ` https://nightly.link/${NIGHTLY_OWNER }/${NIGHTLY_REPO }/actions/runs/${nightlyRunId .value }/${artifactName }.zip ` ;
198+ };
199+
200+ const getUrl = (pattern : string , nightlyPattern ? : string ) => {
201+ if (useNightly .value && nightlyPattern && nightlyRunId .value ) {
202+ const url = getNightlyUrl (nightlyPattern );
203+ return url || githubUrl (pattern );
204+ }
205+ return useMirror .value ? mirrorUrl (pattern ) : githubUrl (pattern );
206+ };
207+
208+ const isNightlyAvailable = (pattern ? : string ): boolean => {
209+ if (! pattern || ! nightlyRunId .value ) return false ;
210+ return findArtifact (pattern ) !== null ;
211+ };
73212
74213const copyCmd = async (cmd : string ) => {
75214 await navigator .clipboard .writeText (cmd );
@@ -98,7 +237,29 @@ const changelogLink = computed(() => {
98237 </header >
99238
100239 <div class =" card" >
101- <div class =" mirror-switch" >
240+ <!-- 版本类型切换 -->
241+ <div class =" version-switch" >
242+ <span class =" version-label" :class =" { active: !useNightly }" >{{ t.stable }}</span >
243+ <label class =" switch" >
244+ <input type =" checkbox" v-model =" useNightly" >
245+ <span class =" slider" ></span >
246+ </label >
247+ <span class =" version-label" :class =" { active: useNightly }" >{{ t.nightly }}</span >
248+ <span class =" switch-tip" v-if =" useNightly" >{{ t.nightlyTip }}</span >
249+ <span class =" loading-indicator" v-if =" useNightly && nightlyLoading" >
250+ <iconify-icon icon =" mdi:loading" class =" spin" />
251+ </span >
252+ </div >
253+ <!-- 错误提示 -->
254+ <div class =" error-msg" v-if =" useNightly && nightlyError" >
255+ <iconify-icon icon =" mdi:alert-circle" />
256+ {{ nightlyError }}
257+ <button class =" retry-btn" @click =" fetchLatestNightlyRun" >
258+ <iconify-icon icon =" mdi:refresh" />
259+ </button >
260+ </div >
261+ <!-- 下载源切换 (仅稳定版显示) -->
262+ <div class =" mirror-switch" v-if =" !useNightly" >
102263 <span class =" source-label" :class =" { active: !useMirror }" >{{ t.sourceGithub }}</span >
103264 <label class =" switch" >
104265 <input type =" checkbox" v-model =" useMirror" >
@@ -117,10 +278,16 @@ const changelogLink = computed(() => {
117278 </div >
118279 <div class =" opts" >
119280 <template v-for =" f in p .files " :key =" f .pattern || f .copy " >
120- <a v-if =" f.pattern" :href =" getUrl(f.pattern)" class =" btn" target =" _blank" >
281+ <a
282+ v-if =" f.pattern"
283+ :href =" getUrl(f.pattern, f.nightlyPattern)"
284+ class =" btn"
285+ :class =" { disabled: useNightly && !isNightlyAvailable(f.nightlyPattern) }"
286+ target =" _blank"
287+ >
121288 <iconify-icon icon =" mdi:download" />{{ t[f.name] }}
122289 </a >
123- <button v-else class =" btn" :class =" { done: copied === f.copy }" @click =" copyCmd(f.copy!)" >
290+ <button v-else class =" btn" :class =" { done: copied === f.copy }" @click =" copyCmd(f.copy!)" :disabled = " useNightly " >
124291 <iconify-icon :icon =" copied === f.copy ? 'mdi:check' : 'mdi:content-copy'" />
125292 {{ copied === f.copy ? t.copied : t[f.name] }}
126293 </button >
@@ -177,6 +344,27 @@ const changelogLink = computed(() => {
177344 border-bottom : 1px solid var (--vp-c-divider );
178345}
179346
347+ .version-switch {
348+ display : flex ;
349+ align-items : center ;
350+ justify-content : center ;
351+ gap : 12px ;
352+ padding : 12px 24px ;
353+ background : var (--vp-c-bg );
354+ border-bottom : 1px solid var (--vp-c-divider );
355+ }
356+
357+ .version-label {
358+ font-size : 0.875rem ;
359+ color : var (--vp-c-text-3 );
360+ transition : color 0.2s ;
361+ }
362+
363+ .version-label.active {
364+ color : var (--vp-c-brand-1 );
365+ font-weight : 500 ;
366+ }
367+
180368.source-label {
181369 font-size : 0.875rem ;
182370 color : var (--vp-c-text-3 );
@@ -316,6 +504,52 @@ input:checked + .slider:before {
316504 transform : translateY (-1px );
317505}
318506
507+ .btn :disabled ,
508+ .btn.disabled {
509+ opacity : 0.5 ;
510+ cursor : not-allowed ;
511+ transform : none ;
512+ pointer-events : none ;
513+ }
514+
515+ .loading-indicator {
516+ margin-left : 8px ;
517+ }
518+
519+ .spin {
520+ animation : spin 1s linear infinite ;
521+ }
522+
523+ @keyframes spin {
524+ from { transform : rotate (0deg ); }
525+ to { transform : rotate (360deg ); }
526+ }
527+
528+ .error-msg {
529+ display : flex ;
530+ align-items : center ;
531+ justify-content : center ;
532+ gap : 8px ;
533+ padding : 12px 24px ;
534+ background : var (--vp-c-danger-soft );
535+ color : var (--vp-c-danger-1 );
536+ font-size : 0.875rem ;
537+ }
538+
539+ .retry-btn {
540+ display : inline-flex ;
541+ align-items : center ;
542+ padding : 4px 8px ;
543+ border-radius : 4px ;
544+ background : var (--vp-c-bg );
545+ border : 1px solid var (--vp-c-divider );
546+ cursor : pointer ;
547+ }
548+
549+ .retry-btn :hover {
550+ background : var (--vp-c-brand-soft );
551+ }
552+
319553.notes {
320554 text-align : center ;
321555 margin-top : 32px ;
0 commit comments