mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-04-03 14:20:50 +08:00
@@ -1,22 +1,36 @@
|
||||
export default {
|
||||
title: {
|
||||
qr: 'QR Code Login',
|
||||
phone: 'Phone Login'
|
||||
phone: 'Phone Login',
|
||||
token: 'Cookie Login'
|
||||
},
|
||||
qrTip: 'Scan with NetEase Cloud Music APP',
|
||||
phoneTip: 'Login with NetEase Cloud account',
|
||||
tokenTip: 'Enter a valid NetEase Cloud Music Cookie to login',
|
||||
placeholder: {
|
||||
phone: 'Phone Number',
|
||||
password: 'Password'
|
||||
password: 'Password',
|
||||
token: 'Please enter NetEase Cloud Music Cookie (token)'
|
||||
},
|
||||
button: {
|
||||
login: 'Login',
|
||||
switchToQr: 'QR Code Login',
|
||||
switchToPhone: 'Phone Login'
|
||||
switchToPhone: 'Phone Login',
|
||||
switchToToken: 'Use Cookie Login',
|
||||
backToQr: 'Back to QR Code Login',
|
||||
tokenLogin: 'Cookie Login',
|
||||
autoGetCookie: 'Auto Get Cookie'
|
||||
},
|
||||
message: {
|
||||
loginSuccess: 'Login successful',
|
||||
tokenLoginSuccess: 'Cookie login successful',
|
||||
loadError: 'Error loading login information',
|
||||
qrCheckError: 'Error checking QR code status'
|
||||
}
|
||||
qrCheckError: 'Error checking QR code status',
|
||||
tokenRequired: 'Please enter Cookie',
|
||||
tokenInvalid: 'Invalid Cookie, please check and try again',
|
||||
autoGetCookieSuccess: 'Auto get Cookie successful',
|
||||
autoGetCookieFailed: 'Auto get Cookie failed',
|
||||
autoGetCookieTip: 'Will open NetEase Cloud Music login page, please complete login and close the window'
|
||||
},
|
||||
qrTitle: 'NetEase Cloud Music QR Code Login'
|
||||
};
|
||||
|
||||
@@ -19,6 +19,14 @@ export default {
|
||||
manualTheme: 'Manual Switch',
|
||||
language: 'Language Settings',
|
||||
languageDesc: 'Change display language',
|
||||
tokenManagement: 'Cookie Management',
|
||||
tokenManagementDesc: 'Manage NetEase Cloud Music login Cookie',
|
||||
tokenStatus: 'Current Cookie Status',
|
||||
tokenSet: 'Set',
|
||||
tokenNotSet: 'Not Set',
|
||||
setToken: 'Set Cookie',
|
||||
modifyToken: 'Modify Cookie',
|
||||
clearToken: 'Clear Cookie',
|
||||
font: 'Font Settings',
|
||||
fontDesc: 'Select fonts, prioritize fonts in order',
|
||||
fontScope: {
|
||||
@@ -185,6 +193,7 @@ export default {
|
||||
hideLyrics: 'Hide Lyrics',
|
||||
hidePlayBar: 'Hide Play Bar',
|
||||
hideMiniPlayBar: 'Hide Mini Play Bar',
|
||||
showMiniPlayBar: 'Show Mini Play Bar',
|
||||
backgroundTheme: 'Background Theme',
|
||||
themeOptions: {
|
||||
default: 'Default',
|
||||
@@ -283,5 +292,33 @@ export default {
|
||||
emptyListHint: 'Empty list means allow all IPs',
|
||||
saveSuccess: 'Remote control settings saved',
|
||||
accessInfo: 'Remote control access address:'
|
||||
},
|
||||
cookie: {
|
||||
title: 'Cookie Settings',
|
||||
description: 'Please enter NetEase Cloud Music Cookie:',
|
||||
placeholder: 'Please paste the complete Cookie...',
|
||||
help: {
|
||||
format: 'Cookie usually starts with "MUSIC_U="',
|
||||
source: 'Can be obtained from browser developer tools network requests',
|
||||
storage: 'Cookie will be automatically saved to local storage after setting'
|
||||
},
|
||||
action: {
|
||||
save: 'Save Cookie',
|
||||
paste: 'Paste',
|
||||
clear: 'Clear'
|
||||
},
|
||||
validation: {
|
||||
required: 'Please enter Cookie',
|
||||
format: 'Cookie format may be incorrect, please check if it contains MUSIC_U'
|
||||
},
|
||||
message: {
|
||||
saveSuccess: 'Cookie saved successfully',
|
||||
saveError: 'Failed to save Cookie',
|
||||
pasteSuccess: 'Pasted successfully',
|
||||
pasteError: 'Paste failed, please copy manually'
|
||||
},
|
||||
info: {
|
||||
length: 'Current length: {length} characters'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,22 +1,36 @@
|
||||
export default {
|
||||
title: {
|
||||
qr: 'QRコードログイン',
|
||||
phone: '電話番号ログイン'
|
||||
phone: '電話番号ログイン',
|
||||
token: 'Cookieログイン'
|
||||
},
|
||||
qrTip: 'NetEase Cloudアプリでログイン',
|
||||
phoneTip: 'NetEase Cloudアカウントでログイン',
|
||||
tokenTip: '有効なNetEase Cloud MusicのCookieを入力してログイン',
|
||||
placeholder: {
|
||||
phone: '電話番号',
|
||||
password: 'パスワード'
|
||||
password: 'パスワード',
|
||||
token: 'NetEase Cloud MusicのCookie(token)を入力してください'
|
||||
},
|
||||
button: {
|
||||
login: 'ログイン',
|
||||
switchToQr: 'QRコードログイン',
|
||||
switchToPhone: '電話番号ログイン'
|
||||
switchToPhone: '電話番号ログイン',
|
||||
switchToToken: 'Cookieログインを使用',
|
||||
backToQr: 'QRコードログインに戻る',
|
||||
tokenLogin: 'Cookieログイン',
|
||||
autoGetCookie: 'Cookie自動取得'
|
||||
},
|
||||
message: {
|
||||
loginSuccess: 'ログイン成功',
|
||||
tokenLoginSuccess: 'Cookieログイン成功',
|
||||
loadError: 'ログイン情報の読み込み中にエラーが発生しました',
|
||||
qrCheckError: 'QRコードの状態確認中にエラーが発生しました'
|
||||
}
|
||||
qrCheckError: 'QRコードの状態確認中にエラーが発生しました',
|
||||
tokenRequired: 'Cookieを入力してください',
|
||||
tokenInvalid: 'Cookieが無効です。確認して再試行してください',
|
||||
autoGetCookieSuccess: 'Cookie自動取得成功',
|
||||
autoGetCookieFailed: 'Cookie自動取得失敗',
|
||||
autoGetCookieTip: 'NetEase Cloud Musicのログインページを開きます。ログイン完了後、ウィンドウを閉じてください'
|
||||
},
|
||||
qrTitle: 'NetEase Cloud Music QRコードログイン'
|
||||
};
|
||||
@@ -19,6 +19,14 @@ export default {
|
||||
manualTheme: '手動切り替え',
|
||||
language: '言語設定',
|
||||
languageDesc: '表示言語を切り替え',
|
||||
tokenManagement: 'Cookie管理',
|
||||
tokenManagementDesc: 'NetEase Cloud MusicログインCookieを管理',
|
||||
tokenStatus: '現在のCookieステータス',
|
||||
tokenSet: '設定済み',
|
||||
tokenNotSet: '未設定',
|
||||
setToken: 'Cookieを設定',
|
||||
modifyToken: 'Cookieを変更',
|
||||
clearToken: 'Cookieをクリア',
|
||||
font: 'フォント設定',
|
||||
fontDesc: 'フォントを選択します。前に配置されたフォントが優先されます',
|
||||
fontScope: {
|
||||
@@ -183,6 +191,7 @@ export default {
|
||||
hideLyrics: '歌詞を非表示',
|
||||
hidePlayBar: '再生バーを非表示',
|
||||
hideMiniPlayBar: 'ミニ再生バーを非表示',
|
||||
showMiniPlayBar: 'ミニ再生バーを表示',
|
||||
backgroundTheme: '背景テーマ',
|
||||
themeOptions: {
|
||||
default: 'デフォルト',
|
||||
@@ -281,5 +290,33 @@ export default {
|
||||
emptyListHint: '空のリストはすべてのIPアクセスを許可することを意味します',
|
||||
saveSuccess: 'リモートコントロール設定を保存しました',
|
||||
accessInfo: 'リモートコントロールアクセスアドレス:'
|
||||
},
|
||||
cookie: {
|
||||
title: 'Cookie設定',
|
||||
description: 'NetEase Cloud MusicのCookieを入力してください:',
|
||||
placeholder: '完全なCookieを貼り付けてください...',
|
||||
help: {
|
||||
format: 'Cookieは通常「MUSIC_U=」で始まります',
|
||||
source: 'ブラウザの開発者ツールのネットワークリクエストから取得できます',
|
||||
storage: 'Cookie設定後、自動的にローカルストレージに保存されます'
|
||||
},
|
||||
action: {
|
||||
save: 'Cookieを保存',
|
||||
paste: '貼り付け',
|
||||
clear: 'クリア'
|
||||
},
|
||||
validation: {
|
||||
required: 'Cookieを入力してください',
|
||||
format: 'Cookie形式が正しくない可能性があります。MUSIC_Uが含まれているか確認してください'
|
||||
},
|
||||
message: {
|
||||
saveSuccess: 'Cookieの保存に成功しました',
|
||||
saveError: 'Cookieの保存に失敗しました',
|
||||
pasteSuccess: '貼り付けに成功しました',
|
||||
pasteError: '貼り付けに失敗しました。手動でコピーしてください'
|
||||
},
|
||||
info: {
|
||||
length: '現在の長さ:{length} 文字'
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,22 +1,36 @@
|
||||
export default {
|
||||
title: {
|
||||
qr: 'QR코드 로그인',
|
||||
phone: '휴대폰 번호 로그인'
|
||||
phone: '휴대폰 번호 로그인',
|
||||
token: 'Cookie 로그인'
|
||||
},
|
||||
qrTip: '넷이즈 클라우드 뮤직 앱으로 QR코드를 스캔하여 로그인',
|
||||
phoneTip: '넷이즈 클라우드 계정으로 로그인',
|
||||
tokenTip: '유효한 넷이즈 클라우드 뮤직 Cookie을 입력하여 로그인',
|
||||
placeholder: {
|
||||
phone: '휴대폰 번호',
|
||||
password: '비밀번호'
|
||||
password: '비밀번호',
|
||||
token: '넷이즈 클라우드 뮤직 Cookie(token)을 입력하세요'
|
||||
},
|
||||
button: {
|
||||
login: '로그인',
|
||||
switchToQr: 'QR코드 로그인',
|
||||
switchToPhone: '휴대폰 번호 로그인'
|
||||
switchToPhone: '휴대폰 번호 로그인',
|
||||
switchToToken: 'Cookie 로그인 사용',
|
||||
backToQr: 'QR코드 로그인으로 돌아가기',
|
||||
tokenLogin: 'Cookie 로그인',
|
||||
autoGetCookie: 'Cookie 자동 가져오기'
|
||||
},
|
||||
message: {
|
||||
loginSuccess: '로그인 성공',
|
||||
tokenLoginSuccess: 'Cookie 로그인 성공',
|
||||
loadError: '로그인 정보 로드 중 오류 발생',
|
||||
qrCheckError: 'QR코드 상태 확인 중 오류 발생'
|
||||
}
|
||||
qrCheckError: 'QR코드 상태 확인 중 오류 발생',
|
||||
tokenRequired: 'Cookie을 입력하세요',
|
||||
tokenInvalid: 'Cookie이 유효하지 않습니다. 확인 후 다시 시도하세요',
|
||||
autoGetCookieSuccess: 'Cookie 자동 가져오기 성공',
|
||||
autoGetCookieFailed: 'Cookie 자동 가져오기 실패',
|
||||
autoGetCookieTip: '넷이즈 클라우드 뮤직 로그인 페이지를 열겠습니다. 로그인 완료 후 창을 닫아주세요'
|
||||
},
|
||||
qrTitle: '넷이즈 클라우드 뮤직 QR코드 로그인'
|
||||
};
|
||||
@@ -19,6 +19,14 @@ export default {
|
||||
manualTheme: '수동 전환',
|
||||
language: '언어 설정',
|
||||
languageDesc: '표시 언어 전환',
|
||||
tokenManagement: 'Cookie 관리',
|
||||
tokenManagementDesc: '넷이즈 클라우드 뮤직 로그인 Cookie 관리',
|
||||
tokenStatus: '현재 Cookie 상태',
|
||||
tokenSet: '설정됨',
|
||||
tokenNotSet: '설정되지 않음',
|
||||
setToken: 'Cookie 설정',
|
||||
modifyToken: 'Cookie 수정',
|
||||
clearToken: 'Cookie 지우기',
|
||||
font: '폰트 설정',
|
||||
fontDesc: '폰트 선택, 앞에 있는 폰트를 우선 사용',
|
||||
fontScope: {
|
||||
@@ -183,6 +191,7 @@ export default {
|
||||
hideLyrics: '가사 숨기기',
|
||||
hidePlayBar: '재생바 숨기기',
|
||||
hideMiniPlayBar: '미니 재생바 숨기기',
|
||||
showMiniPlayBar: '미니 재생바 표시',
|
||||
backgroundTheme: '배경 테마',
|
||||
themeOptions: {
|
||||
default: '기본',
|
||||
@@ -281,5 +290,33 @@ export default {
|
||||
emptyListHint: '빈 목록은 모든 IP 액세스를 허용함을 의미합니다',
|
||||
saveSuccess: '원격 제어 설정이 저장되었습니다',
|
||||
accessInfo: '원격 제어 액세스 주소:'
|
||||
},
|
||||
cookie: {
|
||||
title: 'Cookie 설정',
|
||||
description: '넷이즈 클라우드 뮤직의 Cookie를 입력하세요:',
|
||||
placeholder: '완전한 Cookie를 붙여넣으세요...',
|
||||
help: {
|
||||
format: 'Cookie는 일반적으로 "MUSIC_U="로 시작합니다',
|
||||
source: '브라우저 개발자 도구의 네트워크 요청에서 얻을 수 있습니다',
|
||||
storage: 'Cookie 설정 후 자동으로 로컬 저장소에 저장됩니다'
|
||||
},
|
||||
action: {
|
||||
save: 'Cookie 저장',
|
||||
paste: '붙여넣기',
|
||||
clear: '지우기'
|
||||
},
|
||||
validation: {
|
||||
required: 'Cookie를 입력하세요',
|
||||
format: 'Cookie 형식이 올바르지 않을 수 있습니다. MUSIC_U가 포함되어 있는지 확인하세요'
|
||||
},
|
||||
message: {
|
||||
saveSuccess: 'Cookie 저장 성공',
|
||||
saveError: 'Cookie 저장 실패',
|
||||
pasteSuccess: '붙여넣기 성공',
|
||||
pasteError: '붙여넣기 실패, 수동으로 복사하세요'
|
||||
},
|
||||
info: {
|
||||
length: '현재 길이: {length} 문자'
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,22 +1,36 @@
|
||||
export default {
|
||||
title: {
|
||||
qr: '扫码登录',
|
||||
phone: '手机号登录'
|
||||
phone: '手机号登录',
|
||||
token: 'Cookie登录'
|
||||
},
|
||||
qrTip: '使用网易云APP扫码登录',
|
||||
phoneTip: '使用网易云账号登录',
|
||||
tokenTip: '输入有效的网易云音乐Cookie即可登录',
|
||||
placeholder: {
|
||||
phone: '手机号',
|
||||
password: '密码'
|
||||
password: '密码',
|
||||
token: '请输入网易云音乐Cookie(token)'
|
||||
},
|
||||
button: {
|
||||
login: '登录',
|
||||
switchToQr: '扫码登录',
|
||||
switchToPhone: '手机号登录'
|
||||
switchToPhone: '手机号登录',
|
||||
switchToToken: '使用Cookie登录',
|
||||
backToQr: '返回二维码登录',
|
||||
tokenLogin: 'Cookie登录',
|
||||
autoGetCookie: '自动获取Cookie'
|
||||
},
|
||||
message: {
|
||||
loginSuccess: '登录成功',
|
||||
tokenLoginSuccess: 'Cookie登录成功',
|
||||
loadError: '加载登录信息时出错',
|
||||
qrCheckError: '检查二维码状态时出错'
|
||||
}
|
||||
qrCheckError: '检查二维码状态时出错',
|
||||
tokenRequired: '请输入Cookie',
|
||||
tokenInvalid: 'Cookie无效,请检查后重试',
|
||||
autoGetCookieSuccess: '自动获取Cookie成功',
|
||||
autoGetCookieFailed: '自动获取Cookie失败',
|
||||
autoGetCookieTip: '将打开网易云音乐登录页面,请完成登录后关闭窗口'
|
||||
},
|
||||
qrTitle: '扫码登录网易云音乐'
|
||||
};
|
||||
|
||||
@@ -19,6 +19,14 @@ export default {
|
||||
manualTheme: '手动切换',
|
||||
language: '语言设置',
|
||||
languageDesc: '切换显示语言',
|
||||
tokenManagement: 'Cookie管理',
|
||||
tokenManagementDesc: '管理网易云音乐登录Cookie',
|
||||
tokenStatus: '当前Cookie状态',
|
||||
tokenSet: '已设置',
|
||||
tokenNotSet: '未设置',
|
||||
setToken: '设置Cookie',
|
||||
modifyToken: '修改Cookie',
|
||||
clearToken: '清除Cookie',
|
||||
font: '字体设置',
|
||||
fontDesc: '选择字体,优先使用排在前面的字体',
|
||||
fontScope: {
|
||||
@@ -183,6 +191,7 @@ export default {
|
||||
hideLyrics: '隐藏歌词',
|
||||
hidePlayBar: '隐藏播放栏',
|
||||
hideMiniPlayBar: '隐藏迷你播放栏',
|
||||
showMiniPlayBar: '显示迷你播放栏',
|
||||
backgroundTheme: '背景主题',
|
||||
themeOptions: {
|
||||
default: '默认',
|
||||
@@ -281,5 +290,33 @@ export default {
|
||||
emptyListHint: '空列表表示允许所有IP访问',
|
||||
saveSuccess: '远程控制设置已保存',
|
||||
accessInfo: '远程控制访问地址:'
|
||||
},
|
||||
cookie: {
|
||||
title: 'Cookie设置',
|
||||
description: '请输入网易云音乐的Cookie:',
|
||||
placeholder: '请粘贴完整的Cookie...',
|
||||
help: {
|
||||
format: 'Cookie通常以 "MUSIC_U=" 开头',
|
||||
source: '可以从浏览器开发者工具的网络请求中获取',
|
||||
storage: 'Cookie设置后将自动保存到本地存储'
|
||||
},
|
||||
action: {
|
||||
save: '保存Cookie',
|
||||
paste: '粘贴',
|
||||
clear: '清空'
|
||||
},
|
||||
validation: {
|
||||
required: '请输入Cookie',
|
||||
format: 'Cookie格式可能不正确,请检查是否包含MUSIC_U'
|
||||
},
|
||||
message: {
|
||||
saveSuccess: 'Cookie保存成功',
|
||||
saveError: 'Cookie保存失败',
|
||||
pasteSuccess: '粘贴成功',
|
||||
pasteError: '粘贴失败,请手动复制'
|
||||
},
|
||||
info: {
|
||||
length: '当前长度:{length} 字符'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,22 +1,36 @@
|
||||
export default {
|
||||
title: {
|
||||
qr: '掃碼登入',
|
||||
phone: '手機號登入'
|
||||
phone: '手機號登入',
|
||||
token: 'Cookie登入'
|
||||
},
|
||||
qrTip: '使用網易雲APP掃碼登入',
|
||||
phoneTip: '使用網易雲帳號登入',
|
||||
tokenTip: '輸入有效的網易雲音樂Cookie即可登入',
|
||||
placeholder: {
|
||||
phone: '手機號',
|
||||
password: '密碼'
|
||||
password: '密碼',
|
||||
token: '請輸入網易雲音樂Cookie(token)'
|
||||
},
|
||||
button: {
|
||||
login: '登入',
|
||||
switchToQr: '掃碼登入',
|
||||
switchToPhone: '手機號登入'
|
||||
switchToPhone: '手機號登入',
|
||||
switchToToken: '使用Cookie登入',
|
||||
backToQr: '返回二維碼登入',
|
||||
tokenLogin: 'Cookie登入',
|
||||
autoGetCookie: '自動取得Cookie'
|
||||
},
|
||||
message: {
|
||||
loginSuccess: '登入成功',
|
||||
tokenLoginSuccess: 'Cookie登入成功',
|
||||
loadError: '載入登入資訊時出錯',
|
||||
qrCheckError: '檢查二維碼狀態時出錯'
|
||||
}
|
||||
qrCheckError: '檢查二維碼狀態時出錯',
|
||||
tokenRequired: '請輸入Cookie',
|
||||
tokenInvalid: 'Cookie無效,請檢查後重試',
|
||||
autoGetCookieSuccess: '自動取得Cookie成功',
|
||||
autoGetCookieFailed: '自動取得Cookie失敗',
|
||||
autoGetCookieTip: '將開啟網易雲音樂登入頁面,請完成登入後關閉視窗'
|
||||
},
|
||||
qrTitle: '掃碼登入網易雲音樂'
|
||||
};
|
||||
|
||||
@@ -19,6 +19,14 @@ export default {
|
||||
manualTheme: '手動切換',
|
||||
language: '語言設定',
|
||||
languageDesc: '切換顯示語言',
|
||||
tokenManagement: 'Cookie管理',
|
||||
tokenManagementDesc: '管理網易雲音樂登入Cookie',
|
||||
tokenStatus: '目前Cookie狀態',
|
||||
tokenSet: '已設定',
|
||||
tokenNotSet: '未設定',
|
||||
setCookie: '設定Cookie',
|
||||
modifyToken: '修改Cookie',
|
||||
clearToken: '清除Cookie',
|
||||
font: '字體設定',
|
||||
fontDesc: '選擇字體,優先使用排在前面的字體',
|
||||
fontScope: {
|
||||
@@ -183,6 +191,7 @@ export default {
|
||||
hideLyrics: '隱藏歌詞',
|
||||
hidePlayBar: '隱藏播放列',
|
||||
hideMiniPlayBar: '隱藏迷你播放列',
|
||||
showMiniPlayBar: '顯示迷你播放列',
|
||||
backgroundTheme: '背景主題',
|
||||
themeOptions: {
|
||||
default: '預設',
|
||||
@@ -221,5 +230,33 @@ export default {
|
||||
closeColorPicker: '關閉色板'
|
||||
},
|
||||
placeholder: '#1db954'
|
||||
},
|
||||
cookie: {
|
||||
title: 'Cookie設定',
|
||||
description: '請輸入網易雲音樂的Cookie:',
|
||||
placeholder: '請貼上完整的Cookie...',
|
||||
help: {
|
||||
format: 'Cookie通常以 "MUSIC_U=" 開頭',
|
||||
source: '可以從瀏覽器開發者工具的網路請求中取得',
|
||||
storage: 'Cookie設定後將自動儲存到本機儲存'
|
||||
},
|
||||
action: {
|
||||
save: '儲存Cookie',
|
||||
paste: '貼上',
|
||||
clear: '清空'
|
||||
},
|
||||
validation: {
|
||||
required: '請輸入Cookie',
|
||||
format: 'Cookie格式可能不正確,請檢查是否包含MUSIC_U'
|
||||
},
|
||||
message: {
|
||||
saveSuccess: 'Cookie儲存成功',
|
||||
saveError: 'Cookie儲存失敗',
|
||||
pasteSuccess: '貼上成功',
|
||||
pasteError: '貼上失敗,請手動複製'
|
||||
},
|
||||
info: {
|
||||
length: '目前長度:{length} 字元'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ import { loadLyricWindow } from './lyric';
|
||||
import { initializeConfig } from './modules/config';
|
||||
import { initializeFileManager } from './modules/fileManager';
|
||||
import { initializeFonts } from './modules/fonts';
|
||||
import { initializeLoginWindow } from './modules/loginWindow';
|
||||
import { initializeRemoteControl } from './modules/remoteControl';
|
||||
import { initializeShortcuts, registerShortcuts } from './modules/shortcuts';
|
||||
import { initializeTray, updateCurrentSong, updatePlayState, updateTrayMenu } from './modules/tray';
|
||||
@@ -41,6 +42,8 @@ function initialize() {
|
||||
initializeWindowManager();
|
||||
// 初始化字体管理
|
||||
initializeFonts();
|
||||
// 初始化登录窗口
|
||||
initializeLoginWindow();
|
||||
|
||||
// 创建主窗口
|
||||
mainWindow = createMainWindow(icon);
|
||||
|
||||
104
src/main/modules/loginWindow.ts
Normal file
104
src/main/modules/loginWindow.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { BrowserWindow, ipcMain, session } from 'electron';
|
||||
import { join } from 'path';
|
||||
|
||||
import i18n from '../../i18n/main';
|
||||
|
||||
let loginWindow: BrowserWindow | null = null;
|
||||
|
||||
const loginUrl = 'https://music.163.com/#/login/';
|
||||
const loginTitle = i18n.global.t('login.qrTitle');
|
||||
|
||||
/**
|
||||
* 打开登录窗口获取Cookie
|
||||
*/
|
||||
const openLoginWindow = async (mainWin: BrowserWindow) => {
|
||||
let loginTimer: NodeJS.Timeout;
|
||||
|
||||
// 如果登录窗口已存在,则聚焦并返回
|
||||
if (loginWindow && !loginWindow.isDestroyed()) {
|
||||
loginWindow.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
const loginSession = session.fromPartition('persist:login');
|
||||
|
||||
// 清除 Cookie
|
||||
await loginSession.clearStorageData({
|
||||
storages: ['cookies', 'localstorage'],
|
||||
});
|
||||
|
||||
loginWindow = new BrowserWindow({
|
||||
parent: mainWin,
|
||||
title: loginTitle,
|
||||
width: 1280,
|
||||
height: 800,
|
||||
center: true,
|
||||
autoHideMenuBar: true,
|
||||
webPreferences: {
|
||||
session: loginSession,
|
||||
sandbox: false,
|
||||
webSecurity: false,
|
||||
preload: join(__dirname, '../../preload/index.js'),
|
||||
},
|
||||
});
|
||||
|
||||
// 打开网易云登录页面
|
||||
loginWindow.loadURL(loginUrl);
|
||||
|
||||
// 阻止新窗口创建
|
||||
loginWindow.webContents.setWindowOpenHandler(() => {
|
||||
return { action: 'deny' };
|
||||
});
|
||||
|
||||
// 检查是否登录
|
||||
const checkLogin = async () => {
|
||||
try {
|
||||
if (!loginWindow || loginWindow.isDestroyed()) {
|
||||
if (loginTimer) clearInterval(loginTimer);
|
||||
return;
|
||||
}
|
||||
|
||||
const MUSIC_U = await loginSession.cookies.get({
|
||||
name: 'MUSIC_U',
|
||||
});
|
||||
|
||||
if (MUSIC_U && MUSIC_U?.length > 0) {
|
||||
if (loginTimer) clearInterval(loginTimer);
|
||||
const value = `MUSIC_U=${MUSIC_U[0].value};`;
|
||||
|
||||
mainWin?.webContents.send('send-cookies', value);
|
||||
|
||||
// 关闭登录窗口
|
||||
loginWindow.destroy();
|
||||
loginWindow = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('检查登录状态失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 循环检查登录状态
|
||||
loginWindow.webContents.once('did-finish-load', () => {
|
||||
loginWindow?.show();
|
||||
loginTimer = setInterval(checkLogin, 500);
|
||||
|
||||
loginWindow?.on('closed', () => {
|
||||
if (loginTimer) clearInterval(loginTimer);
|
||||
loginWindow = null;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 初始化登录窗口相关的IPC监听
|
||||
*/
|
||||
export function initializeLoginWindow() {
|
||||
ipcMain.on('open-login', (event) => {
|
||||
const mainWin = BrowserWindow.fromWebContents(event.sender);
|
||||
if (mainWin) {
|
||||
openLoginWindow(mainWin);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export default openLoginWindow;
|
||||
202
src/renderer/components/login/CookieLogin.vue
Normal file
202
src/renderer/components/login/CookieLogin.vue
Normal file
@@ -0,0 +1,202 @@
|
||||
<script lang="ts" setup>
|
||||
import { useMessage } from 'naive-ui';
|
||||
import { onBeforeUnmount, onMounted } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { getUserDetail } from '@/api/login';
|
||||
import { useUserStore } from '@/store/modules/user';
|
||||
import { isElectron, setAnimationClass } from '@/utils';
|
||||
|
||||
defineOptions({
|
||||
name: 'CookieLogin'
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
const message = useMessage();
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
|
||||
const token = ref('');
|
||||
|
||||
// Token登录
|
||||
const loginByToken = async () => {
|
||||
if (!token.value.trim()) {
|
||||
message.error(t('login.message.tokenRequired'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 直接设置token到localStorage
|
||||
localStorage.setItem('token', token.value.trim());
|
||||
|
||||
// 获取用户信息验证token有效性
|
||||
const user = await getUserDetail();
|
||||
if (user.data && user.data.profile) {
|
||||
userStore.user = user.data.profile;
|
||||
localStorage.setItem('user', JSON.stringify(user.data.profile));
|
||||
message.success(t('login.message.tokenLoginSuccess'));
|
||||
router.push('/user');
|
||||
} else {
|
||||
// token无效,清除localStorage
|
||||
localStorage.removeItem('token');
|
||||
message.error(t('login.message.tokenInvalid'));
|
||||
}
|
||||
} catch (error) {
|
||||
// token无效,清除localStorage
|
||||
localStorage.removeItem('token');
|
||||
message.error(t('login.message.tokenInvalid'));
|
||||
console.error('Token登录失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 自动获取Cookie
|
||||
const autoGetCookie = () => {
|
||||
if (!isElectron) {
|
||||
message.error('此功能仅在桌面版中可用');
|
||||
return;
|
||||
}
|
||||
|
||||
message.info(t('login.message.autoGetCookieTip'));
|
||||
window.electron.ipcRenderer.send('open-login');
|
||||
};
|
||||
|
||||
// 监听Cookie接收
|
||||
const handleCookieReceived = async (_event: any, cookieValue: string) => {
|
||||
try {
|
||||
// 设置Cookie到localStorage
|
||||
localStorage.setItem('token', cookieValue);
|
||||
|
||||
// 验证Cookie有效性
|
||||
const user = await getUserDetail();
|
||||
if (user.data && user.data.profile) {
|
||||
userStore.user = user.data.profile;
|
||||
localStorage.setItem('user', JSON.stringify(user.data.profile));
|
||||
message.success(t('login.message.autoGetCookieSuccess'));
|
||||
router.push('/user');
|
||||
} else {
|
||||
localStorage.removeItem('token');
|
||||
message.error(t('login.message.autoGetCookieFailed'));
|
||||
}
|
||||
} catch (error) {
|
||||
localStorage.removeItem('token');
|
||||
message.error(t('login.message.autoGetCookieFailed'));
|
||||
console.error('自动获取Cookie失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 在组件挂载时添加监听器
|
||||
onMounted(() => {
|
||||
if (isElectron) {
|
||||
window.electron.ipcRenderer.on('send-cookies', handleCookieReceived);
|
||||
}
|
||||
});
|
||||
|
||||
// 在组件卸载时移除监听器
|
||||
onBeforeUnmount(() => {
|
||||
if (isElectron) {
|
||||
window.electron.ipcRenderer.removeAllListeners('send-cookies');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="cookie-login" :class="setAnimationClass('animate__fadeInUp')">
|
||||
<div class="login-title">{{ t('login.title.token') }}</div>
|
||||
<div class="phone-page">
|
||||
<textarea
|
||||
v-model="token"
|
||||
class="token-input"
|
||||
:placeholder="t('login.placeholder.token')"
|
||||
rows="4"
|
||||
/>
|
||||
</div>
|
||||
<div class="text">{{ t('login.tokenTip') }}</div>
|
||||
<n-button class="btn-login" @click="loginByToken()">{{
|
||||
t('login.button.tokenLogin')
|
||||
}}</n-button>
|
||||
<n-button v-if="isElectron" class="btn-auto-cookie" @click="autoGetCookie()" type="info">
|
||||
{{ t('login.button.autoGetCookie') }}
|
||||
</n-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.cookie-login {
|
||||
animation-duration: 0.5s;
|
||||
@apply flex flex-col items-center;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
@apply text-2xl font-bold mb-6 text-white;
|
||||
}
|
||||
|
||||
.text {
|
||||
@apply mt-4 text-white text-xs;
|
||||
}
|
||||
|
||||
.phone-page {
|
||||
@apply bg-light dark:bg-gray-800 bg-opacity-90 dark:bg-opacity-90;
|
||||
width: 250px;
|
||||
@apply rounded-2xl overflow-hidden;
|
||||
padding: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.token-input {
|
||||
@apply w-full outline-none resize-none;
|
||||
@apply text-gray-900 dark:text-white bg-transparent;
|
||||
@apply placeholder-gray-500 dark:placeholder-gray-400;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
min-height: 100px;
|
||||
padding: 16px;
|
||||
margin: 0;
|
||||
border: none;
|
||||
border-radius: inherit;
|
||||
box-sizing: border-box;
|
||||
|
||||
&:focus {
|
||||
@apply outline-none;
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
@apply text-gray-400 dark:text-gray-500;
|
||||
}
|
||||
|
||||
/* 移除浏览器默认样式 */
|
||||
&::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: rgba(156, 163, 175, 0.3);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(156, 163, 175, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.btn-login {
|
||||
width: 250px;
|
||||
height: 40px;
|
||||
@apply mt-10 text-white rounded-xl;
|
||||
@apply bg-green-600 hover:bg-green-700 transition-colors;
|
||||
}
|
||||
|
||||
.btn-auto-cookie {
|
||||
width: 250px;
|
||||
height: 40px;
|
||||
@apply mt-4 text-white rounded-xl;
|
||||
@apply bg-blue-600 hover:bg-blue-700 transition-colors;
|
||||
}
|
||||
</style>
|
||||
270
src/renderer/components/login/QrLogin.vue
Normal file
270
src/renderer/components/login/QrLogin.vue
Normal file
@@ -0,0 +1,270 @@
|
||||
<script lang="ts" setup>
|
||||
import { useMessage } from 'naive-ui';
|
||||
import { onBeforeUnmount, onMounted } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { checkQr, createQr, getQrKey, getUserDetail } from '@/api/login';
|
||||
import { useUserStore } from '@/store/modules/user';
|
||||
import { setAnimationClass } from '@/utils';
|
||||
|
||||
defineOptions({
|
||||
name: 'QrLogin'
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
const message = useMessage();
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
|
||||
const qrUrl = ref<string>();
|
||||
const timerRef = ref(null);
|
||||
const qrStatus = ref<'loading' | 'active' | 'expired' | 'scanned' | 'confirmed'>('loading');
|
||||
const isRefreshing = ref(false);
|
||||
|
||||
const loadLogin = async () => {
|
||||
try {
|
||||
isRefreshing.value = true;
|
||||
qrStatus.value = 'loading';
|
||||
|
||||
// 清理之前的定时器
|
||||
if (timerRef.value) {
|
||||
clearInterval(timerRef.value);
|
||||
timerRef.value = null;
|
||||
}
|
||||
|
||||
const qrKey = await getQrKey();
|
||||
const key = qrKey.data.data.unikey;
|
||||
const { data } = await createQr(key);
|
||||
qrUrl.value = data.data.qrimg;
|
||||
qrStatus.value = 'active';
|
||||
|
||||
const timer = timerIsQr(key);
|
||||
timerRef.value = timer as any;
|
||||
} catch (error) {
|
||||
console.error(t('login.message.loadError'), error);
|
||||
qrStatus.value = 'expired';
|
||||
message.error(t('login.message.loadError'));
|
||||
} finally {
|
||||
isRefreshing.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const timerIsQr = (key: string) => {
|
||||
const timer = setInterval(async () => {
|
||||
try {
|
||||
const { data } = await checkQr(key);
|
||||
|
||||
// 二维码过期或不存在
|
||||
if (data.code === 800) {
|
||||
qrStatus.value = 'expired';
|
||||
clearInterval(timer);
|
||||
timerRef.value = null;
|
||||
message.warning('二维码已过期,请点击刷新获取新的二维码');
|
||||
return;
|
||||
}
|
||||
|
||||
// 等待扫码
|
||||
if (data.code === 801) {
|
||||
qrStatus.value = 'active';
|
||||
return;
|
||||
}
|
||||
|
||||
// 已扫码,等待确认
|
||||
if (data.code === 802) {
|
||||
qrStatus.value = 'scanned';
|
||||
message.info('已扫码,请在手机上确认登录');
|
||||
return;
|
||||
}
|
||||
|
||||
// 登录成功
|
||||
if (data.code === 803) {
|
||||
qrStatus.value = 'confirmed';
|
||||
localStorage.setItem('token', data.cookie);
|
||||
const user = await getUserDetail();
|
||||
userStore.user = user.data.profile;
|
||||
localStorage.setItem('user', JSON.stringify(user.data.profile));
|
||||
message.success(t('login.message.loginSuccess'));
|
||||
|
||||
clearInterval(timer);
|
||||
timerRef.value = null;
|
||||
router.push('/user');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(t('login.message.qrCheckError'), error);
|
||||
qrStatus.value = 'expired';
|
||||
clearInterval(timer);
|
||||
timerRef.value = null;
|
||||
message.error('检查二维码状态失败,请刷新重试');
|
||||
}
|
||||
}, 3000);
|
||||
|
||||
return timer;
|
||||
};
|
||||
|
||||
// 手动刷新二维码
|
||||
const refreshQr = () => {
|
||||
loadLogin();
|
||||
};
|
||||
|
||||
// 获取状态显示文本
|
||||
const getStatusText = () => {
|
||||
switch (qrStatus.value) {
|
||||
case 'loading':
|
||||
return '正在加载二维码...';
|
||||
case 'active':
|
||||
return t('login.qrTip');
|
||||
case 'expired':
|
||||
return '二维码已过期,请点击刷新';
|
||||
case 'scanned':
|
||||
return '已扫码,请在手机上确认登录';
|
||||
case 'confirmed':
|
||||
return '登录成功,正在跳转...';
|
||||
default:
|
||||
return t('login.qrTip');
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadLogin();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (timerRef.value) {
|
||||
clearInterval(timerRef.value);
|
||||
timerRef.value = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="qr-login" :class="setAnimationClass('animate__fadeInUp')">
|
||||
<div class="login-title">{{ t('login.title.qr') }}</div>
|
||||
|
||||
<!-- 二维码容器 -->
|
||||
<div class="qr-container">
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="qrStatus === 'loading'" class="qr-loading">
|
||||
<n-spin size="large" />
|
||||
<div class="loading-text">正在生成二维码...</div>
|
||||
</div>
|
||||
|
||||
<!-- 二维码图片 -->
|
||||
<div v-else class="qr-image-wrapper" :class="{ expired: qrStatus === 'expired' }">
|
||||
<img class="qr-img" :src="qrUrl" />
|
||||
|
||||
<!-- 过期遮罩 -->
|
||||
<div v-if="qrStatus === 'expired'" class="expired-overlay">
|
||||
<div class="expired-text">二维码已过期</div>
|
||||
<n-button class="refresh-btn" type="primary" @click="refreshQr" :loading="isRefreshing">
|
||||
{{ isRefreshing ? '刷新中...' : '点击刷新' }}
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<!-- 已扫码遮罩 -->
|
||||
<div v-if="qrStatus === 'scanned'" class="scanned-overlay">
|
||||
<div class="scanned-icon">✓</div>
|
||||
<div class="scanned-text">已扫码</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 状态文本 -->
|
||||
<div class="text" :class="{ expired: qrStatus === 'expired', scanned: qrStatus === 'scanned' }">
|
||||
{{ getStatusText() }}
|
||||
</div>
|
||||
|
||||
<!-- 手动刷新按钮 -->
|
||||
<div v-if="qrStatus === 'active'" class="refresh-area">
|
||||
<n-button text class="manual-refresh" @click="refreshQr" :loading="isRefreshing">
|
||||
刷新二维码
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.qr-login {
|
||||
animation-duration: 0.5s;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
@apply text-2xl font-bold mb-6 text-white;
|
||||
}
|
||||
|
||||
.qr-container {
|
||||
@apply relative;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
@apply mx-auto;
|
||||
}
|
||||
|
||||
.qr-loading {
|
||||
@apply flex flex-col items-center justify-center h-full;
|
||||
|
||||
.loading-text {
|
||||
@apply mt-4 text-white text-sm;
|
||||
}
|
||||
}
|
||||
|
||||
.qr-image-wrapper {
|
||||
@apply relative w-full h-full;
|
||||
|
||||
&.expired {
|
||||
.qr-img {
|
||||
@apply opacity-30;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.qr-img {
|
||||
@apply w-full h-full rounded-2xl transition-all duration-300;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.expired-overlay {
|
||||
@apply absolute inset-0 flex flex-col items-center justify-center;
|
||||
@apply bg-black bg-opacity-50 rounded-2xl;
|
||||
|
||||
.expired-text {
|
||||
@apply text-white text-sm mb-3;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
@apply text-sm;
|
||||
}
|
||||
}
|
||||
|
||||
.scanned-overlay {
|
||||
@apply absolute inset-0 flex flex-col items-center justify-center;
|
||||
@apply bg-green-500 bg-opacity-80 rounded-2xl;
|
||||
|
||||
.scanned-icon {
|
||||
@apply text-white text-4xl font-bold mb-2;
|
||||
}
|
||||
|
||||
.scanned-text {
|
||||
@apply text-white text-sm;
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
@apply mt-4 text-white text-xs transition-colors duration-300;
|
||||
|
||||
&.expired {
|
||||
@apply text-orange-400;
|
||||
}
|
||||
|
||||
&.scanned {
|
||||
@apply text-green-400;
|
||||
}
|
||||
}
|
||||
|
||||
.refresh-area {
|
||||
@apply mt-3;
|
||||
|
||||
.manual-refresh {
|
||||
@apply text-gray-300 hover:text-white text-xs;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -36,12 +36,8 @@
|
||||
<div class="tab-content">
|
||||
<div class="settings-grid">
|
||||
<div class="settings-item">
|
||||
<span>{{ t('settings.lyricSettings.hidePlayBar') }}</span>
|
||||
<n-switch v-model:value="config.hidePlayBar" />
|
||||
</div>
|
||||
<div class="settings-item">
|
||||
<span>{{ t('settings.lyricSettings.hideMiniPlayBar') }}</span>
|
||||
<n-switch v-model:value="config.hideMiniPlayBar" />
|
||||
<span>{{ t('settings.lyricSettings.showMiniPlayBar') }}</span>
|
||||
<n-switch v-model:value="showMiniPlayBar" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="theme-section">
|
||||
@@ -120,7 +116,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import { DEFAULT_LYRIC_CONFIG, LyricConfig } from '@/types/lyric';
|
||||
@@ -129,6 +125,22 @@ const { t } = useI18n();
|
||||
const config = ref<LyricConfig>({ ...DEFAULT_LYRIC_CONFIG });
|
||||
const emit = defineEmits(['themeChange']);
|
||||
|
||||
// 显示mini播放栏开关
|
||||
const showMiniPlayBar = computed({
|
||||
get: () => !config.value.hideMiniPlayBar,
|
||||
set: (value: boolean) => {
|
||||
if (value) {
|
||||
// 显示mini播放栏,隐藏普通播放栏
|
||||
config.value.hideMiniPlayBar = false;
|
||||
config.value.hidePlayBar = true;
|
||||
} else {
|
||||
// 显示普通播放栏,隐藏mini播放栏
|
||||
config.value.hideMiniPlayBar = true;
|
||||
config.value.hidePlayBar = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => config.value,
|
||||
(newConfig) => {
|
||||
|
||||
197
src/renderer/components/settings/CookieSettingsModal.vue
Normal file
197
src/renderer/components/settings/CookieSettingsModal.vue
Normal file
@@ -0,0 +1,197 @@
|
||||
<script lang="ts" setup>
|
||||
import { useMessage } from 'naive-ui';
|
||||
import { ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
defineOptions({
|
||||
name: 'CookieSettingsModal'
|
||||
});
|
||||
|
||||
interface Props {
|
||||
show: boolean;
|
||||
initialValue?: string;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:show', value: boolean): void;
|
||||
(e: 'save', value: string): void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
initialValue: ''
|
||||
});
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const { t } = useI18n();
|
||||
const message = useMessage();
|
||||
|
||||
const tokenInput = ref('');
|
||||
const isLoading = ref(false);
|
||||
|
||||
// 监听显示状态变化,重置输入框
|
||||
watch(
|
||||
() => props.show,
|
||||
(newShow) => {
|
||||
if (newShow) {
|
||||
tokenInput.value = props.initialValue;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 监听初始值变化
|
||||
watch(
|
||||
() => props.initialValue,
|
||||
(newValue) => {
|
||||
if (props.show) {
|
||||
tokenInput.value = newValue;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 关闭弹窗
|
||||
const handleClose = () => {
|
||||
emit('update:show', false);
|
||||
};
|
||||
|
||||
// 保存Cookie
|
||||
const handleSave = async () => {
|
||||
const trimmedToken = tokenInput.value.trim();
|
||||
|
||||
if (!trimmedToken) {
|
||||
message.error(t('settings.cookie.validation.required'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 简单验证Cookie格式
|
||||
if (!trimmedToken.includes('MUSIC_U=')) {
|
||||
message.warning(t('settings.cookie.validation.format'));
|
||||
}
|
||||
|
||||
try {
|
||||
isLoading.value = true;
|
||||
emit('save', trimmedToken);
|
||||
message.success(t('settings.cookie.message.saveSuccess'));
|
||||
handleClose();
|
||||
} catch (error) {
|
||||
console.error('保存Cookie失败:', error);
|
||||
message.error(t('settings.cookie.message.saveError'));
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 清空输入框
|
||||
const handleClear = () => {
|
||||
tokenInput.value = '';
|
||||
};
|
||||
|
||||
// 从剪贴板粘贴
|
||||
const handlePaste = async () => {
|
||||
try {
|
||||
const text = await navigator.clipboard.readText();
|
||||
if (text) {
|
||||
tokenInput.value = text;
|
||||
message.success(t('settings.cookie.message.pasteSuccess'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('粘贴失败:', error);
|
||||
message.error(t('settings.cookie.message.pasteError'));
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-modal
|
||||
:show="show"
|
||||
preset="dialog"
|
||||
:title="t('settings.cookie.title')"
|
||||
@update:show="emit('update:show', $event)"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="ri-key-line"></i>
|
||||
<span>{{ t('settings.cookie.title') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400 mb-2">
|
||||
{{ t('settings.cookie.description') }}
|
||||
</div>
|
||||
|
||||
<div class="relative">
|
||||
<n-input
|
||||
v-model:value="tokenInput"
|
||||
type="textarea"
|
||||
:placeholder="t('settings.cookie.placeholder')"
|
||||
:rows="6"
|
||||
:autosize="{ minRows: 4, maxRows: 8 }"
|
||||
style="font-family: monospace; font-size: 12px"
|
||||
class="cookie-input"
|
||||
/>
|
||||
|
||||
<!-- 工具按钮 -->
|
||||
<div class="absolute top-2 right-2 flex gap-1">
|
||||
<n-button
|
||||
size="tiny"
|
||||
quaternary
|
||||
@click="handlePaste"
|
||||
:title="t('settings.cookie.action.paste')"
|
||||
>
|
||||
<i class="ri-clipboard-line"></i>
|
||||
</n-button>
|
||||
<n-button
|
||||
size="tiny"
|
||||
quaternary
|
||||
@click="handleClear"
|
||||
:title="t('settings.cookie.action.clear')"
|
||||
>
|
||||
<i class="ri-delete-bin-line"></i>
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 帮助信息 -->
|
||||
<div class="text-xs text-gray-500 space-y-1">
|
||||
<p>• {{ t('settings.cookie.help.format') }}</p>
|
||||
<p>• {{ t('settings.cookie.help.source') }}</p>
|
||||
<p>• {{ t('settings.cookie.help.storage') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Cookie长度提示 -->
|
||||
<div v-if="tokenInput" class="text-xs text-gray-400">
|
||||
{{ t('settings.cookie.info.length', { length: tokenInput.length }) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #action>
|
||||
<div class="flex gap-2">
|
||||
<n-button @click="handleClose">
|
||||
{{ t('common.cancel') }}
|
||||
</n-button>
|
||||
<n-button
|
||||
type="primary"
|
||||
@click="handleSave"
|
||||
:disabled="!tokenInput.trim()"
|
||||
:loading="isLoading"
|
||||
>
|
||||
{{ t('settings.cookie.action.save') }}
|
||||
</n-button>
|
||||
</div>
|
||||
</template>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.cookie-input {
|
||||
:deep(.n-input__textarea) {
|
||||
font-family:
|
||||
'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', 'Fira Mono', 'Droid Sans Mono', 'Consolas',
|
||||
monospace;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,10 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
import { useMessage } from 'naive-ui';
|
||||
import { onMounted } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { checkQr, createQr, getQrKey, getUserDetail, loginByCellphone } from '@/api/login';
|
||||
import { loginByCellphone } from '@/api/login';
|
||||
import CookieLogin from '@/components/login/CookieLogin.vue';
|
||||
import QrLogin from '@/components/login/QrLogin.vue';
|
||||
import { useUserStore } from '@/store/modules/user';
|
||||
import { setAnimationClass } from '@/utils';
|
||||
|
||||
@@ -16,79 +17,10 @@ const { t } = useI18n();
|
||||
const message = useMessage();
|
||||
const router = useRouter();
|
||||
const isQr = ref(true);
|
||||
const isTokenLogin = ref(false);
|
||||
|
||||
const qrUrl = ref<string>();
|
||||
const userStore = useUserStore();
|
||||
|
||||
onMounted(() => {
|
||||
loadLogin();
|
||||
});
|
||||
|
||||
const timerRef = ref(null);
|
||||
|
||||
const loadLogin = async () => {
|
||||
try {
|
||||
if (timerRef.value) {
|
||||
clearInterval(timerRef.value);
|
||||
timerRef.value = null;
|
||||
}
|
||||
if (!isQr.value) return;
|
||||
const qrKey = await getQrKey();
|
||||
const key = qrKey.data.data.unikey;
|
||||
const { data } = await createQr(key);
|
||||
qrUrl.value = data.data.qrimg;
|
||||
|
||||
const timer = timerIsQr(key);
|
||||
timerRef.value = timer as any;
|
||||
} catch (error) {
|
||||
console.error(t('login.message.loadError'), error);
|
||||
}
|
||||
};
|
||||
|
||||
const timerIsQr = (key: string) => {
|
||||
const timer = setInterval(async () => {
|
||||
try {
|
||||
const { data } = await checkQr(key);
|
||||
|
||||
if (data.code === 800) {
|
||||
clearInterval(timer);
|
||||
timerRef.value = null;
|
||||
}
|
||||
if (data.code === 803) {
|
||||
localStorage.setItem('token', data.cookie);
|
||||
const user = await getUserDetail();
|
||||
userStore.user = user.data.profile;
|
||||
localStorage.setItem('user', JSON.stringify(user.data.profile));
|
||||
message.success(t('login.message.loginSuccess'));
|
||||
|
||||
clearInterval(timer);
|
||||
timerRef.value = null;
|
||||
router.push('/user');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(t('login.message.qrCheckError'), error);
|
||||
clearInterval(timer);
|
||||
timerRef.value = null;
|
||||
}
|
||||
}, 3000);
|
||||
|
||||
return timer;
|
||||
};
|
||||
|
||||
// 离开页面时
|
||||
onBeforeUnmount(() => {
|
||||
if (timerRef.value) {
|
||||
clearInterval(timerRef.value);
|
||||
timerRef.value = null;
|
||||
}
|
||||
});
|
||||
|
||||
// 是否扫码登陆
|
||||
// const chooseQr = () => {
|
||||
// isQr.value = !isQr.value;
|
||||
// loadLogin();
|
||||
// };
|
||||
|
||||
// 手机号登录
|
||||
const phone = ref('');
|
||||
const password = ref('');
|
||||
@@ -110,12 +42,17 @@ const loginPhone = async () => {
|
||||
<div class="phone-login">
|
||||
<div class="bg"></div>
|
||||
<div class="content">
|
||||
<div v-if="isQr" class="phone" :class="setAnimationClass('animate__fadeInUp')">
|
||||
<div class="login-title">{{ t('login.title.qr') }}</div>
|
||||
<img class="qr-img" :src="qrUrl" />
|
||||
<div class="text">{{ t('login.qrTip') }}</div>
|
||||
<!-- 二维码登录组件 -->
|
||||
<div v-if="isQr && !isTokenLogin" class="phone">
|
||||
<qr-login />
|
||||
</div>
|
||||
<div v-else class="phone" :class="setAnimationClass('animate__fadeInUp')">
|
||||
|
||||
<!-- 手机号登录 -->
|
||||
<div
|
||||
v-else-if="!isQr && !isTokenLogin"
|
||||
class="phone"
|
||||
:class="setAnimationClass('animate__fadeInUp')"
|
||||
>
|
||||
<div class="login-title">{{ t('login.title.phone') }}</div>
|
||||
<div class="phone-page">
|
||||
<input
|
||||
@@ -134,11 +71,18 @@ const loginPhone = async () => {
|
||||
<div class="text">{{ t('login.phoneTip') }}</div>
|
||||
<n-button class="btn-login" @click="loginPhone()">{{ t('login.button.login') }}</n-button>
|
||||
</div>
|
||||
|
||||
<!-- Cookie登录组件 -->
|
||||
<div v-else-if="isTokenLogin" class="phone">
|
||||
<cookie-login />
|
||||
</div>
|
||||
</div>
|
||||
<div class="bottom">
|
||||
<!-- <div class="title" @click="chooseQr()">
|
||||
{{ isQr ? t('login.button.switchToPhone') : t('login.button.switchToQr') }}
|
||||
</div> -->
|
||||
<div class="login-switch">
|
||||
<div class="title" @click="isTokenLogin = !isTokenLogin">
|
||||
{{ isTokenLogin ? t('login.button.backToQr') : t('login.button.switchToToken') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -184,9 +128,6 @@ const loginPhone = async () => {
|
||||
|
||||
.content {
|
||||
@apply absolute w-full h-full p-4 flex flex-col items-center justify-center pb-20 text-center;
|
||||
.qr-img {
|
||||
@apply rounded-2xl cursor-pointer transition-opacity;
|
||||
}
|
||||
|
||||
.phone {
|
||||
animation-duration: 0.5s;
|
||||
@@ -208,6 +149,7 @@ const loginPhone = async () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-login {
|
||||
width: 250px;
|
||||
height: 40px;
|
||||
|
||||
@@ -108,6 +108,34 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Token管理 -->
|
||||
<div class="set-item">
|
||||
<div>
|
||||
<div class="set-item-title">{{ t('settings.basic.tokenManagement') }}</div>
|
||||
<div class="set-item-content">
|
||||
<div class="text-sm text-gray-500 mb-2">
|
||||
{{ t('settings.basic.tokenStatus') }}:
|
||||
{{
|
||||
currentToken ? t('settings.basic.tokenSet') : t('settings.basic.tokenNotSet')
|
||||
}}
|
||||
</div>
|
||||
<div v-if="currentToken" class="text-xs text-gray-400 mb-2 font-mono break-all">
|
||||
{{ currentToken.substring(0, 50) }}...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<n-button size="small" @click="showTokenModal = true">
|
||||
{{
|
||||
currentToken ? t('settings.basic.modifyToken') : t('settings.basic.setToken')
|
||||
}}
|
||||
</n-button>
|
||||
<n-button v-if="currentToken" size="small" type="error" @click="clearToken">
|
||||
{{ t('settings.basic.clearToken') }}
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="set-item">
|
||||
<div>
|
||||
<div class="set-item-title">{{ t('settings.basic.animation') }}</div>
|
||||
@@ -524,6 +552,44 @@
|
||||
<remote-control-setting v-model:visible="showRemoteControlModal" />
|
||||
</template>
|
||||
|
||||
<!-- Token设置弹窗 -->
|
||||
<n-modal v-model:show="showTokenModal" preset="dialog" title="Cookie设置">
|
||||
<template #header>
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="ri-key-line"></i>
|
||||
<span>Cookie设置</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400 mb-2">
|
||||
请输入网易云音乐的Cookie:
|
||||
</div>
|
||||
<n-input
|
||||
v-model:value="tokenInput"
|
||||
type="textarea"
|
||||
placeholder="请粘贴完整的Cookie..."
|
||||
:rows="6"
|
||||
:autosize="{ minRows: 4, maxRows: 8 }"
|
||||
style="font-family: monospace; font-size: 12px"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-xs text-gray-500">
|
||||
<p>• Cookie通常以 "MUSIC_U=" 开头</p>
|
||||
<p>• 可以从浏览器开发者工具的网络请求中获取</p>
|
||||
<p>• Cookie设置后将自动保存到本地存储</p>
|
||||
</div>
|
||||
</div>
|
||||
<template #action>
|
||||
<div class="flex gap-2">
|
||||
<n-button @click="showTokenModal = false">取消</n-button>
|
||||
<n-button type="primary" @click="saveToken" :disabled="!tokenInput.trim()">
|
||||
保存Cookie
|
||||
</n-button>
|
||||
</div>
|
||||
</template>
|
||||
</n-modal>
|
||||
|
||||
<!-- 清除缓存弹窗 -->
|
||||
<clear-cache-settings v-model:show="showClearCacheModal" @confirm="clearCache" />
|
||||
</div>
|
||||
@@ -536,6 +602,7 @@ import { computed, h, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import localData from '@/../main/set.json';
|
||||
import { getUserDetail } from '@/api/login';
|
||||
import Coffee from '@/components/Coffee.vue';
|
||||
import DonationList from '@/components/common/DonationList.vue';
|
||||
import PlayBottom from '@/components/common/PlayBottom.vue';
|
||||
@@ -991,6 +1058,84 @@ const showMusicSourcesModal = ref(false);
|
||||
|
||||
// 远程控制设置弹窗
|
||||
const showRemoteControlModal = ref(false);
|
||||
|
||||
// Token管理相关
|
||||
const showTokenModal = ref(false);
|
||||
const tokenInput = ref('');
|
||||
const currentToken = ref(localStorage.getItem('token') || '');
|
||||
|
||||
// 保存Token
|
||||
const saveToken = async () => {
|
||||
if (!tokenInput.value.trim()) {
|
||||
message.error('请输入Token');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 临时保存原有token
|
||||
const originalToken = localStorage.getItem('token');
|
||||
|
||||
// 设置新token
|
||||
localStorage.setItem('token', tokenInput.value.trim());
|
||||
|
||||
// 验证token有效性
|
||||
const user = await getUserDetail();
|
||||
if (user.data && user.data.profile) {
|
||||
// token有效,更新用户信息
|
||||
userStore.setUser(user.data.profile);
|
||||
currentToken.value = tokenInput.value.trim();
|
||||
message.success('Token设置成功');
|
||||
showTokenModal.value = false;
|
||||
tokenInput.value = '';
|
||||
|
||||
// 刷新当前页面
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
} else {
|
||||
// token无效,恢复原有token
|
||||
if (originalToken) {
|
||||
localStorage.setItem('token', originalToken);
|
||||
} else {
|
||||
localStorage.removeItem('token');
|
||||
}
|
||||
message.error('Token无效,请检查后重试');
|
||||
}
|
||||
} catch (error) {
|
||||
// token无效,恢复原有token
|
||||
const originalToken = localStorage.getItem('token');
|
||||
if (originalToken) {
|
||||
localStorage.setItem('token', originalToken);
|
||||
} else {
|
||||
localStorage.removeItem('token');
|
||||
}
|
||||
message.error('Token无效,请检查后重试');
|
||||
console.error('Token验证失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 清除Token
|
||||
const clearToken = () => {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
currentToken.value = '';
|
||||
userStore.user = null;
|
||||
message.success('Token已清除');
|
||||
|
||||
// 刷新页面
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
// 监听localStorage中token的变化
|
||||
watch(
|
||||
() => localStorage.getItem('token'),
|
||||
(newToken) => {
|
||||
currentToken.value = newToken || '';
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
Reference in New Issue
Block a user