From d67caeb69c115d85d6ca739394ecc2e558d6f685 Mon Sep 17 00:00:00 2001 From: XPoet Date: Mon, 25 Dec 2023 15:02:24 +0800 Subject: [PATCH 1/2] feat: you can log in with GitHub OAuth authorization (#60) --- .env.development | 15 ++ .env.production | 15 ++ README.md | 36 ++-- src/App.vue | 34 ++-- src/auto-imports.d.ts | 1 + src/common/api/repo.ts | 5 +- src/common/api/user.ts | 2 +- src/common/constant/settings.ts | 5 + src/common/constant/storage.ts | 2 + src/common/model/user-config.ts | 1 + src/common/model/vite-config.ts | 7 +- src/components.d.ts | 9 +- .../authorization-status-bar.styl | 31 ++++ .../authorization-status-bar.vue | 49 ++++++ .../header-content/header-content.vue | 8 +- src/components/nav-content/nav-content.vue | 5 +- src/locales/en.json | 23 ++- src/locales/zh-CN.json | 23 ++- src/locales/zh-TW.json | 23 ++- src/router/index.ts | 11 +- src/stores/index.ts | 4 +- src/stores/modules/github-authorize/index.ts | 59 +++++++ src/stores/modules/github-authorize/types.ts | 15 ++ src/stores/modules/user-config-info/index.ts | 3 +- src/stores/types.ts | 2 + src/styles/base.styl | 14 +- src/styles/element-plus.styl | 10 ++ src/utils/request/axios.ts | 2 +- src/utils/storage.ts | 27 ++- .../github-authorize/github-authorize.styl | 0 .../github-authorize/github-authorize.vue | 22 +++ src/views/my-config/my-config.util.ts | 94 ++++++++-- src/views/my-config/my-config.vue | 63 ++++--- src/views/picx-login/picx-login.model.ts | 10 ++ src/views/picx-login/picx-login.styl | 51 ++++++ src/views/picx-login/picx-login.util.ts | 142 +++++++++++++++ src/views/picx-login/picx-login.vue | 162 ++++++++++++++++++ 37 files changed, 870 insertions(+), 115 deletions(-) create mode 100644 src/components/authorization-status-bar/authorization-status-bar.styl create mode 100644 src/components/authorization-status-bar/authorization-status-bar.vue create mode 100644 src/stores/modules/github-authorize/index.ts create mode 100644 src/stores/modules/github-authorize/types.ts create mode 100644 src/views/github-authorize/github-authorize.styl create mode 100644 src/views/github-authorize/github-authorize.vue create mode 100644 src/views/picx-login/picx-login.model.ts create mode 100644 src/views/picx-login/picx-login.styl create mode 100644 src/views/picx-login/picx-login.util.ts create mode 100644 src/views/picx-login/picx-login.vue diff --git a/.env.development b/.env.development index 790e537b..842ae0af 100644 --- a/.env.development +++ b/.env.development @@ -1,2 +1,17 @@ # use pwa VITE_USE_PWA = false + +# PicX GitHub APP Client ID +VITE_CLIENT_ID = Iv1.274fe6f96551b91f + +# PicX GitHub APP Redirect URI +VITE_REDIRECT_URI = http://localhost:4000 + +# PicX GitHub APP Authorize URI +VITE_AUTHORIZE_URI = https://github.com/login/oauth/authorize + +# PicX GitHub APP Installations URL +VITE_INSTALL_URL = https://github.com/apps/picx-app/installations/select_target + +# PicX GitHub APP Installations Target User URL +VITE_INSTALL_URL_USER = https://github.com/apps/picx-app/installations/new/permissions?target_id= diff --git a/.env.production b/.env.production index 8a92e5c1..3088fbe8 100644 --- a/.env.production +++ b/.env.production @@ -1,2 +1,17 @@ # use pwa VITE_USE_PWA = true + +# PicX GitHub APP Client ID +VITE_CLIENT_ID = Iv1.274fe6f96551b91f + +# PicX GitHub APP Redirect URI +VITE_REDIRECT_URI = https://picx.xpoet.cn + +# PicX GitHub APP Authorize URI +VITE_AUTHORIZE_URI = https://github.com/login/oauth/authorize + +# PicX GitHub APP Installations URL +VITE_INSTALL_URL = https://github.com/apps/picx-app/installations/select_target + +# PicX GitHub APP Installations Target User URL +VITE_INSTALL_URL_USER = https://github.com/apps/picx-app/installations/new/permissions?target_id= diff --git a/README.md b/README.md index 605ce966..e7d7e578 100644 --- a/README.md +++ b/README.md @@ -18,43 +18,43 @@ ## 亮点 | Highlights -- 在线使用,无需下载,无需安装。 -- 操作简单,文档完善,持续维护。 -- 代码开源,数据安全,完全免费。 +- 在线使用、无需下载、无需安装。 +- 操作简单、文档完善、持续维护。 +- 代码开源、数据安全、完全免费。 ## 如何使用 | How to use -只需 [创建一个 GitHub Token](https://github.com/settings/tokens/new),在 [PicX 官网](https://picx.xpoet.cn) 使用 Token 完成图床配置即可。 +通过 [GitHub OAuth 授权](https://picx-docs.xpoet.cn/docs/usage-guide/config.html#github-oauth-%E6%8E%88%E6%9D%83%E7%99%BB%E5%BD%95) 或 [填写 GitHub Token](https://picx-docs.xpoet.cn/docs/usage-guide/config.html#%E5%A1%AB%E5%86%99-github-token-%E7%99%BB%E5%BD%95) 登录到 [PicX](https://picx.xpoet.cn),完成 [图床配置](https://picx-docs.xpoet.cn/docs/usage-guide/config.html#%E5%9B%BE%E5%BA%8A%E9%85%8D%E7%BD%AE) 后即可使用。 -**在线使用 https://picx.xpoet.cn** +**在线使用入口 https://picx.xpoet.cn** ## 文档 | Documents **官方文档 https://picx-docs.xpoet.cn** -通过阅读 **[快速开始](https://picx-docs.xpoet.cn/usage-guide/get-start.html)** 教程,可帮助你迅速上手 PicX。 +通过阅读 **[快速开始](https://picx-docs.xpoet.cn/docs/usage-guide/get-start.html)** 教程,可帮助你迅速上手 PicX。 ## 功能 | Features -- [x] 支持 **[拖拽](https://picx-docs.xpoet.cn/usage-guide/upload.html#%E6%8B%96%E6%8B%BD%E5%9B%BE%E7%89%87)**、**[复制粘贴](https://picx-docs.xpoet.cn/usage-guide/upload.html#%E5%A4%8D%E5%88%B6%E7%B2%98%E8%B4%B4)**、**[选择文件](https://picx-docs.xpoet.cn/usage-guide/upload.html#%E9%80%89%E6%8B%A9%E6%96%87%E4%BB%B6)** 等方式进行选择图片 -- [x] 支持图片 **[重命名](https://picx-docs.xpoet.cn/usage-guide/upload.html#%E9%87%8D%E5%91%BD%E5%90%8D)**、**[哈希化](https://picx-docs.xpoet.cn/usage-guide/upload.html#%E5%93%88%E5%B8%8C%E5%8C%96)**(确保图片名唯一)和 **[设置命名前缀](https://picx-docs.xpoet.cn/usage-guide/upload.html#%E5%89%8D%E7%BC%80%E5%91%BD%E5%90%8D)** -- [x] 支持 **批量上传图片**、**[批量删除图片](https://picx-docs.xpoet.cn/usage-guide/management.html#%E5%88%A0%E9%99%A4-%E6%89%B9%E9%87%8F%E5%88%A0%E9%99%A4)** 和 **[批量复制图片链接](https://picx-docs.xpoet.cn/usage-guide/management.html#%E5%A4%8D%E5%88%B6-%E6%89%B9%E9%87%8F%E5%A4%8D%E5%88%B6%E9%93%BE%E6%8E%A5)** +- [x] 支持 **[拖拽](https://picx-docs.xpoet.cn/docs/usage-guide/upload.html#%E6%8B%96%E6%8B%BD%E5%9B%BE%E7%89%87)**、**[复制粘贴](https://picx-docs.xpoet.cn/docs/usage-guide/upload.html#%E5%A4%8D%E5%88%B6%E7%B2%98%E8%B4%B4)**、**[选择文件](https://picx-docs.xpoet.cn/docs/usage-guide/upload.html#%E9%80%89%E6%8B%A9%E6%96%87%E4%BB%B6)** 等方式进行选择图片 +- [x] 支持上传时对图片 **[重命名](https://picx-docs.xpoet.cn/docs/usage-guide/upload.html#%E9%87%8D%E5%91%BD%E5%90%8D)**、**[哈希化](https://picx-docs.xpoet.cn/docs/usage-guide/upload.html#%E5%93%88%E5%B8%8C%E5%8C%96)**(确保图片名唯一)和 **[设置命名前缀](https://picx-docs.xpoet.cn/docs/usage-guide/upload.html#%E5%89%8D%E7%BC%80%E5%91%BD%E5%90%8D)** +- [x] 支持 **批量上传图片**、**[批量删除图片](https://picx-docs.xpoet.cn/usage-guide/management.html#%E5%88%A0%E9%99%A4-%E6%89%B9%E9%87%8F%E5%88%A0%E9%99%A4)** 和 **[批量复制图片链接](https://picx-docs.xpoet.cn/docs/usage-guide/management.html#%E6%89%B9%E9%87%8F%E5%A4%8D%E5%88%B6%E5%A4%9A%E5%BC%A0%E5%9B%BE%E7%89%87%E9%93%BE%E6%8E%A5)** - [x] 支持图床 **多级目录** 管理 (创建多级目录 / 查看多级目录下图片) -- [x] 支持 **[一键复制](https://picx-docs.xpoet.cn/usage-guide/upload.html#%E5%A4%8D%E5%88%B6%E5%9B%BE%E7%89%87%E9%93%BE%E6%8E%A5)** 图片链接和 **[自由转换 Markdown / HTML / BBCode 格式](https://picx-docs.xpoet.cn/usage-guide/settings.html#%E5%9B%BE%E7%89%87%E9%93%BE%E6%8E%A5%E6%A0%BC%E5%BC%8F%E8%AE%BE%E7%BD%AE)** -- [x] 内置 **[多种图片链接规则](https://picx-docs.xpoet.cn/usage-guide/settings.html#%E9%80%89%E6%8B%A9%E5%9B%BE%E7%89%87%E9%93%BE%E6%8E%A5%E8%A7%84%E5%88%99)**(Staticaly、jsDelivr、ChinaJsDelivr 等) -- [x] 支持 **[自定义配置图片链接规则](https://picx-docs.xpoet.cn/usage-guide/settings.html#%E9%85%8D%E7%BD%AE%E8%87%AA%E5%AE%9A%E4%B9%89%E5%9B%BE%E7%89%87%E9%93%BE%E6%8E%A5%E8%A7%84%E5%88%99)** -- [x] 支持 **[图片压缩](https://picx-docs.xpoet.cn/usage-guide/settings.html#%E5%9B%BE%E7%89%87%E5%8E%8B%E7%BC%A9%E8%AE%BE%E7%BD%AE)** (内置高效压缩算法,可配置在上传前自动压缩) -- [x] 支持配置 **[图片水印](https://picx-docs.xpoet.cn/usage-guide/settings.html#%E5%9B%BE%E7%89%87%E6%B0%B4%E5%8D%B0%E8%AE%BE%E7%BD%AE)** +- [x] 支持 **[一键复制](https://picx-docs.xpoet.cn/docs/usage-guide/upload.html#%E5%A4%8D%E5%88%B6%E5%9B%BE%E7%89%87%E9%93%BE%E6%8E%A5)** 图片链接和 **[自由转换 Markdown / HTML / BBCode 格式](https://picx-docs.xpoet.cn/docs/usage-guide/settings.html#%E5%9B%BE%E7%89%87%E9%93%BE%E6%8E%A5%E6%A0%BC%E5%BC%8F%E8%AE%BE%E7%BD%AE)** +- [x] 内置 **[多种图片链接规则](https://picx-docs.xpoet.cn/docs/usage-guide/settings.html#%E5%9B%BE%E7%89%87%E9%93%BE%E6%8E%A5%E8%A7%84%E5%88%99%E9%85%8D%E7%BD%AE)**(GitHub、GitHub Pages、jsDelivr、Statically 等) +- [x] 支持 **[自定义配置图片链接规则](https://picx-docs.xpoet.cn/docs/usage-guide/settings.html#%E9%85%8D%E7%BD%AE%E8%87%AA%E5%AE%9A%E4%B9%89%E5%9B%BE%E7%89%87%E9%93%BE%E6%8E%A5%E8%A7%84%E5%88%99)** +- [x] 支持 **[图片压缩](https://picx-docs.xpoet.cn/docs/usage-guide/settings.html#%E5%9B%BE%E7%89%87%E5%8E%8B%E7%BC%A9%E8%AE%BE%E7%BD%AE)** (内置高效压缩算法,可配置在上传前自动压缩) +- [x] 支持配置 **[图片水印](https://picx-docs.xpoet.cn/docs/usage-guide/settings.html#%E5%9B%BE%E7%89%87%E6%B0%B4%E5%8D%B0%E8%AE%BE%E7%BD%AE)** - [x] 支持 **PWA** -- [x] 支持 **[暗夜模式](https://picx-docs.xpoet.cn/usage-guide/settings.html#%E4%B8%BB%E9%A2%98%E8%AE%BE%E7%BD%AE)** (自动切换 / 自由切换) +- [x] 支持 **[暗夜模式](https://picx-docs.xpoet.cn/docs/usage-guide/settings.html#%E4%B8%BB%E9%A2%98%E8%AE%BE%E7%BD%AE)** (自动切换 / 自由切换) - [x] i18n(中文简体、中文繁体、英文) -- [x] 工具箱([图片压缩](https://picx-docs.xpoet.cn/usage-guide/toolbox.html#%E5%9B%BE%E7%89%87%E5%8E%8B%E7%BC%A9)、[图片转 Base64](https://picx-docs.xpoet.cn/usage-guide/toolbox.html#%E5%9B%BE%E7%89%87%E8%BD%AC-base64)、[图片水印](https://picx-docs.xpoet.cn/usage-guide/toolbox.html#%E5%9B%BE%E7%89%87%E6%B0%B4%E5%8D%B0)) +- [x] 工具箱([图片压缩](https://picx-docs.xpoet.cn/docs/usage-guide/toolbox.html#%E5%9B%BE%E7%89%87%E5%8E%8B%E7%BC%A9)、[图片转 Base64](https://picx-docs.xpoet.cn/docs/usage-guide/toolbox.html#%E5%9B%BE%E7%89%87%E8%BD%AC-base64)、[图片水印](https://picx-docs.xpoet.cn/docs/usage-guide/toolbox.html#%E5%9B%BE%E7%89%87%E6%B0%B4%E5%8D%B0)) ## 贡献 | Contribution 欢迎各种形式的贡献,包括但不限于:美化界面、增加功能、性能优化、修复 Bug、完善文档等。 -> [PicX 贡献指南](https://picx-docs.xpoet.cn/contribution-guide/contribution-guide.html) +参与贡献必读:[PicX 贡献指南](https://picx-docs.xpoet.cn/docs/contribution-guide/contribution-guide.html) ### 致谢 @@ -70,7 +70,7 @@ ## 赞赏 | Appreciation -PicX 的更新迭代依靠作者工作之外的时间,维护不易,如果对你有帮助,欢迎赞赏作者,支持开源。 +PicX 的更新迭代依靠作者工作之外的时间,维护不易,如果对你有帮助,可以赞赏作者,支持开源。 diff --git a/src/App.vue b/src/App.vue index a060f0c6..6634dda1 100644 --- a/src/App.vue +++ b/src/App.vue @@ -16,6 +16,7 @@ import { getLanguageByRegion, getRegionByIP, setWindowTitle, throttle } from '@/ import { ElementPlusSizeEnum, LanguageEnum } from '@/common/model' import MainContainer from '@/views/main-container/main-container.vue' import router from '@/router' +import { initGithubAuthorize } from '@/views/picx-login/picx-login.util' const instance = getCurrentInstance() const store = useStore() @@ -59,38 +60,34 @@ const setLanguage = (language: LanguageEnum) => { setWindowTitle(router.currentRoute.value.meta.title as string) } -const initSetLanguage = () => { - // 初始化设置 - setLanguage(userSettings.language) - - // 根据 IP 自动设置 +const setLanguageByIP = () => { getRegionByIP().then((region) => { const language = getLanguageByRegion(region) if (language !== userSettings.language) { const confirmTxt = instance?.proxy?.$t(`confirm`, language) - const cancelTxt = instance?.proxy?.$t(`cancel`, language) const msgTxt = instance?.proxy?.$t(`toggle_language_msg`, language, { region: instance?.proxy?.$t(`region.${region}`, language), language: instance?.proxy?.$t(`language.${language}`, language) }) const msgInstance = ElMessage({ - customClass: 'toggle-language-message', + customClass: 'custom-message-container', duration: 0, offset: 20, - message: `
+ type: 'info', + message: `
${msgTxt} ${confirmTxt} - ${cancelTxt}
`, - dangerouslyUseHTMLString: true + dangerouslyUseHTMLString: true, + showClose: true }) document - .querySelector('.toggle-language-message .content-box .confirm') + .querySelector('.custom-message-container .language .confirm') ?.addEventListener('click', () => { setLanguage(language) store.dispatch('SET_USER_SETTINGS', { @@ -98,16 +95,18 @@ const initSetLanguage = () => { }) msgInstance.close() }) - - document - .querySelector('.toggle-language-message .content-box .cancel') - ?.addEventListener('click', () => { - msgInstance.close() - }) } }) } +const initSetLanguage = () => { + // 初始化设置 + setLanguage(userSettings.language) + + // 根据 IP 自动设置 + setLanguageByIP() +} + const init = () => { elementPlusSizeHandle(window.innerWidth) window.addEventListener( @@ -119,6 +118,7 @@ const init = () => { setThemeMode() initSetLanguage() + initGithubAuthorize() } watch( diff --git a/src/auto-imports.d.ts b/src/auto-imports.d.ts index 001d94e0..cee8c010 100644 --- a/src/auto-imports.d.ts +++ b/src/auto-imports.d.ts @@ -18,5 +18,6 @@ declare global { const IEpPicture: typeof import('~icons/ep/picture')['default'] const IEpPostcard: typeof import('~icons/ep/postcard')['default'] const IEpSetting: typeof import('~icons/ep/setting')['default'] + const IEpSwitch: typeof import('~icons/ep/switch')['default'] const IEpUpload: typeof import('~icons/ep/upload')['default'] } diff --git a/src/common/api/repo.ts b/src/common/api/repo.ts index becafbc4..394a3141 100644 --- a/src/common/api/repo.ts +++ b/src/common/api/repo.ts @@ -116,7 +116,8 @@ export const createRepo = (token: string) => { description: INIT_REPO_DESC, private: false }, - headers: { Authorization: `token ${token}` }, - success422: true + headers: { Authorization: `Bearer ${token}` }, + success422: true, + noShowErrorMsg: true }) } diff --git a/src/common/api/user.ts b/src/common/api/user.ts index dfcbf380..ee278f3f 100644 --- a/src/common/api/user.ts +++ b/src/common/api/user.ts @@ -9,7 +9,7 @@ export const getGitHubUserInfo = (token: string) => { return request({ url: '/user', method: 'GET', - headers: { Authorization: `token ${token}` } + headers: { Authorization: `Bearer ${token}` } }) } diff --git a/src/common/constant/settings.ts b/src/common/constant/settings.ts index 464e425a..fcf39501 100644 --- a/src/common/constant/settings.ts +++ b/src/common/constant/settings.ts @@ -12,3 +12,8 @@ export const IMG_UPLOAD_MAX_SIZE: number = 30 // MB * 图片重命名最大长度 */ export const RENAME_MAX_LENGTH: number = 18 + +/** + * GitHub APP 授权 Token 过期时间(8 小时) + */ +export const GITHUB_AUTHORIZE_EXPIRE: number = 8 * 60 * 60 * 1000 diff --git a/src/common/constant/storage.ts b/src/common/constant/storage.ts index 118cfb37..b4f443af 100644 --- a/src/common/constant/storage.ts +++ b/src/common/constant/storage.ts @@ -5,3 +5,5 @@ export const LS_PICX_MANAGEMENT = `${PICX_PREFIX}MANAGEMENT` export const LS_PICX_SETTINGS = `${PICX_PREFIX}SETTINGS` export const SS_PICX_UPLOADED = `${PICX_PREFIX}UPLOADED` export const SS_TOOLBOX_IMG_LIST = `${PICX_PREFIX}TOOLBOX_IMG_LIST` +export const SS_PICX_AUTHORIZATION = `${PICX_PREFIX}AUTHORIZATION` +export const LS_PICX_AUTHORIZATION = `${PICX_PREFIX}AUTHORIZATION` diff --git a/src/common/model/user-config.ts b/src/common/model/user-config.ts index 9fed0061..3d6314a4 100644 --- a/src/common/model/user-config.ts +++ b/src/common/model/user-config.ts @@ -36,6 +36,7 @@ export enum DirModeEnum { export interface UserConfigInfoModel { token: string + id: string owner: string email: string name: string diff --git a/src/common/model/vite-config.ts b/src/common/model/vite-config.ts index c53a2203..f29cb6ff 100644 --- a/src/common/model/vite-config.ts +++ b/src/common/model/vite-config.ts @@ -1,5 +1,10 @@ export declare type Recordable = Record export declare interface ViteEnv { - VITE_USE_PWA?: boolean + VITE_USE_PWA?: boolean // 是否启用 PWA + VITE_CLIENT_ID?: string // PicX GitHub APP Client ID + VITE_REDIRECT_URI?: string // PicX GitHub APP Callback URL + VITE_AUTHORIZE_URI?: string // GitHub Authorize URI + VITE_INSTALL_URL?: string + VITE_INSTALL_URL_USER?: string } diff --git a/src/components.d.ts b/src/components.d.ts index 027e92c4..8fe16647 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -9,12 +9,12 @@ export {} declare module '@vue/runtime-core' { export interface GlobalComponents { + AuthorizationStatusBar: typeof import('./components/authorization-status-bar/authorization-status-bar.vue')['default'] Base64Tool: typeof import('./components/tools/base64-tool/base64-tool.vue')['default'] CloudSettingsBar: typeof import('./components/cloud-settings-bar/cloud-settings-bar.vue')['default'] CompressConfigBox: typeof import('./components/compress-config-box/compress-config-box.vue')['default'] CompressTool: typeof import('./components/tools/compress-tool/compress-tool.vue')['default'] CopyImageLink: typeof import('./components/copy-image-link/copy-image-link.vue')['default'] - Deploy: typeof import('./components/deploy/deploy.vue')['default'] ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb'] ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem'] ElButton: typeof import('element-plus/es')['ElButton'] @@ -43,7 +43,6 @@ declare module '@vue/runtime-core' { ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] ElRow: typeof import('element-plus/es')['ElRow'] ElSelect: typeof import('element-plus/es')['ElSelect'] - ElSpace: typeof import('element-plus/es')['ElSpace'] ElSwitch: typeof import('element-plus/es')['ElSwitch'] ElTable: typeof import('element-plus/es')['ElTable'] ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] @@ -53,17 +52,21 @@ declare module '@vue/runtime-core' { FolderCard: typeof import('./components/folder-card/folder-card.vue')['default'] GettingImages: typeof import('./components/getting-images/getting-images.vue')['default'] HeaderContent: typeof import('./components/header-content/header-content.vue')['default'] + IEpAim: typeof import('~icons/ep/aim')['default'] IEpCaretBottom: typeof import('~icons/ep/caret-bottom')['default'] IEpCaretLeft: typeof import('~icons/ep/caret-left')['default'] IEpCheck: typeof import('~icons/ep/check')['default'] + IEpCircleCheckFilled: typeof import('~icons/ep/circle-check-filled')['default'] IEpClose: typeof import('~icons/ep/close')['default'] IEpCopyDocument: typeof import('~icons/ep/copy-document')['default'] IEpDelete: typeof import('~icons/ep/delete')['default'] - IEpInfoFilled: typeof import('~icons/ep/info-filled')['default'] + IEpDocument: typeof import('~icons/ep/document')['default'] + IEpLink: typeof import('~icons/ep/link')['default'] IEpMoreFilled: typeof import('~icons/ep/more-filled')['default'] IEpOperation: typeof import('~icons/ep/operation')['default'] IEpRefresh: typeof import('~icons/ep/refresh')['default'] IEpRemove: typeof import('~icons/ep/remove')['default'] + IEpSwitch: typeof import('~icons/ep/switch')['default'] IEpUpload: typeof import('~icons/ep/upload')['default'] IEpUploadFilled: typeof import('~icons/ep/upload-filled')['default'] IEpUserFilled: typeof import('~icons/ep/user-filled')['default'] diff --git a/src/components/authorization-status-bar/authorization-status-bar.styl b/src/components/authorization-status-bar/authorization-status-bar.styl new file mode 100644 index 00000000..33306e4b --- /dev/null +++ b/src/components/authorization-status-bar/authorization-status-bar.styl @@ -0,0 +1,31 @@ +.authorization-status-box { + display flex + align-items center + justify-content space-between + padding 2rem 0 2rem 12rem + color var(--text-color-2) + font-size 14rem + border-color var(--text-color-4) + border-style solid + border-width 1rem + border-radius 6rem + + + &.success { + color var(--el-color-success) + background var(--el-color-success-light-9) + border-color var(--el-color-success) + } + + &.warning { + color var(--el-color-warning) + background var(--el-color-warning-light-9) + border-color var(--el-color-warning) + } + + &.error { + color var(--el-color-danger) + background var(--el-color-danger-light-9) + border-color var(--el-color-danger) + } +} diff --git a/src/components/authorization-status-bar/authorization-status-bar.vue b/src/components/authorization-status-bar/authorization-status-bar.vue new file mode 100644 index 00000000..5ecce975 --- /dev/null +++ b/src/components/authorization-status-bar/authorization-status-bar.vue @@ -0,0 +1,49 @@ + + + + + diff --git a/src/components/header-content/header-content.vue b/src/components/header-content/header-content.vue index 8f47862e..1c8cc43e 100644 --- a/src/components/header-content/header-content.vue +++ b/src/components/header-content/header-content.vue @@ -85,7 +85,7 @@ > {{ $t('header.logout') }} -
  • +
  • {{ $t('header.login') }}
  • @@ -111,10 +111,14 @@ const persistUserSettings = () => { store.dispatch('USER_SETTINGS_PERSIST') } +// 退出登录 const logout = () => { store.dispatch('LOGOUT') - router.push('/config') + router.push('/login') document.body.click() + setTimeout(() => { + window.location.reload() + }) } const jumpOwnerRepo = () => { diff --git a/src/components/nav-content/nav-content.vue b/src/components/nav-content/nav-content.vue index 9acf2258..960a81c8 100644 --- a/src/components/nav-content/nav-content.vue +++ b/src/components/nav-content/nav-content.vue @@ -66,6 +66,7 @@ import { useRouter } from 'vue-router' import { useStore } from '@/stores' import { ElementPlusSizeEnum } from '@/common/model' import { navInfoList } from './nav-content.data' +import i18n from '@/plugins/vue/i18n' const router = useRouter() const store = useStore() @@ -89,13 +90,13 @@ const onNavClick = (e: any) => { if (path === '/management') { if (userConfigInfo.selectedRepo === '') { - ElMessage.warning('请选择一个仓库') + ElMessage.warning(i18n.global.t('upload.message2')) router.push('/config') return } if (userConfigInfo.selectedDir === '') { - ElMessage.warning('目录不能为空') + ElMessage.warning(i18n.global.t('upload.message3')) router.push('/config') return } diff --git a/src/locales/en.json b/src/locales/en.json index 3471ebb7..5f2de2f5 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -23,6 +23,7 @@ "usage_count": "Usage count" }, "nav": { + "login": "Log in", "config": "Image Hosting Config", "upload": "Upload Image", "management": "Image Hosting Management", @@ -255,5 +256,25 @@ "zh-TW": "Traditional Chinese", "en": "English" }, - "toggle_language_msg": "We detected that your IP is located in {region}. Do you want to switch to {language}?" + "toggle_language_msg": "We detected that your IP is located in {region}. Do you want to switch to {language}?", + "authorization": { + "msg_1": "GitHub OAuth authorization login has expired, please re-authorize", + "msg_2": "PicX APP is not installed on GitHub and has no operation permission yet", + "msg_3": "PicX GitHub APP is installed successfully. Do you want to authorize the login?", + "msg_4": "Authorization failed, try again later", + "btn_1": "Install now", + "loading_1": "Authorizing login...", + "text_1": "GitHub OAuth authorization login", + "text_2": "Fill in GitHub Token to log in", + "text_3": "Login using GitHub OAuth authorization", + "text_4": "GitHub Token has expired, please authorize your login again", + "text_5": "Logging in using GitHub Token", + "text_6": "Return to login page", + "text_7": "Switch", + "text_8": "PicX GitHub APP must be installed to log in using GitHub OAuth authorization", + "text_9": "Filling in the GitHub Token to log in must generate a Token with operation permissions", + "text_10": "View tutorial", + "text_11": "Install PicX GitHub APP", + "text_12": "Create GitHub Token" + } } diff --git a/src/locales/zh-CN.json b/src/locales/zh-CN.json index 9c4bcdcf..efc1e381 100644 --- a/src/locales/zh-CN.json +++ b/src/locales/zh-CN.json @@ -23,6 +23,7 @@ "usage_count": "使用次数" }, "nav": { + "login": "登录", "config": "图床配置", "upload": "上传图片", "management": "图床管理", @@ -256,5 +257,25 @@ "zh-TW": "中文繁体", "en": "英文" }, - "toggle_language_msg": "检测到你的 IP 所属地为{region},是否切换{language}?" + "toggle_language_msg": "检测到你的 IP 所属地为{region},是否切换{language}?", + "authorization": { + "msg_1": "GitHub OAuth 授权登录已过期,请重新授权", + "msg_2": "未在 GitHub 安装 PicX APP,暂无操作权限", + "msg_3": "PicX GitHub APP 安装成功,是否进行授权登录?", + "msg_4": "授权失败,稍后重试", + "btn_1": "立即安装", + "loading_1": "正在授权登录...", + "text_1": "GitHub OAuth 授权登录", + "text_2": "填写 GitHub Token 登录", + "text_3": "正在使用 GitHub OAuth 授权登录", + "text_4": "GitHub Token 已过期,请重新授权登录", + "text_5": "正在使用填写 GitHub Token 登录", + "text_6": "返回登录页", + "text_7": "切换", + "text_8": "使用 GitHub OAuth 授权登录必须安装 PicX GitHub APP", + "text_9": "填写 GitHub Token 登录必须生成具有操作权限的 Token", + "text_10": "查看教程", + "text_11": "安装 PicX GitHub APP", + "text_12": "创建 GitHub Token" + } } diff --git a/src/locales/zh-TW.json b/src/locales/zh-TW.json index 25ea1c60..217b2098 100644 --- a/src/locales/zh-TW.json +++ b/src/locales/zh-TW.json @@ -23,6 +23,7 @@ "usage_count": "使用次數" }, "nav": { + "login": "登錄", "config": "圖床配置", "upload": "上傳圖片", "management": "圖床管理", @@ -256,5 +257,25 @@ "zh-TW": "中文繁體", "en": "英文" }, - "toggle_language_msg": "檢測到你的 IP 所屬地為{region},是否切換{language}?" + "toggle_language_msg": "檢測到你的 IP 所屬地為{region},是否切換{language}?", + "authorization": { + "msg_1": "GitHub OAuth 授權登入已過期,請重新授權", + "msg_2": "未在 GitHub 安裝 PicX APP,暫無操作權限", + "msg_3": "PicX GitHub APP 安裝成功,是否進行授權登入?", + "msg_4": "授權失敗,稍後重試", + "btn_1": "立即安裝", + "loading_1": "正在授權登入...", + "text_1": "GitHub OAuth 授權登入", + "text_2": "填入 GitHub Token 登入", + "text_3": "正在使用 GitHub OAuth 授權登入", + "text_4": "GitHub Token 已過期,請重新授權登入", + "text_5": "正在使用填入 GitHub Token 登入", + "text_6": "返回登入頁面", + "text_7": "切換", + "text_8": "使用 GitHub OAuth 授權登入必須安裝 PicX GitHub APP", + "text_9": "填寫 GitHub Token 登入必須產生具有操作權限的 Token", + "text_10": "檢視教學", + "text_11": "安裝 PicX GitHub APP", + "text_12": "建立 GitHub Token" + } } diff --git a/src/router/index.ts b/src/router/index.ts index fce7839e..01ba7b02 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -1,5 +1,5 @@ import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router' -import config from '@/views/my-config/my-config.vue' +import login from '@/views/picx-login/picx-login.vue' import upload from '@/views/upload-image/upload-image.vue' import management from '@/views/imgs-management/imgs-management.vue' import settings from '@/views/my-settings/my-settings.vue' @@ -13,15 +13,16 @@ import { setWindowTitle } from '@/utils' const routes: Array = [ { path: '/', - name: 'index', - redirect: { - name: 'upload' + name: 'login', + component: login, + meta: { + title: `nav.login` } }, { path: '/config', name: 'config', - component: config, + component: () => import('@/views/my-config/my-config.vue'), meta: { title: `nav.config` } diff --git a/src/stores/index.ts b/src/stores/index.ts index b380baae..8e294905 100644 --- a/src/stores/index.ts +++ b/src/stores/index.ts @@ -8,6 +8,7 @@ import uploadAreaActiveModule from './modules/upload-area-active' import userSettingsModule from './modules/user-settings' import toolboxImageListModule from './modules/toolbox-image-list' import uploadImageListModule from './modules/upload-image-list' +import githubAuthorizeModule from './modules/github-authorize' // Create a new store instance. export const store = createStore({ @@ -18,7 +19,8 @@ export const store = createStore({ uploadAreaActiveModule, userSettingsModule, toolboxImageListModule, - uploadImageListModule + uploadImageListModule, + githubAuthorizeModule }, state: { diff --git a/src/stores/modules/github-authorize/index.ts b/src/stores/modules/github-authorize/index.ts new file mode 100644 index 00000000..575826de --- /dev/null +++ b/src/stores/modules/github-authorize/index.ts @@ -0,0 +1,59 @@ +import { Module } from 'vuex' +import RootStateTypes from '@/stores/types' +import GitHubAuthorizeStateTypes, { GitHubAuthorizationInfo } from './types' +import { deepAssignObject, getLocal, setLocal } from '@/utils' +import { LS_PICX_AUTHORIZATION } from '@/common/constant' + +const initAuthorizationInfo = (): GitHubAuthorizationInfo => { + const initInfo: GitHubAuthorizationInfo = { + authorized: false, + installed: false, + token: '', + tokenCreateTime: 0, + code: '', + codeCreateTime: 0, + installationId: '', + manualToken: '', + isAutoAuthorize: false + } + + const LSInfo = getLocal(LS_PICX_AUTHORIZATION) + + if (LSInfo) { + deepAssignObject(initInfo, LSInfo) + return initInfo + } + + return initInfo +} + +const githubAuthorizeModule: Module = { + state: { + authorizationInfo: initAuthorizationInfo() + }, + + actions: { + // 设置 GitHub APP 授权状态信息 + SET_GITHUB_AUTHORIZATION_INFO({ state, dispatch }, authorizationInfo: GitHubAuthorizationInfo) { + // eslint-disable-next-line no-restricted-syntax + for (const key in authorizationInfo) { + if (Object.hasOwn(state.authorizationInfo, key)) { + // @ts-ignore + state.authorizationInfo[key] = authorizationInfo[key] + } + } + dispatch('GITHUB_AUTHORIZATION_INFO_PERSIST') + }, + + // 持久化存储 GitHub APP 授权状态信息 + GITHUB_AUTHORIZATION_INFO_PERSIST({ state }) { + setLocal(LS_PICX_AUTHORIZATION, state.authorizationInfo) + } + }, + + getters: { + getGitHubAuthorizationInfo: (state): GitHubAuthorizationInfo => state.authorizationInfo + } +} + +export default githubAuthorizeModule diff --git a/src/stores/modules/github-authorize/types.ts b/src/stores/modules/github-authorize/types.ts new file mode 100644 index 00000000..ffbc8904 --- /dev/null +++ b/src/stores/modules/github-authorize/types.ts @@ -0,0 +1,15 @@ +export interface GitHubAuthorizationInfo { + authorized: boolean + token: string + tokenCreateTime: number + code: string + codeCreateTime: number + installed: boolean + installationId: string + manualToken: string + isAutoAuthorize: boolean +} + +export default interface GitHubAuthorizeStateTypes { + authorizationInfo: GitHubAuthorizationInfo +} diff --git a/src/stores/modules/user-config-info/index.ts b/src/stores/modules/user-config-info/index.ts index f008e25b..a0198029 100644 --- a/src/stores/modules/user-config-info/index.ts +++ b/src/stores/modules/user-config-info/index.ts @@ -8,6 +8,7 @@ import { LS_PICX_CONFIG, NEW_DIR_COUNT_MAX } from '@/common/constant' const initUserConfigInfo = (): UserConfigInfoModel => { const initConfig: UserConfigInfoModel = { token: '', + id: '', owner: '', email: '', name: '', @@ -87,7 +88,7 @@ const userConfigInfoModule: Module = { }, // 设置用户配置信息 - SET_USER_CONFIG_INFO({ state, dispatch }, configInfo: UserConfigInfoStateTypes) { + SET_USER_CONFIG_INFO({ state, dispatch }, configInfo: UserConfigInfoModel) { // eslint-disable-next-line no-restricted-syntax for (const key in configInfo) { // eslint-disable-next-line no-prototype-builtins diff --git a/src/stores/types.ts b/src/stores/types.ts index 273cd43d..fb202b9a 100644 --- a/src/stores/types.ts +++ b/src/stores/types.ts @@ -3,6 +3,7 @@ import UserConfigInfoStateTypes from './modules/user-config-info/types' import UploadAreaActiveStateTypes from './modules/upload-area-active/types' import ToolboxImageListStateTypes from './modules/toolbox-image-list/types' import UploadImageListStateTypes from './modules/upload-image-list/types' +import GitHubAuthorizeStateTypes from './modules/github-authorize/types' export default interface RootStateTypes { rootName: string @@ -14,4 +15,5 @@ export interface AllStateTypes extends RootStateTypes { uploadAreaActiveModule: UploadAreaActiveStateTypes toolboxImageListModule: ToolboxImageListStateTypes uploadImageListModule: UploadImageListStateTypes + githubAuthorizeModule: GitHubAuthorizeStateTypes } diff --git a/src/styles/base.styl b/src/styles/base.styl index 011913e1..e4f30f16 100644 --- a/src/styles/base.styl +++ b/src/styles/base.styl @@ -197,15 +197,8 @@ li { } -.toggle-language-message { +.custom-message-container { box-sizing border-box - background var(--background-color) - border-radius 3rem - box-shadow var(--el-box-shadow-light) - - .el-icon { - display none - } .content-box { line-height 2 @@ -215,8 +208,9 @@ li { } .btn { + display inline-block margin-left 2rem - padding 2rem 4rem + padding 0 6rem font-size 12rem border-style solid border-width 1rem @@ -224,6 +218,7 @@ li { cursor pointer } + .confirm { color var(--el-color-primary) background var(--el-color-primary-light-9) @@ -236,6 +231,7 @@ li { } } + .cancel { color var(--el-color-info) background var(--el-color-info-light-9) diff --git a/src/styles/element-plus.styl b/src/styles/element-plus.styl index 3d70cb60..eaaa4240 100644 --- a/src/styles/element-plus.styl +++ b/src/styles/element-plus.styl @@ -6,3 +6,13 @@ --el-color-primary-light-8 #d9e2ff --el-color-primary-light-9 #ecefff } + +.el-message__icon { + width 15rem + height 15rem + + svg { + width 15rem + height 15rem + } +} diff --git a/src/utils/request/axios.ts b/src/utils/request/axios.ts index f006cbbd..a2393cb8 100644 --- a/src/utils/request/axios.ts +++ b/src/utils/request/axios.ts @@ -18,7 +18,7 @@ axios.interceptors.request.use( if (userConfig) { const { token } = userConfig if (config.baseURL?.includes(baseURL) && token) { - config.headers.Authorization = `token ${token}` + config.headers.Authorization = `Bearer ${token}` } } return config diff --git a/src/utils/storage.ts b/src/utils/storage.ts index f04485e3..cfc598a3 100644 --- a/src/utils/storage.ts +++ b/src/utils/storage.ts @@ -1,26 +1,35 @@ /** - * 获取 sessionStorage 的值 + * 获取 localStorage 值 * @param key */ -export const getSession = (key: string) => { - const temp = window.sessionStorage.getItem(key) +export const getLocal = (key: string) => { + const temp = window.localStorage.getItem(key) return temp ? JSON.parse(temp) : null } /** - * 设置 sessionStorage + * 设置 localStorage * @param key * @param value */ -export const setSession = (key: string, value: any) => { - sessionStorage.setItem(key, JSON.stringify(value)) +export const setLocal = (key: string, value: any) => { + localStorage.setItem(key, JSON.stringify(value)) } /** - * 获取 localStorage 值 + * 获取 sessionStorage 的值 * @param key */ -export const getLocal = (key: string) => { - const temp = window.localStorage.getItem(key) +export const getSession = (key: string) => { + const temp = window.sessionStorage.getItem(key) return temp ? JSON.parse(temp) : null } + +/** + * 设置 sessionStorage + * @param key + * @param value + */ +export const setSession = (key: string, value: any) => { + sessionStorage.setItem(key, JSON.stringify(value)) +} diff --git a/src/views/github-authorize/github-authorize.styl b/src/views/github-authorize/github-authorize.styl new file mode 100644 index 00000000..e69de29b diff --git a/src/views/github-authorize/github-authorize.vue b/src/views/github-authorize/github-authorize.vue new file mode 100644 index 00000000..8a7216cf --- /dev/null +++ b/src/views/github-authorize/github-authorize.vue @@ -0,0 +1,22 @@ + + + + + diff --git a/src/views/my-config/my-config.util.ts b/src/views/my-config/my-config.util.ts index f9c36dab..f4f35f77 100644 --- a/src/views/my-config/my-config.util.ts +++ b/src/views/my-config/my-config.util.ts @@ -11,34 +11,36 @@ import { createRepo, getGitHubUserInfo, initEmptyRepo } from '@/common/api' import { INIT_REPO_BARNCH, INIT_REPO_NAME } from '@/common/constant' import { formatDatetime } from '@/utils' import router from '@/router' +import i18n from '@/plugins/vue/i18n' const userConfigInfo = computed(() => store.getters.getUserConfigInfo).value /** * 重置图床配置 */ -export const resetConfig = () => { - store.dispatch('LOGOUT') +export const resetConfig = async () => { + await store.dispatch('LOGOUT') } /** * 持久化用户图床配置信息 */ -export const persistUserConfigInfo = () => { - store.dispatch('USER_CONFIG_INFO_PERSIST') +export const persistUserConfigInfo = async () => { + await store.dispatch('USER_CONFIG_INFO_PERSIST') } /** * 保存用户信息 * @param userInfo */ -export function saveUserInfo(userInfo: any) { +export async function saveUserInfo(userInfo: any) { userConfigInfo.logined = true + userConfigInfo.id = userInfo.id userConfigInfo.owner = userInfo.login userConfigInfo.name = userInfo.name userConfigInfo.email = userInfo.email userConfigInfo.avatarUrl = userInfo.avatar_url - persistUserConfigInfo() + await persistUserConfigInfo() } /** @@ -58,18 +60,18 @@ export const initReHandConfig = () => { /** * 前往 上传图片 页面 */ -export const goUploadPage = async ($t: any) => { +export const goUploadPage = async () => { const { selectedDir, dirMode } = userConfigInfo - let warningMessage: string = $t('config.message6') + let warningMessage: string = i18n.global.t('config.message6') if (selectedDir === '') { // eslint-disable-next-line default-case switch (dirMode) { case DirModeEnum.newDir: - warningMessage = $t('config.message7') + warningMessage = i18n.global.t('config.message7') break case DirModeEnum.repoDir: - warningMessage = $t('config.message8', { repo: userConfigInfo.selectedRepo }) + warningMessage = i18n.global.t('config.message8', { repo: userConfigInfo.selectedRepo }) break } ElMessage.warning({ message: warningMessage }) @@ -78,20 +80,61 @@ export const goUploadPage = async ($t: any) => { } } +/** + * GitHub APP 安装状态处理 + * @param repoInfo + * @param authorized + * @param token + */ +export const installedStatusHandle = async (repoInfo: any, authorized: boolean, token: string) => { + if (authorized && token) { + if (repoInfo) { + await store.dispatch('SET_GITHUB_AUTHORIZATION_INFO', { + installed: true + }) + } else { + const msgInstance = ElMessage({ + customClass: 'custom-message-container', + duration: 0, + offset: 20, + type: 'warning', + message: `
    + ${i18n.global.t('authorization.msg_2')} + + ${i18n.global.t('authorization.btn_1')} + +
    `, + dangerouslyUseHTMLString: true + }) + + document + .querySelector('.custom-message-container .authorization .confirm') + ?.addEventListener('click', () => { + msgInstance.close() + let url = import.meta.env.VITE_INSTALL_URL as string + if (userConfigInfo.id) { + url = import.meta.env.VITE_INSTALL_URL_USER + userConfigInfo.id + } + window.location.href = url + }) + } + } +} + /** * 一键自动配置图床 */ -export const oneClickAutoConfig = async ($t: any) => { +export const oneClickAutoConfig = async () => { const { token } = userConfigInfo if (!token) { - ElMessage.error({ message: $t('config.message1') }) + ElMessage.error({ message: i18n.global.t('config.message1') }) return } const loading = ElLoading.service({ lock: true, - text: $t('config.loading6') + text: i18n.global.t('config.loading6') }) try { @@ -100,18 +143,31 @@ export const oneClickAutoConfig = async ($t: any) => { if (!userInfo) { loading.close() - ElMessage.error({ message: $t('config.message2') }) + ElMessage.error({ message: i18n.global.t('config.message2') }) return } - saveUserInfo(userInfo) + if (!store.getters.getGitHubAuthorizationInfo.isAutoAuthorize) { + await store.dispatch('SET_GITHUB_AUTHORIZATION_INFO', { + manualToken: userConfigInfo.token + }) + } + + await saveUserInfo(userInfo) const repoInfo = await createRepo(userConfigInfo.token) console.log('createRepo >> ', repoInfo) + const authorizationInfo = computed(() => store.getters.getGitHubAuthorizationInfo).value + const { token, authorized } = authorizationInfo + + await installedStatusHandle(repoInfo, authorized, token) + if (!repoInfo) { loading.close() - ElMessage.error({ message: $t('config.message3') }) + if (!(authorized && token)) { + ElMessage.error({ message: i18n.global.t('config.message3') }) + } return } @@ -123,13 +179,13 @@ export const oneClickAutoConfig = async ($t: any) => { userConfigInfo.selectedDir = formatDatetime('yyyyMMdd') userConfigInfo.dirMode = DirModeEnum.autoDir userConfigInfo.dirList = [] - persistUserConfigInfo() + await persistUserConfigInfo() await initEmptyRepo(userConfigInfo, false) loading.close() - ElMessage.success({ message: $t('config.message4') }) + ElMessage.success({ message: i18n.global.t('config.message4') }) await router.push('/upload') } catch (err) { - ElMessage.error({ message: $t('config.message5') }) + ElMessage.error({ message: i18n.global.t('config.message5') }) console.error('oneClickAutoConfig >> ', err) } } diff --git a/src/views/my-config/my-config.vue b/src/views/my-config/my-config.vue index 89c7c41e..48158ea6 100644 --- a/src/views/my-config/my-config.vue +++ b/src/views/my-config/my-config.vue @@ -1,10 +1,13 @@