diff --git a/picx/src/App.vue b/picx/src/App.vue new file mode 100644 index 0000000..d239c0c --- /dev/null +++ b/picx/src/App.vue @@ -0,0 +1,74 @@ + + + + + diff --git a/picx/src/assets/logo.png b/picx/src/assets/logo.png new file mode 100644 index 0000000..a339708 Binary files /dev/null and b/picx/src/assets/logo.png differ diff --git a/picx/src/common/api/index.ts b/picx/src/common/api/index.ts new file mode 100644 index 0000000..50005a2 --- /dev/null +++ b/picx/src/common/api/index.ts @@ -0,0 +1,82 @@ +import { computed } from 'vue' +import axios from '@/utils/axios' +import { store } from '@/store' +import { getFileSuffix, isImage } from '@/utils/file-handle-helper' +import structureImageObject from '@/utils/image-helper' + +const userConfigInfo = computed(() => store.getters.getUserConfigInfo).value + +/** + * 获取指定路径(path)下的目录列表 + * @param path 路径 + */ +export const getDirListByPath = (path: string = '') => { + return new Promise((resolve) => { + axios + .get( + `/repos/${userConfigInfo.owner}/${userConfigInfo.selectedRepos}/contents/${path}`, + { + params: { + ref: userConfigInfo.selectedBranch + } + } + ) + .then((res: any) => { + if (res && res.status === 200 && res.data.length > 0) { + resolve( + res.data + .filter((v: any) => v.type === 'dir') + .map((x: any) => ({ + value: x.name, + label: x.name + })) + ) + } else { + resolve(null) + } + }) + .catch(() => { + resolve(null) + }) + }) +} + +/** + * 获取指定路径(path)下的目录和图片 + * @param path + */ +export const getContentByReposPath = (path: string = '') => { + return new Promise((resolve) => { + axios + .get( + `/repos/${userConfigInfo.owner}/${userConfigInfo.selectedRepos}/contents/${path}`, + { + params: { + ref: userConfigInfo.selectedBranch + } + } + ) + .then((res: any) => { + if (res && res.status === 200 && res.data.length > 0) { + res.data + .filter((v: any) => v.type === 'dir') + .forEach((x: any) => store.dispatch('DIR_IMAGE_LIST_ADD_DIR', x.path)) + + setTimeout(() => { + res.data + .filter((v: any) => v.type === 'file' && isImage(getFileSuffix(v.name))) + .forEach((x: any) => + store.dispatch('DIR_IMAGE_LIST_ADD_IMAGE', structureImageObject(x, path)) + ) + }, 100) + + resolve(true) + } else { + resolve(null) + } + }) + .catch(() => { + resolve(null) + }) + }) +} diff --git a/picx/src/common/model/delete.model.ts b/picx/src/common/model/delete.model.ts new file mode 100644 index 0000000..2a79dd6 --- /dev/null +++ b/picx/src/common/model/delete.model.ts @@ -0,0 +1,6 @@ +/* eslint-disable import/prefer-default-export */ +export enum deleteStatusEnum { + deleted = 'deleted', + allDeleted = 'allDeleted', + deleteFail = 'deleteFail' +} diff --git a/picx/src/common/model/dir.model.ts b/picx/src/common/model/dir.model.ts new file mode 100644 index 0000000..40fac92 --- /dev/null +++ b/picx/src/common/model/dir.model.ts @@ -0,0 +1,11 @@ +export interface DirModel { + value: string + label: string +} + +export enum DirModeEnum { + autoDir = 'autoDir', + newDir = 'newDir', + rootDir = 'rootDir', + reposDir = 'reposDir' +} diff --git a/picx/src/common/model/external-link.model.ts b/picx/src/common/model/external-link.model.ts new file mode 100644 index 0000000..82e6c4c --- /dev/null +++ b/picx/src/common/model/external-link.model.ts @@ -0,0 +1,8 @@ +enum ExternalLinkType { + staticaly = 'staticaly', + jsdelivr = 'jsdelivr', + github = 'github', + cloudflare = 'cloudflare' +} + +export default ExternalLinkType diff --git a/picx/src/common/model/storage.model.ts b/picx/src/common/model/storage.model.ts new file mode 100644 index 0000000..7f7a94b --- /dev/null +++ b/picx/src/common/model/storage.model.ts @@ -0,0 +1,6 @@ +const PICX_PREFIX = 'PICX_' + +export const PICX_CONFIG = `${PICX_PREFIX}CONFIG` +export const PICX_UPLOADED = `${PICX_PREFIX}UPLOADED` +export const PICX_MANAGEMENT = `${PICX_PREFIX}MANAGEMENT_MULTI` +export const PICX_SETTINGS = `${PICX_PREFIX}SETTINGS` diff --git a/picx/src/common/model/upload.model.ts b/picx/src/common/model/upload.model.ts new file mode 100644 index 0000000..2b91a39 --- /dev/null +++ b/picx/src/common/model/upload.model.ts @@ -0,0 +1,68 @@ +export enum UploadStatusEnum { + // eslint-disable-next-line no-unused-vars + uploaded = 'uploaded', + // eslint-disable-next-line no-unused-vars + allUploaded = 'allUploaded', + // eslint-disable-next-line no-unused-vars + uploadFail = 'uploadFail' +} + +export interface UploadedImageModel { + type: string + uuid: string + sha: string + dir: string + path: string + name: string + size: any + deleting: boolean + is_transform_md: boolean + checked: boolean + github_url: string + jsdelivr_cdn_url: string + staticaly_cdn_url: string + cloudflare_cdn_url: string +} + +export interface ToUploadImageModel { + uuid: string + + uploadStatus: { + progress: number + uploading: boolean + } + + imgData: { + base64Content: string + base64Url: string + } + + fileInfo: { + compressedSize?: number | undefined + originSize?: number | undefined + size: number | undefined + lastModified: number | undefined + } + + filename: { + name: string + hash: string + suffix: string + prefixName: string + now: string + initName: string + newName: string + isHashRename: boolean + isRename: boolean + isPrefix: boolean + } + + externalLink: { + github: string + jsdelivr: string + staticaly: string + cloudflare: string + } + + uploadedImg?: UploadedImageModel +} diff --git a/picx/src/common/model/user-config-info.model.ts b/picx/src/common/model/user-config-info.model.ts new file mode 100644 index 0000000..f11ed55 --- /dev/null +++ b/picx/src/common/model/user-config-info.model.ts @@ -0,0 +1,35 @@ +import { DirModeEnum, DirModel } from './dir.model' + +export interface ReposModel { + value: string + label: string + desc?: string +} + +export interface BranchModel { + value: string + label: string +} + +export enum BranchModeEnum { + newBranch = 'newBranch', + reposBranch = 'reposBranch' +} + +export interface UserConfigInfoModel { + token: string + owner: string + email: string + name: string + avatarUrl: string + selectedRepos: string + reposList: ReposModel[] + selectedBranch: string + branchMode: BranchModeEnum + branchList: BranchModel[] + dirMode: DirModeEnum + selectedDir: string + selectedDirList: string[] + dirList: DirModel[] + loggingStatus: boolean +} diff --git a/picx/src/common/model/user-settings.model.ts b/picx/src/common/model/user-settings.model.ts new file mode 100644 index 0000000..5d44ef4 --- /dev/null +++ b/picx/src/common/model/user-settings.model.ts @@ -0,0 +1,15 @@ +import { CompressEncoderMap } from '../../utils/compress' +import ExternalLinkType from '@/common/model/external-link.model' + +export interface UserSettingsModel { + defaultHash: boolean + defaultMarkdown: boolean + defaultPrefix: boolean + prefixName: string + themeMode: 'auto' | 'light' | 'dark' + autoLightThemeTime: string[] + isCompress: boolean + compressEncoder: CompressEncoderMap + elementPlusSize: 'large' | 'default' | 'small' + externalLinkType: ExternalLinkType +} diff --git a/picx/src/common/model/vite-config.model.ts b/picx/src/common/model/vite-config.model.ts new file mode 100644 index 0000000..19121e8 --- /dev/null +++ b/picx/src/common/model/vite-config.model.ts @@ -0,0 +1,11 @@ +export declare type Recordable = Record + +export declare interface ViteEnv { + VITE_PORT?: number + VITE_USE_PWA?: boolean + VITE_PUBLIC_PATH?: string + VITE_GLOB_APP_TITLE?: string + VITE_GLOB_APP_SHORT_NAME?: string + VITE_OPEN_BROWSER?: boolean + VITE_CORS?: boolean +} diff --git a/picx/src/components/copy-external-link/copy-external-link.styl b/picx/src/components/copy-external-link/copy-external-link.styl new file mode 100644 index 0000000..f51c446 --- /dev/null +++ b/picx/src/components/copy-external-link/copy-external-link.styl @@ -0,0 +1,62 @@ +@import "../../style/base.styl" + +.copy-external-link-box { + position relative + width 100% + height 100% + box-sizing border-box + display flex + justify-content space-between + align-items flex-end + padding 2rem + + .markdown-icon-box { + width 26rem + height 20rem + position relative + cursor pointer + + .markdown-icon { + path { + fill var(--markdown-icon-color) + } + + &.active { + path { + fill var(--markdown-icon-active-color) + } + } + } + } + + + .btn-box { + position relative + box-sizing border-box + height 100% + display flex + justify-content space-between + + .btn-item { + height 20rem + box-sizing border-box + border-radius 5rem + font-size 12rem + cursor pointer + transition all 0.3s ease + } + + .copy-url { + box-sizing border-box + border 1rem solid var(--default-text-color) + color var(--default-text-color) + padding 1rem 2rem + margin-right 4rem + + &:hover { + background var(--default-text-color) + color var(--background-color) + } + } + } +} diff --git a/picx/src/components/copy-external-link/copy-external-link.vue b/picx/src/components/copy-external-link/copy-external-link.vue new file mode 100644 index 0000000..edb4cfa --- /dev/null +++ b/picx/src/components/copy-external-link/copy-external-link.vue @@ -0,0 +1,104 @@ + + + + + diff --git a/picx/src/components/folder-card/folder-card.styl b/picx/src/components/folder-card/folder-card.styl new file mode 100644 index 0000000..b61ac4b --- /dev/null +++ b/picx/src/components/folder-card/folder-card.styl @@ -0,0 +1,47 @@ +@import '../../style/base.styl' + +.folder-card { + position relative + width 110rem + height 106rem + display flex + align-items center + flex-direction column + justify-content flex-start + cursor pointer + box-sizing border-box + padding 3rem + user-select none + + &:hover { + background var(--second-background-color) + } + + .icon { + display flex + align-items center + justify-content center + width 50rem + height 50rem + + svg { + width 100% + height 100% + } + } + + + .text { + width 90% + font-size 14rem + margin-top 5rem + text-align center + overflow hidden + text-overflow ellipsis + display -webkit-box + -webkit-box-orient vertical + -webkit-line-clamp 2 + word-wrap break-word + word-break break-all + } +} diff --git a/picx/src/components/folder-card/folder-card.vue b/picx/src/components/folder-card/folder-card.vue new file mode 100644 index 0000000..f332c32 --- /dev/null +++ b/picx/src/components/folder-card/folder-card.vue @@ -0,0 +1,116 @@ + + + + + diff --git a/picx/src/components/header-content/header-content.styl b/picx/src/components/header-content/header-content.styl new file mode 100644 index 0000000..825efab --- /dev/null +++ b/picx/src/components/header-content/header-content.styl @@ -0,0 +1,92 @@ +@import "../../style/base.styl" + +.header { + width 100% + height 100% + background var(--background-color) + padding 0 20rem + box-sizing border-box + display flex + justify-content space-between + align-items center + + .header-left { + height 100% + display flex + justify-content flex-start + + .brand { + height 100% + display flex + justify-content flex-start + align-items center + cursor pointer + + .logo { + width 46rem + height 46rem + margin-right 10rem + + img { + width 100% + } + + } + + + .title { + font-size 36rem + font-weight bold + } + } + + + .website-count { + box-sizing border-box + display flex + align-items flex-end + font-size 14rem + margin-left 10rem + padding-bottom 12rem + cursor pointer + + +picx-mobile() { + display none + } + } + } + + + .header-right { + .user-info { + display: flex; + align-items: center; + cursor: pointer; + + .username { + font-size: 16rem; + } + + .avatar { + display: flex; + justify-content: center; + align-items: center; + width: 38rem; + height: 38rem; + color: var(--default-text-color); + border-radius: 50%; + border: 1rem solid var(--default-text-color); + margin-left: 10rem; + padding: 1rem; + box-sizing: border-box; + + img { + width: 100%; + height: 100%; + border-radius: 50%; + } + } + + } + } +} diff --git a/picx/src/components/header-content/header-content.vue b/picx/src/components/header-content/header-content.vue new file mode 100644 index 0000000..79dcd9c --- /dev/null +++ b/picx/src/components/header-content/header-content.vue @@ -0,0 +1,122 @@ + + + + + diff --git a/picx/src/components/image-card/image-card.styl b/picx/src/components/image-card/image-card.styl new file mode 100644 index 0000000..1db2ce8 --- /dev/null +++ b/picx/src/components/image-card/image-card.styl @@ -0,0 +1,142 @@ +@import "../../style/base.styl" + +$infoBoxHeight = 56rem + +.image-card { + position relative + width 100% + height 100% + box-shadow 1rem 2rem 3rem var(--shadow-color) + box-sizing border-box + padding-bottom $infoBoxHeight + user-select none + + &.checked, &:hover { + box-shadow 0 0 10rem #666 + } + + &.listing { + display flex + justify-content flex-start + align-items center + padding 5rem + border-radius $box-border-radius + + .image-box { + height 45rem + width 45rem + } + + .info-box { + position relative + width 80% + } + + :deep(.el-loading-mask) { + .el-loading-spinner { + margin-top -25rem + + .circular { + height 24rem + width 24rem + } + + .el-loading-text { + margin 0 + } + } + + } + } + + + .image-box { + position relative + width 100% + height 100% + + img { + width 100% + height 100% + object-fit cover + cursor pointer + } + } + + + .info-box { + width 100% + height $infoBoxHeight + position absolute + bottom 0 + left 0 + + .image-info { + width 100% + height 100% + padding 5rem + color var(--default-text-color) + box-sizing border-box + display flex + flex-direction column + justify-content space-between + + .rename-input { + height 20rem + display flex + margin-bottom 4rem + } + + .filename { + height 16rem + overflow hidden + text-overflow ellipsis + white-space nowrap + font-size 14rem + margin-bottom 6rem + } + } + } + + + .operation-box { + position absolute + top 10rem + right 8rem + width calc(100% - 16rem) + display flex + justify-content space-between + + .operation-left { + .picked-btn { + i { + font-weight bold + } + } + } + + .operation-right { + display flex + } + + .operation-btn { + width 32rem + height 32rem + border-radius 50% + box-shadow 0 0 6rem #555 + cursor pointer + background var(--background-color) + display flex + justify-content center + align-items center + margin-right 8rem + font-size 18rem + + &:last-child { + margin-right 0 + } + } + } + + +} diff --git a/picx/src/components/image-card/image-card.vue b/picx/src/components/image-card/image-card.vue new file mode 100644 index 0000000..4df449a --- /dev/null +++ b/picx/src/components/image-card/image-card.vue @@ -0,0 +1,287 @@ + + + + + diff --git a/picx/src/components/image-selector/image-selector.styl b/picx/src/components/image-selector/image-selector.styl new file mode 100644 index 0000000..2a3a18c --- /dev/null +++ b/picx/src/components/image-selector/image-selector.styl @@ -0,0 +1,52 @@ +@import "../../style/base.styl" + +.selector-wrapper { + padding 4rem 12rem + width 100% + box-sizing border-box + display flex + justify-content space-between + align-items center + border-bottom 1rem solid var(--third-background-color) + + + .selector-left-box { + display flex + align-items center + + :deep(.el-checkbox) { + font-weight unset + } + + :deep(.el-checkbox__label ) { + line-height unset + } + + .cancel-select-btn { + color #576b95 + cursor pointer + } + + div.item { + margin-left 8rem + } + } + + .selector-right-box { + .btn-icon { + cursor: pointer; + font-size: 22rem; + margin-left: 10rem; + } + + } +} + +.temp-batch-externalink { + opacity 0 + position absolute + left -9999rem + top -9999rem + width 0 + height 0 +} diff --git a/picx/src/components/image-selector/image-selector.vue b/picx/src/components/image-selector/image-selector.vue new file mode 100644 index 0000000..45162f3 --- /dev/null +++ b/picx/src/components/image-selector/image-selector.vue @@ -0,0 +1,103 @@ + + + + diff --git a/picx/src/components/image-viewer/image-viewer.styl b/picx/src/components/image-viewer/image-viewer.styl new file mode 100644 index 0000000..7f88182 --- /dev/null +++ b/picx/src/components/image-viewer/image-viewer.styl @@ -0,0 +1,75 @@ +$transition-duration = 0.3s +$transition-delay = 0s + +.image-viewer { + position fixed + left 0 + top 0 + width 100% + height 100% + display flex + align-items center + justify-content center + background rgba(0, 0, 0, 0) + visibility hidden + z-index 1000 + padding 6% + box-sizing border-box + transition-property visibility, background + transition-delay $transition-delay, $transition-delay + transition-duration $transition-duration, $transition-duration + transition-timing-function ease, ease + + &.active { + background rgba(0, 0, 0, 0.6) + visibility visible + + .image-box { + transform scale(1) + padding 2rem + + .image-info { + display block + } + } + + + } + + + .image-box { + position relative; + width 60% + height 100% + display flex + flex-direction column + justify-content center + align-items center + transform scale(0) + transition-property transform + transition-delay $transition-delay + transition-duration $transition-duration + transition-timing-function ease + + + @media (max-width: 1200px) { + width 80% + } + + .img { + cursor zoom-out + max-width 100% + max-height 100% + } + + .image-info { + display none + padding 10rem + + .item { + margin 0 6rem + } + } + + } +} diff --git a/picx/src/components/image-viewer/image-viewer.vue b/picx/src/components/image-viewer/image-viewer.vue new file mode 100644 index 0000000..a2512ca --- /dev/null +++ b/picx/src/components/image-viewer/image-viewer.vue @@ -0,0 +1,44 @@ + + + + + diff --git a/picx/src/components/main-container/main-container.styl b/picx/src/components/main-container/main-container.styl new file mode 100644 index 0000000..523c84e --- /dev/null +++ b/picx/src/components/main-container/main-container.styl @@ -0,0 +1,73 @@ +@import "../../style/base.styl" + +$top-height = 60rem +$left-side-width = 80rem + +.main-container { + position absolute + box-sizing border-box + width 100% + height 100% + background var(--second-background-color) + padding-top $top-height + font-size 15rem + + + + .top { + position absolute + top 0 + left 0 + box-sizing border-box + width 100% + height $top-height + } + + + .bottom { + position relative + box-sizing border-box + width 100% + height 100% + padding-top $component-interval + + + .container { + position relative + box-sizing border-box + width 100% + height 100% + padding-left $left-side-width + + + .left { + position absolute + box-sizing border-box + width $left-side-width + height 100% + top 0 + left 0 + } + + + .right { + position relative + width 100% + height 100% + box-sizing border-box + padding 0 $component-interval 0 $component-interval + + + .content { + position relative + box-sizing border-box + width 100% + height 100% + } + } + + } + + + } +} diff --git a/picx/src/components/main-container/main-container.vue b/picx/src/components/main-container/main-container.vue new file mode 100644 index 0000000..088f3f4 --- /dev/null +++ b/picx/src/components/main-container/main-container.vue @@ -0,0 +1,64 @@ + + + + + diff --git a/picx/src/components/nav-content/nav-content.styl b/picx/src/components/nav-content/nav-content.styl new file mode 100644 index 0000000..6cbcac4 --- /dev/null +++ b/picx/src/components/nav-content/nav-content.styl @@ -0,0 +1,40 @@ +@import "../../style/base.styl" + +.nav { + position relative + width 100% + height 100% + box-sizing border-box + background var(--background-color) + + ul.nav-list { + padding 0 + margin 0 + + li.nav-item { + box-sizing border-box + width 100% + height 76rem + cursor pointer + + &.active { + font-weight bold + background var(--second-background-color) + } + + .nav-content { + display flex + flex-direction column + justify-content center + align-items center + + .nav-name { + margin-top 5rem + font-size 12rem + } + } + } + + } + +} diff --git a/picx/src/components/nav-content/nav-content.vue b/picx/src/components/nav-content/nav-content.vue new file mode 100644 index 0000000..10dc70e --- /dev/null +++ b/picx/src/components/nav-content/nav-content.vue @@ -0,0 +1,152 @@ + + + + + diff --git a/picx/src/components/selected-info-bar/selected-info-bar.styl b/picx/src/components/selected-info-bar/selected-info-bar.styl new file mode 100644 index 0000000..eb3f16e --- /dev/null +++ b/picx/src/components/selected-info-bar/selected-info-bar.styl @@ -0,0 +1,16 @@ +.selected-info-bar-box { + height 100% + display flex + align-items center + justify-content flex-start + font-size 12rem + box-sizing border-box + + .info-item { + margin-right 8rem + + &:last-child { + margin-right 0 + } + } +} diff --git a/picx/src/components/selected-info-bar/selected-info-bar.vue b/picx/src/components/selected-info-bar/selected-info-bar.vue new file mode 100644 index 0000000..0fbaabb --- /dev/null +++ b/picx/src/components/selected-info-bar/selected-info-bar.vue @@ -0,0 +1,35 @@ + + + + + diff --git a/picx/src/components/site-count/site-count.vue b/picx/src/components/site-count/site-count.vue new file mode 100644 index 0000000..685b6ac --- /dev/null +++ b/picx/src/components/site-count/site-count.vue @@ -0,0 +1,58 @@ + + + + diff --git a/picx/src/components/to-upload-image-card/to-upload-image-card.styl b/picx/src/components/to-upload-image-card/to-upload-image-card.styl new file mode 100644 index 0000000..10c8d52 --- /dev/null +++ b/picx/src/components/to-upload-image-card/to-upload-image-card.styl @@ -0,0 +1,211 @@ +@import "../../style/base.styl" + +$info-item-height = 68rem +$info-item-border = 1rem +$info-item-padding = 5rem +$compressed-file-background-color = #228eff +$image-width = $info-item-height - ($info-item-border * 2) + +.to-upload-image-list-card { + position relative + width 100% + box-sizing border-box + margin-top 6rem + + .header { + width 100% + height 30rem + box-sizing border-box + font-size 12rem + display flex + align-items center + justify-content space-between + padding-bottom 6rem + } + + .body { + width 100% + height 100% + max-height 170rem + overflow-y auto + box-sizing border-box + padding 10rem + border 1rem solid var(--border-color) + margin-top 10rem + + &::-webkit-scrollbar { + width 5rem + } + + &::-webkit-scrollbar-thumb { + border-radius 2rem + } + + .image-uploading-info-box { + position relative + width 100% + box-sizing border-box + padding 0 + margin 0 + + .image-uploading-info-item { + position relative + box-sizing border-box + width 100% + height $info-item-height + border $info-item-border solid var(--border-color) + border-radius 5rem + margin-bottom 10rem + overflow hidden + font-size 15rem + padding-left $image-width + transition all 0.3s ease + + &.disable { + pointer-events none + cursor not-allowed + } + + &:last-child { + margin-bottom 0 + } + + &:hover { + box-shadow 0 0 5rem var(--shadow-hover-color) + } + + .left-image-box { + position absolute + top 0 + left 0 + width $image-width + height 100% + box-sizing border-box + margin-right 5rem + + img { + object-fit cover + width 100% + height 100% + overflow hidden + cursor pointer + border-top-left-radius 5rem + border-bottom-left-radius 5rem + } + } + + + .right-operation-box { + position relative + width 100% + height 100% + box-sizing border-box + padding $info-item-padding 20rem $info-item-padding $info-item-padding + + .top, .bottom { + width 100% + height 50% + box-sizing border-box + padding 0 5rem + } + + .top { + display flex + justify-content space-between + + .image-name, + .image-info { + display flex + align-items center + box-sizing border-box + height 100% + } + + .image-name { + font-size 13rem + overflow hidden + text-overflow ellipsis + white-space nowrap + } + + .image-info { + font-size 12rem + + .item { + padding 1rem 4rem + background var(--third-background-color) + border-radius 2rem + margin-left 10rem + + &.compressed { + color $compressed-file-background-color + } + } + } + } + + + .bottom { + display flex + align-items center + + &.rename-operation { + + .el-checkbox { + margin-right 20rem + } + + .rename-input { + input { + height 23rem + line-height 23rem + } + } + } + } + } + + + .upload-status-box { + box-sizing border-box + color #fff + position absolute + right -17rem + top -7rem + width 46rem + height 26rem + text-align center + transform rotate(45deg) + box-shadow 0 1rem 1rem var(--border-color) + + &.wait-upload { + background var(--await-upload-color) + } + + &.uploading { + background var(--uploading-color) + } + + &.uploaded { + background var(--uploaded-color) + } + + i { + font-size 12rem + margin-top 12rem + transform rotate(-45deg) + } + } + + .remove-to-upload-image { + position absolute + bottom 5rem + right 5rem + cursor pointer + } + + } + } + + } +} diff --git a/picx/src/components/to-upload-image-card/to-upload-image-card.vue b/picx/src/components/to-upload-image-card/to-upload-image-card.vue new file mode 100644 index 0000000..99d6614 --- /dev/null +++ b/picx/src/components/to-upload-image-card/to-upload-image-card.vue @@ -0,0 +1,300 @@ + + + + + diff --git a/picx/src/components/tutorials-step/step1.vue b/picx/src/components/tutorials-step/step1.vue new file mode 100644 index 0000000..af8e1ac --- /dev/null +++ b/picx/src/components/tutorials-step/step1.vue @@ -0,0 +1,40 @@ + + + + + diff --git a/picx/src/components/tutorials-step/step2.vue b/picx/src/components/tutorials-step/step2.vue new file mode 100644 index 0000000..924264b --- /dev/null +++ b/picx/src/components/tutorials-step/step2.vue @@ -0,0 +1,54 @@ + + + + + diff --git a/picx/src/components/tutorials-step/step3.vue b/picx/src/components/tutorials-step/step3.vue new file mode 100644 index 0000000..ff9acd5 --- /dev/null +++ b/picx/src/components/tutorials-step/step3.vue @@ -0,0 +1,62 @@ + + + diff --git a/picx/src/components/upload-area/upload-area.styl b/picx/src/components/upload-area/upload-area.styl new file mode 100644 index 0000000..54c2900 --- /dev/null +++ b/picx/src/components/upload-area/upload-area.styl @@ -0,0 +1,57 @@ +@import "../../style/base.styl" + +.upload-area { + position: relative; + width: 100%; + height: 300rem; + border: 4rem dashed var(--third-text-color) + box-sizing border-box + display: flex; + align-items: center; + justify-content: center; + z-index: 999; + + &.focus { + border-color: var(--upload-area-focus-color); + } + + &:hover { + border-color: var(--upload-area-focus-color); + } + + label { + display: block; + position: absolute; + width: 100%; + height: 100%; + z-index: 1000; + cursor: pointer; + } + + input[type="file"] { + position: absolute; + left: -9999rem; + top: -9999rem; + } + + .tips { + text-align: center; + color: #aaa; + + .icon { + font-size: 100rem; + } + + .text { + cursor: default; + font-size: 20rem; + } + } + + img { + object-fit: cover; + width: 100%; + height: 100%; + } + +} diff --git a/picx/src/components/upload-area/upload-area.vue b/picx/src/components/upload-area/upload-area.vue new file mode 100644 index 0000000..2169abf --- /dev/null +++ b/picx/src/components/upload-area/upload-area.vue @@ -0,0 +1,148 @@ + + + + + diff --git a/picx/src/main.ts b/picx/src/main.ts new file mode 100644 index 0000000..aac61c5 --- /dev/null +++ b/picx/src/main.ts @@ -0,0 +1,21 @@ +import { createApp } from 'vue' +import * as ElementPlusIconsVue from '@element-plus/icons-vue' +import router from '@/router/index' +import { key, store } from '@/store' +import App from './App.vue' +import 'element-plus/theme-chalk/dark/css-vars.css' + +if (import.meta.env.MODE === 'production') { + // @ts-ignore + import('@/utils/register-sw.ts') +} + +const app = createApp(App) + +// import element-plus icons +// eslint-disable-next-line no-restricted-syntax +for (const [key, component] of Object.entries(ElementPlusIconsVue)) { + app.component(key, component) +} +// @ts-ignore +app.use(router).use(store, key).mount('#app') diff --git a/picx/src/plugins/index.ts b/picx/src/plugins/index.ts new file mode 100644 index 0000000..b9848df --- /dev/null +++ b/picx/src/plugins/index.ts @@ -0,0 +1,32 @@ +import type { Plugin } from 'vite' +import vue from '@vitejs/plugin-vue' +import AutoImport from 'unplugin-auto-import/vite' +import Components from 'unplugin-vue-components/vite' +import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' +import { ViteEnv } from '@/common/model/vite-config.model' + +import configPWAPlugin from './pwa' + +export default function createVitePlugins(viteEnv: ViteEnv, isBuild: boolean) { + const vitePlugins: (Plugin | Plugin[])[] = [vue()] + + // On-demand import style for Element Plus + vitePlugins.push( + AutoImport({ + resolvers: [ElementPlusResolver()] + }), + Components({ + resolvers: [ElementPlusResolver()] + }) + ) + + // production env + if (isBuild) { + // add plugin vite-plugin-pwa + if (viteEnv.VITE_USE_PWA) { + vitePlugins.push(configPWAPlugin(viteEnv)) + } + } + + return vitePlugins +} diff --git a/picx/src/plugins/pwa.ts b/picx/src/plugins/pwa.ts new file mode 100644 index 0000000..9d89758 --- /dev/null +++ b/picx/src/plugins/pwa.ts @@ -0,0 +1,24 @@ +/** + * Zero config PWA for Vite + * Plugin: vite-plugin-pwa + * https://github.com/antfu/vite-plugin-pwa + */ +import { VitePWA } from 'vite-plugin-pwa' +import { ViteEnv } from '@/common/model/vite-config.model' + +export default function configPWAPlugin(env: ViteEnv) { + return VitePWA({ + registerType: 'autoUpdate', + manifest: { + name: env.VITE_GLOB_APP_TITLE, + short_name: env.VITE_GLOB_APP_SHORT_NAME, + icons: [ + { + src: './logo@192x192.png', + sizes: '192x192', + type: 'image/png' + } + ] + } + }) +} diff --git a/picx/src/router/index.ts b/picx/src/router/index.ts new file mode 100644 index 0000000..ef83833 --- /dev/null +++ b/picx/src/router/index.ts @@ -0,0 +1,82 @@ +import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router' +import config from '@/views/config/config.vue' +import upload from '@/views/upload/upload.vue' +import management from '@/views/management/management.vue' +import tutorials from '@/views/tutorials/tutorials.vue' +import settings from '@/views/settings/settings.vue' +import { store } from '@/store' + +const titleSuffix = ` | PicX 图床神器` + +const routes: Array = [ + { + path: '/', + name: 'index', + redirect: { + name: 'upload' + } + }, + { + path: '/config', + name: 'config', + component: config, + meta: { + title: `图床配置${titleSuffix}` + } + }, + { + path: '/upload', + name: 'upload', + component: upload, + meta: { + title: `图片上传${titleSuffix}` + } + }, + { + path: '/management', + name: 'Management', + component: management, + meta: { + title: `图床管理${titleSuffix}` + } + }, + { + path: '/tutorials', + name: 'tutorials', + component: tutorials, + meta: { + title: `使用教程${titleSuffix}` + } + }, + { + path: '/about', + name: 'about', + component: () => import('@/views/about/about.vue'), + meta: { + title: `帮助反馈${titleSuffix}` + } + }, + { + path: '/settings', + name: 'settings', + component: settings, + meta: { + title: `我的设置${titleSuffix}` + } + } +] + +const router = createRouter({ + history: createWebHashHistory(), + routes +}) + +router.beforeEach((to, from, next) => { + if (to.meta.title) (window).document.title = to.meta.title + if (from.path === '/management') { + store.dispatch('USER_CONFIG_INFO_RESET') + } + next() +}) + +export default router diff --git a/picx/src/shims-vue.d.ts b/picx/src/shims-vue.d.ts new file mode 100644 index 0000000..d53ab3d --- /dev/null +++ b/picx/src/shims-vue.d.ts @@ -0,0 +1,6 @@ +declare module '*.vue' { + import { DefineComponent } from 'vue' + + const component: DefineComponent<{}, {}, any> + export default component +} diff --git a/picx/src/store/index.ts b/picx/src/store/index.ts new file mode 100644 index 0000000..5209066 --- /dev/null +++ b/picx/src/store/index.ts @@ -0,0 +1,57 @@ +import { InjectionKey } from 'vue' +import { createStore, Store, useStore as baseUseStore } from 'vuex' +import RootStateTypes, { AllStateTypes } from './types' +import dirImageListModule from './modules/dir-image-list' +import toUploadImageModule from './modules/to-upload-image' +import uploadedImageListModule from './modules/uploaded-image-list' +import userConfigInfoModule from './modules/user-config-info' +import imageViewerModule from './modules/image-viewer' +import imageCardModule from './modules/image-card' +import uploadAreaActiveModule from './modules/upload-area-active' +import uploadSettingsModule from './modules/upload-settings' +import userSettingsModule from './modules/user-settings' + +// Create a new store instance. +export const store = createStore({ + modules: { + dirImageListModule, + toUploadImageModule, + uploadedImageListModule, + userConfigInfoModule, + imageViewerModule, + imageCardModule, + uploadAreaActiveModule, + uploadSettingsModule, + userSettingsModule + }, + + state: { + rootName: 'root' + }, + + mutations: {}, + + actions: { + // 退出登录(删除 localStorage 和 sessionStorage 数据,清空 state 的值) + LOGOUT({ dispatch, commit }) { + dispatch('DIR_IMAGE_LOGOUT') + dispatch('TO_UPLOAD_IMAGE_LOGOUT') + dispatch('UPLOADED_LIST_LOGOUT') + dispatch('USER_CONFIG_INFO_LOGOUT') + commit('IMAGE_VIEWER_LOGOUT') + commit('UPLOAD_AREA_ACTIVE_LOGOUT') + commit('UPLOAD_SETTINGS_LOGOUT') + dispatch('USER_SETTINGS_LOGOUT') + localStorage.clear() + sessionStorage.clear() + } + }, + + getters: {} +}) + +export const key: InjectionKey> = Symbol('vuex-store') + +export function useStore() { + return baseUseStore(key) +} diff --git a/picx/src/store/modules/dir-image-list/index.ts b/picx/src/store/modules/dir-image-list/index.ts new file mode 100644 index 0000000..d482d6c --- /dev/null +++ b/picx/src/store/modules/dir-image-list/index.ts @@ -0,0 +1,260 @@ +import { Module } from 'vuex' +import { PICX_MANAGEMENT } from '@/common/model/storage.model' +import DirImageListStateTypes, { DirObject } from './types' +import RootStateTypes from '../../types' +import { + createDirObject, + getUpLevelDirList, + getUpOneLevelDir +} from '@/store/modules/dir-image-list/utils' +import { UploadedImageModel } from '@/common/model/upload.model' +import { getDirContent } from '@/views/management/management.util' + +const initDirObject = () => { + const dirObj = localStorage.getItem(PICX_MANAGEMENT) + return dirObj ? JSON.parse(dirObj) : createDirObject('/', '/') +} + +const dirImageListModule: Module = { + state: { + name: 'dirImageListModule', + dirObject: initDirObject() + }, + + mutations: {}, + + actions: { + // 图床管理 - 增加目录 + DIR_IMAGE_LIST_ADD_DIR({ state, dispatch }, dirPath: string) { + if (dirPath === '/') { + return + } + + const findAssign = (dirObj: DirObject, dir: string, dirPath: string) => { + if (dirObj) { + if (!dirObj.childrenDirs.some((v: DirObject) => v.dir === dir)) { + dirObj.childrenDirs.push(createDirObject(dir, dirPath)) + } + const temp = dirObj.childrenDirs.find((x: DirObject) => x.dir === dir) + return temp || createDirObject(dir, dirPath) + } + return createDirObject(dir, dirPath) + } + + const dirList: string[] = dirPath.split('/') + let dirPathC = '' + let tempDirObj: DirObject = state.dirObject + + // eslint-disable-next-line no-plusplus + for (let i = 0, len = dirList.length; i < len; i++) { + const dirName = dirList[i] + dirPathC += `${i > 0 ? '/' : ''}${dirName}` + tempDirObj = findAssign(tempDirObj, dirName, dirPathC) + + if (i === 0) { + dispatch('USER_CONFIG_INFO_ADD_DIR', dirName) + } + } + + dispatch('DIR_IMAGE_LIST_PERSIST') + }, + + // 图床管理 - 删除目录 + DIR_IMAGE_LIST_REMOVE_DIR({ state, dispatch }, dirPath: string) { + if (dirPath === '/') { + return + } + + const rmDir = (dirObj: DirObject, dir: string, isRm: boolean) => { + if (dir === '/') { + return state.dirObject + } + + const temp = dirObj.childrenDirs.find((v) => v.dir === dir) + if (!temp) { + return dirObj + } + + if (isRm) { + const rmIndex = dirObj.childrenDirs.findIndex((v: any) => v.dir === dir) + if (rmIndex !== -1) { + dirObj.childrenDirs.splice(rmIndex, 1) + } + } + + return temp + } + + const dirList = dirPath.split('/') + + let tempDirObj = state.dirObject + dirList.forEach((d, i) => { + tempDirObj = rmDir(tempDirObj, d, i === dirList.length - 1) + + // 删除在用户配置信息模块里的目录项 + if (i === 0) { + dispatch('USER_CONFIG_INFO_REMOVE_DIR', d) + } + }) + + dispatch('DIR_IMAGE_LIST_PERSIST') + }, + + // 图床管理 - 增加图片 + DIR_IMAGE_LIST_ADD_IMAGE({ state, dispatch }, item: UploadedImageModel) { + const addImg = ( + dirObj: DirObject, + dir: string, + Img: UploadedImageModel, + isAdd: boolean = false + ) => { + if (!dirObj) { + return state.dirObject + } + + const temp = dirObj.childrenDirs?.find((x: DirObject) => x.dir === dir) + if (!temp) { + return state.dirObject + } + + if (isAdd && !temp.imageList.some((v) => v.name === Img.name)) { + temp.imageList.push(Img) + } + + return temp + } + + let tempDirObj: DirObject = state.dirObject + + if (item.dir === '/') { + if (!tempDirObj.imageList.some((v) => v.name === item.name)) { + tempDirObj.imageList.push(item) + } + } else { + const dirList: string[] = item.dir.split('/') + dirList.forEach((dir, i) => { + tempDirObj = addImg(tempDirObj, dir, item, i === dirList.length - 1) + }) + } + dispatch('DIR_IMAGE_LIST_PERSIST') + }, + + // 图床管理 - 删除图片(即删除指定目录里的指定图片) + DIR_IMAGE_LIST_REMOVE({ state, dispatch }, item: any) { + // 删除 + const rm = (list: UploadedImageModel[], uuid: string) => { + if (list.length) { + const rmIndex = list.findIndex((v: any) => v.uuid === uuid) + if (rmIndex !== -1) { + list.splice(rmIndex, 1) + } + } + } + + // 删除图片 + const rmImg = ( + dirObj: DirObject, + dir: string, + img: UploadedImageModel, + isRm: boolean + ) => { + if (!dirObj) { + return state.dirObject + } + + const temp = dirObj.childrenDirs.find((x: DirObject) => x.dir === dir) + if (!temp) { + return state.dirObject + } + + if (temp.dir === dir && isRm) { + rm(temp.imageList, img.uuid) + } + + return temp + } + + const { dir, uuid } = item + + if (dir === '/') { + rm(state.dirObject.imageList, uuid) + dispatch('DIR_IMAGE_LIST_PERSIST') + return + } + + const dirList: string[] = dir.split('/') + let tempDirObj: DirObject = state.dirObject + + dirList.forEach((d, i) => { + tempDirObj = rmImg(tempDirObj, d, item, i === dirList.length - 1) + if (!tempDirObj.imageList.length && !tempDirObj.childrenDirs.length) { + const dirPathList = getUpLevelDirList(tempDirObj.dirPath) + + // 循环遍历判断上一级目录的内容是否为空,为空则删除,依次往上查找,直到根目录 + dirPathList.forEach((dp) => { + const dpc = getDirContent(dp, state.dirObject) + if (dpc && !dpc.imageList.length && !dpc.childrenDirs.length) { + const { dirPath } = getUpOneLevelDir(dp) + dispatch('SET_USER_CONFIG_INFO', { selectedDir: dirPath }) + dispatch('DIR_IMAGE_LIST_REMOVE_DIR', dp) + } + }) + } + }) + }, + + // 图床管理 - 初始化指定目录(即删除指定目录的子目录列表和图片列表) -- OK + DIR_IMAGE_LIST_INIT_DIR({ state, dispatch }, dirPath: string) { + let tempDirObj = state.dirObject + + if (dirPath === '/') { + tempDirObj.imageList = [] + tempDirObj.childrenDirs = [] + dispatch('DIR_IMAGE_LIST_PERSIST') + return + } + + const initDirObject = (dirObj: DirObject, dir: string, isInit: boolean) => { + if (!dirObj) { + return state.dirObject + } + + const temp = dirObj.childrenDirs?.find((x: DirObject) => x.dir === dir) + if (!temp) { + return state.dirObject + } + + if (isInit) { + temp.imageList = [] + temp.childrenDirs = [] + } + + return temp + } + + const dirList = dirPath.split('/') + + dirList.forEach((d, i) => { + tempDirObj = initDirObject(tempDirObj, d, i === dirList.length - 1) + }) + + dispatch('DIR_IMAGE_LIST_PERSIST') + }, + + // 图床管理 - 持久化存储 -- OK + DIR_IMAGE_LIST_PERSIST({ state }) { + localStorage.setItem(PICX_MANAGEMENT, JSON.stringify(state.dirObject)) + }, + + // 图床管理 - 退出登录 + DIR_IMAGE_LOGOUT({ state }) { + state.dirObject = createDirObject('/', '/') + } + }, + + getters: { + getDirObject: (state: any) => state.dirObject + } +} + +export default dirImageListModule diff --git a/picx/src/store/modules/dir-image-list/types.ts b/picx/src/store/modules/dir-image-list/types.ts new file mode 100644 index 0000000..fdda470 --- /dev/null +++ b/picx/src/store/modules/dir-image-list/types.ts @@ -0,0 +1,14 @@ +import { UploadedImageModel } from '@/common/model/upload.model' + +export interface DirObject { + type: 'dir' + dir: string + dirPath: string + childrenDirs: DirObject[] + imageList: UploadedImageModel[] +} + +export default interface DirImageListStateTypes { + name: string + dirObject: DirObject +} diff --git a/picx/src/store/modules/dir-image-list/utils.ts b/picx/src/store/modules/dir-image-list/utils.ts new file mode 100644 index 0000000..3d4694d --- /dev/null +++ b/picx/src/store/modules/dir-image-list/utils.ts @@ -0,0 +1,73 @@ +import { DirObject } from '@/store/modules/dir-image-list/types' + +/** + * 构造一个新的目录对象 + * @param dir + * @param dirPath + */ +export const createDirObject = (dir: string, dirPath: string): DirObject => { + return { + type: 'dir', + dir, + dirPath, + childrenDirs: [], + imageList: [] + } +} + +/** + * 获取上一级目录 + * @param dirPath + */ +export const getUpOneLevelDir = (dirPath: string) => { + if (dirPath === '/') { + return { + currentDir: '/', + dirPath: '/' + } + } + + const dirList = dirPath.split('/') + + if (dirList.length === 1) { + return { + currentDir: '/', + dirPath: '/' + } + } + + if (dirList.length > 1) { + dirList.length -= 1 + return { + currentDir: dirList[dirList.length - 1], + dirPath: dirList.join('/') + } + } + + return { + currentDir: '/', + dirPath: '/' + } +} + +/** + * 获取上级目录列表 + * @param dirPath + */ +export const getUpLevelDirList = (dirPath: string) => { + if (dirPath === '/') { + return [] + } + + const dirList = dirPath.split('/') + + const tempL: string[] = [] + let tempP = '' + + dirList.forEach((d, i) => { + tempP += `${i > 0 ? '/' : ''}${d}` + tempL.unshift(tempP) + }) + + return tempL +} diff --git a/picx/src/store/modules/image-card/index.ts b/picx/src/store/modules/image-card/index.ts new file mode 100644 index 0000000..32c4e42 --- /dev/null +++ b/picx/src/store/modules/image-card/index.ts @@ -0,0 +1,41 @@ +import { Module } from 'vuex' +import { ImageCardStateTypes } from './types' +import RootStateTypes from '../../types' +import { UploadedImageModel } from '@/common/model/upload.model' + +const imageCardModule: Module = { + state: { + imgCardArr: [] + }, + mutations: { + IMAGE_CARD(state: ImageCardStateTypes, { imageObj }) { + const { uuid, checked } = imageObj + if (checked) { + state.imgCardArr.forEach((item) => { + if (item.uuid === uuid) { + // eslint-disable-next-line no-param-reassign + item.checked = true + } + }) + } + }, + REPLACE_IMAGE_CARD(state: ImageCardStateTypes, { checkedImgArr }) { + if (checkedImgArr.length > 0) { + state.imgCardArr = checkedImgArr + } else { + state.imgCardArr = [] + } + } + }, + actions: {}, + getters: { + getImageCardArr: (state: ImageCardStateTypes) => state.imgCardArr, + getImageCardCheckedArr: (state: ImageCardStateTypes) => { + return state.imgCardArr.filter((item: UploadedImageModel) => { + return item.checked + }) + } + } +} + +export default imageCardModule diff --git a/picx/src/store/modules/image-card/types.ts b/picx/src/store/modules/image-card/types.ts new file mode 100644 index 0000000..b750664 --- /dev/null +++ b/picx/src/store/modules/image-card/types.ts @@ -0,0 +1,5 @@ +import { UploadedImageModel } from '@/common/model/upload.model' + +export interface ImageCardStateTypes { + imgCardArr: UploadedImageModel[] +} diff --git a/picx/src/store/modules/image-viewer/index.ts b/picx/src/store/modules/image-viewer/index.ts new file mode 100644 index 0000000..07a2bf3 --- /dev/null +++ b/picx/src/store/modules/image-viewer/index.ts @@ -0,0 +1,29 @@ +import { Module } from 'vuex' +import ImageViewerStateTypes from './types' +import RootStateTypes from '../../types' + +const imageViewerModule: Module = { + state: { + imageViewer: { + imgInfo: null, + isShow: false + } + }, + mutations: { + IMAGE_VIEWER(state: ImageViewerStateTypes, { imgInfo, isShow }) { + state.imageViewer.imgInfo = imgInfo + state.imageViewer.isShow = isShow + }, + + IMAGE_VIEWER_LOGOUT(state: ImageViewerStateTypes) { + state.imageViewer.isShow = false + state.imageViewer.imgInfo = null + } + }, + actions: {}, + getters: { + getImageViewer: (state: ImageViewerStateTypes) => state.imageViewer + } +} + +export default imageViewerModule diff --git a/picx/src/store/modules/image-viewer/types.ts b/picx/src/store/modules/image-viewer/types.ts new file mode 100644 index 0000000..fbbcf07 --- /dev/null +++ b/picx/src/store/modules/image-viewer/types.ts @@ -0,0 +1,12 @@ +export interface ImgInfo { + name: string + size: number + lastModified: number + url: string +} +export default interface ImageViewerStateTypes { + imageViewer: { + imgInfo: ImgInfo | null + isShow: boolean + } +} diff --git a/picx/src/store/modules/to-upload-image/index.ts b/picx/src/store/modules/to-upload-image/index.ts new file mode 100644 index 0000000..e7f6c1c --- /dev/null +++ b/picx/src/store/modules/to-upload-image/index.ts @@ -0,0 +1,92 @@ +import { ToUploadImageModel } from '@/common/model/upload.model' +import { Module } from 'vuex' +import ToUploadImageStateTypes from '@/store/modules/to-upload-image/types' +import RootStateTypes from '@/store/types' + +const toUploadImageModule: Module = { + state: { + curImgBase64Url: '', + curImgUuid: '', + list: [], + uploadedNumber: 0 + }, + + actions: { + // 要上传的图片列表 - 增加图片项 + TO_UPLOAD_IMAGE_LIST_ADD({ state }, item: ToUploadImageModel) { + state.list.unshift(item) + }, + + // 要上传的图片列表 - 设置当前图片的 Base64Url + TO_UPLOAD_IMAGE_SET_CURRENT({ state }, { uuid, base64Url }) { + state.curImgUuid = uuid + state.curImgBase64Url = base64Url + }, + + // 要上传的图片列表 - 上传完成的图片数量 +1 + TO_UPLOAD_IMAGE_UPLOADED({ state }) { + state.uploadedNumber += 1 + }, + + // 要上传的图片列表 - 删除图片项 + TO_UPLOAD_IMAGE_LIST_REMOVE({ state }, uuid: string) { + if (state.list.length > 0) { + const rmIndex = state.list.findIndex((v: ToUploadImageModel) => v.uuid === uuid) + if (rmIndex !== -1) { + state.list.splice(rmIndex, 1) + } + if (state.list.length === 0) { + state.curImgBase64Url = '' + state.uploadedNumber = 0 + } else if (state.curImgUuid === uuid) { + const cur = state.list[0] + state.curImgBase64Url = cur.imgData.base64Url + state.curImgUuid = cur.uuid + } + } + }, + + // 要上传的图片列表 - 上传失败时,在列表中移除已上传的图片 + TO_UPLOAD_IMAGE_LIST_FAIL({ state }) { + if (state.list.length > 0) { + const temp: ToUploadImageModel[] = state.list.filter( + (v: ToUploadImageModel) => v.uploadStatus.progress !== 100 + ) + if (temp.length > 0) { + state.list = temp + state.uploadedNumber = 0 + state.curImgBase64Url = temp[0].imgData.base64Url + } + } + }, + + // 要上传的图片列表 - 清空 Url + TO_UPLOAD_IMAGE_CLEAN_URL({ state }) { + state.curImgBase64Url = '' + }, + + // 要上传的图片列表 - 清空 List + TO_UPLOAD_IMAGE_CLEAN_LIST({ state }) { + state.list = [] + }, + + // 要上传的图片列表 - 清空上传完成数量 + TO_UPLOAD_IMAGE_CLEAN_UPLOADED_NUMBER({ state }) { + state.uploadedNumber = 0 + }, + + // 要上传的图片列表 - 退出登录 + TO_UPLOAD_IMAGE_LOGOUT({ state }) { + state.curImgBase64Url = '' + state.list = [] + state.uploadedNumber = 0 + } + }, + + getters: { + getToUploadImageList: (state: ToUploadImageStateTypes) => state.list, + getToUploadImage: (state: ToUploadImageStateTypes) => state + } +} + +export default toUploadImageModule diff --git a/picx/src/store/modules/to-upload-image/types.ts b/picx/src/store/modules/to-upload-image/types.ts new file mode 100644 index 0000000..0e7cf40 --- /dev/null +++ b/picx/src/store/modules/to-upload-image/types.ts @@ -0,0 +1,6 @@ +export default interface ToUploadImageStateTypes { + curImgBase64Url: string + curImgUuid: string + list: any[] + uploadedNumber: number +} diff --git a/picx/src/store/modules/upload-area-active/index.ts b/picx/src/store/modules/upload-area-active/index.ts new file mode 100644 index 0000000..0e22bbf --- /dev/null +++ b/picx/src/store/modules/upload-area-active/index.ts @@ -0,0 +1,25 @@ +import { Module } from 'vuex' +import UploadAreaActiveStateTypes from './types' +import RootStateTypes from '../../types' + +const uploadAreaActiveModule: Module = { + state: { + uploadAreaActive: false + }, + mutations: { + // 修改上传区域激活状态 + CHANGE_UPLOAD_AREA_ACTIVE(state: UploadAreaActiveStateTypes, isActive: boolean) { + state.uploadAreaActive = isActive + }, + + UPLOAD_AREA_ACTIVE_LOGOUT(state: UploadAreaActiveStateTypes) { + state.uploadAreaActive = false + } + }, + actions: {}, + getters: { + getUploadAreaActive: (state: UploadAreaActiveStateTypes) => state.uploadAreaActive + } +} + +export default uploadAreaActiveModule diff --git a/picx/src/store/modules/upload-area-active/types.ts b/picx/src/store/modules/upload-area-active/types.ts new file mode 100644 index 0000000..4e8f4f3 --- /dev/null +++ b/picx/src/store/modules/upload-area-active/types.ts @@ -0,0 +1,3 @@ +export default interface UploadAreaActiveStateTypes { + uploadAreaActive: boolean +} diff --git a/picx/src/store/modules/upload-settings/index.ts b/picx/src/store/modules/upload-settings/index.ts new file mode 100644 index 0000000..3455fad --- /dev/null +++ b/picx/src/store/modules/upload-settings/index.ts @@ -0,0 +1,24 @@ +import { Module } from 'vuex' +import UploadAreaActiveStateTypes from './types' +import RootStateTypes from '../../types' + +const uploadSettingsModule: Module = { + state: { + uploadSettings: { + isSetMaxSize: true, + imageMaxSize: 30 * 1024 + } + }, + mutations: { + UPLOAD_SETTINGS_LOGOUT(state: UploadAreaActiveStateTypes) { + state.uploadSettings.isSetMaxSize = true + state.uploadSettings.imageMaxSize = 50 * 1024 + } + }, + actions: {}, + getters: { + getUploadSettings: (state) => state.uploadSettings + } +} + +export default uploadSettingsModule diff --git a/picx/src/store/modules/upload-settings/types.ts b/picx/src/store/modules/upload-settings/types.ts new file mode 100644 index 0000000..5e1ff9d --- /dev/null +++ b/picx/src/store/modules/upload-settings/types.ts @@ -0,0 +1,6 @@ +export default interface UploadSettingsStateTypes { + uploadSettings: { + isSetMaxSize: boolean + imageMaxSize: number + } +} diff --git a/picx/src/store/modules/uploaded-image-list/index.ts b/picx/src/store/modules/uploaded-image-list/index.ts new file mode 100644 index 0000000..ea96a74 --- /dev/null +++ b/picx/src/store/modules/uploaded-image-list/index.ts @@ -0,0 +1,53 @@ +import { Module } from 'vuex' +import UploadedImageListStateTypes from '@/store/modules/uploaded-image-list/types' +import RootStateTypes from '@/store/types' +import { PICX_UPLOADED } from '@/common/model/storage.model' +import { UploadedImageModel } from '@/common/model/upload.model' + +const initUploadedImageList = (): UploadedImageModel[] => { + const imageList: string | null = sessionStorage.getItem(PICX_UPLOADED) + return imageList ? JSON.parse(imageList) : [] +} + +const uploadedImageListModule: Module = { + state: { + uploadedImageList: initUploadedImageList() + }, + + mutations: {}, + + actions: { + // 上传完成的图片列表 - 增加 + UPLOADED_LIST_ADD({ state, dispatch }, item: UploadedImageModel) { + state.uploadedImageList.unshift(item) + dispatch('UPLOADED_LIST_PERSIST') + }, + + // 上传完成的图片列表 - 删除 + UPLOADED_LIST_REMOVE({ state, dispatch }, uuid: string) { + if (state.uploadedImageList.length > 0) { + const rmIndex = state.uploadedImageList.findIndex((v) => v.uuid === uuid) + if (rmIndex !== -1) { + state.uploadedImageList.splice(rmIndex, 1) + dispatch('UPLOADED_LIST_PERSIST') + } + } + }, + + // 上传完成的图片列表 - 持久化 + UPLOADED_LIST_PERSIST({ state }) { + sessionStorage.setItem(PICX_UPLOADED, JSON.stringify(state.uploadedImageList)) + }, + + // 上传完成的图片列表 - 退出登录 + UPLOADED_LIST_LOGOUT({ state }) { + state.uploadedImageList = [] + } + }, + + getters: { + getUploadedImageList: (state: any) => state.uploadedImageList + } +} + +export default uploadedImageListModule diff --git a/picx/src/store/modules/uploaded-image-list/types.ts b/picx/src/store/modules/uploaded-image-list/types.ts new file mode 100644 index 0000000..6048dd1 --- /dev/null +++ b/picx/src/store/modules/uploaded-image-list/types.ts @@ -0,0 +1,5 @@ +import { UploadedImageModel } from '@/common/model/upload.model' + +export default interface UploadedImageListStateTypes { + uploadedImageList: UploadedImageModel[] +} diff --git a/picx/src/store/modules/user-config-info/index.ts b/picx/src/store/modules/user-config-info/index.ts new file mode 100644 index 0000000..6f73e99 --- /dev/null +++ b/picx/src/store/modules/user-config-info/index.ts @@ -0,0 +1,155 @@ +import { Module } from 'vuex' +import { + BranchModeEnum, + UserConfigInfoModel +} from '@/common/model/user-config-info.model' +import { PICX_CONFIG } from '@/common/model/storage.model' +import { deepAssignObject, cleanObject } from '@/utils/object-helper' +import UserConfigInfoStateTypes from '@/store/modules/user-config-info/types' +import RootStateTypes from '@/store/types' +import { DirModeEnum } from '@/common/model/dir.model' +import TimeHelper from '@/utils/time-helper' + +const initUserConfigInfo = (): UserConfigInfoModel => { + const initConfig: UserConfigInfoModel = { + token: '', + owner: '', + email: '', + name: '', + avatarUrl: '', + selectedRepos: '', + reposList: [], + branchMode: BranchModeEnum.reposBranch, + branchList: [], + selectedBranch: '', + selectedDir: '', + dirMode: DirModeEnum.reposDir, + dirList: [], + loggingStatus: false, + selectedDirList: [] + } + + const LSConfig: string | null = localStorage.getItem(PICX_CONFIG) + + if (LSConfig) { + // Assign: oldConfig -> initConfig + deepAssignObject(initConfig, JSON.parse(LSConfig)) + + if (initConfig.selectedBranch && !initConfig.branchList.length) { + initConfig.branchList = [ + { + value: initConfig.selectedBranch, + label: initConfig.selectedBranch + } + ] + } + + if (initConfig.dirMode === DirModeEnum.autoDir) { + initConfig.selectedDir = TimeHelper.getYyyyMmDd() + } + + return initConfig + } + + return initConfig +} + +const userConfigInfoUpdate = (state: UserConfigInfoStateTypes): void => { + const { selectedDir, selectedBranch, dirMode } = state.userConfigInfo + if (dirMode === 'newDir') { + const strList = selectedDir.split('') + let count = 0 + let newStr = '' + // eslint-disable-next-line no-plusplus + for (let i = 0; i < strList.length; i++) { + if (strList[i] === ' ' || strList[i] === '.' || strList[i] === '、') { + strList[i] = '-' + } + if (strList[i] === '/') { + count += 1 + } + if (count >= 3) { + break + } + newStr += strList[i] + } + state.userConfigInfo.selectedDir = newStr + } + state.userConfigInfo.selectedBranch = selectedBranch.replace(/\s+/g, '-') +} + +const userConfigInfoModule: Module = { + state: { + userConfigInfo: initUserConfigInfo() + }, + + actions: { + // 持久化状态获取 + USER_CONFIG_INFO_RESET({ state }) { + state.userConfigInfo = initUserConfigInfo() + }, + // 设置用户配置信息 + SET_USER_CONFIG_INFO( + { state, dispatch }, + configInfo: UserConfigInfoStateTypes, + needPersist: boolean = true + ) { + // eslint-disable-next-line no-restricted-syntax + for (const key in configInfo) { + // eslint-disable-next-line no-prototype-builtins + if (state.userConfigInfo.hasOwnProperty(key)) { + // @ts-ignore + state.userConfigInfo[key] = configInfo[key] + } else if (key === 'needPersist') { + // eslint-disable-next-line + needPersist = false + } + } + if (!needPersist) return + dispatch('USER_CONFIG_INFO_PERSIST') + }, + + // 用户配置信息 - 增加目录 + USER_CONFIG_INFO_ADD_DIR({ state, dispatch }, dir: string) { + if (!state.userConfigInfo.dirList.some((v: any) => v.value === dir)) { + state.userConfigInfo.dirList.push({ label: dir, value: dir }) + dispatch('USER_CONFIG_INFO_PERSIST') + } + }, + + // 用户配置信息 - 删除目录列表的某个目录 + USER_CONFIG_INFO_REMOVE_DIR({ state, dispatch }, dir: string) { + const { dirList } = state.userConfigInfo + if (dirList.some((v: any) => v.value === dir)) { + const rmIndex = dirList.findIndex((v: any) => v.value === dir) + dirList.splice(rmIndex, 1) + dispatch('USER_CONFIG_INFO_PERSIST') + } + }, + + // 持久化用户配置信息 + USER_CONFIG_INFO_PERSIST({ state }) { + userConfigInfoUpdate(state) + localStorage.setItem(PICX_CONFIG, JSON.stringify(state.userConfigInfo)) + }, + + // 修改 userConfigInfo 但无需持久化 (目前提供图床管理页面使用) + USER_CONFIG_INFO_NOT_PERSIST({ state }) { + userConfigInfoUpdate(state) + }, + + // 退出登录 + USER_CONFIG_INFO_LOGOUT({ state }) { + cleanObject(state.userConfigInfo) + } + }, + + getters: { + getUserLoggingStatus: (state: UserConfigInfoStateTypes): boolean => + state.userConfigInfo.loggingStatus, + getUserConfigInfo: (state: UserConfigInfoStateTypes): UserConfigInfoModel => + state.userConfigInfo + } +} + +export default userConfigInfoModule diff --git a/picx/src/store/modules/user-config-info/types.ts b/picx/src/store/modules/user-config-info/types.ts new file mode 100644 index 0000000..e54818e --- /dev/null +++ b/picx/src/store/modules/user-config-info/types.ts @@ -0,0 +1,5 @@ +import { UserConfigInfoModel } from '@/common/model/user-config-info.model' + +export default interface UserConfigInfoStateTypes { + userConfigInfo: UserConfigInfoModel +} diff --git a/picx/src/store/modules/user-settings/index.ts b/picx/src/store/modules/user-settings/index.ts new file mode 100644 index 0000000..6607590 --- /dev/null +++ b/picx/src/store/modules/user-settings/index.ts @@ -0,0 +1,67 @@ +import { Module } from 'vuex' +import { PICX_SETTINGS } from '@/common/model/storage.model' +import { deepAssignObject } from '@/utils/object-helper' +import UserConfigInfoStateTypes from '@/store/modules/user-config-info/types' +import RootStateTypes from '@/store/types' +import { CompressEncoderMap } from '@/utils/compress' +import { UserSettingsModel } from '@/common/model/user-settings.model' +import UserSettingsStateTypes from '@/store/modules/user-settings/types' +import { getLocalItem } from '@/utils/common-utils' +import ExternalLinkType from '@/common/model/external-link.model' + +const initSettings: UserSettingsModel = { + defaultHash: true, + defaultMarkdown: false, + defaultPrefix: false, + prefixName: '', + isCompress: true, + compressEncoder: CompressEncoderMap.webP, + themeMode: 'light', + autoLightThemeTime: ['08:00', '19:00'], + elementPlusSize: 'default', + externalLinkType: ExternalLinkType.staticaly +} + +const initUserSettings = (): UserSettingsModel => { + const LSSettings = getLocalItem(PICX_SETTINGS) + if (LSSettings) { + deepAssignObject(initSettings, LSSettings) + } + return initSettings +} + +const userSettingsModule: Module = { + state: { + userSettings: initUserSettings() + }, + + actions: { + // 设置 + SET_USER_SETTINGS({ state }, configInfo: UserConfigInfoStateTypes) { + // eslint-disable-next-line no-restricted-syntax + for (const key in configInfo) { + // eslint-disable-next-line no-prototype-builtins + if (state.userSettings.hasOwnProperty(key)) { + // @ts-ignore + state.userSettings[key] = configInfo[key] + } + } + }, + + // 持久化 + USER_SETTINGS_PERSIST({ state }) { + localStorage.setItem(PICX_SETTINGS, JSON.stringify(state.userSettings)) + }, + + // 退出登录 + USER_SETTINGS_LOGOUT({ state }) { + state.userSettings = initSettings + } + }, + + getters: { + getUserSettings: (state): UserSettingsModel => state.userSettings + } +} + +export default userSettingsModule diff --git a/picx/src/store/modules/user-settings/types.ts b/picx/src/store/modules/user-settings/types.ts new file mode 100644 index 0000000..596b418 --- /dev/null +++ b/picx/src/store/modules/user-settings/types.ts @@ -0,0 +1,5 @@ +import { UserSettingsModel } from '@/common/model/user-settings.model' + +export default interface UserSettingsStateTypes { + userSettings: UserSettingsModel +} diff --git a/picx/src/store/types.ts b/picx/src/store/types.ts new file mode 100644 index 0000000..dca2872 --- /dev/null +++ b/picx/src/store/types.ts @@ -0,0 +1,21 @@ +import DirImageListStateTypes from './modules/dir-image-list/types' +import ToUploadImageStateTypes from './modules/to-upload-image/types' +import UploadedImageListStateTypes from './modules/uploaded-image-list/types' +import UserConfigInfoStateTypes from './modules/user-config-info/types' +import ImageViewerStateTypes from './modules/image-viewer/types' +import UploadAreaActiveStateTypes from './modules/upload-area-active/types' +import UploadSettingsStateTypes from './modules/upload-settings/types' + +export default interface RootStateTypes { + rootName: string +} + +export interface AllStateTypes extends RootStateTypes { + dirImageListModule: DirImageListStateTypes + toUploadImageModule: ToUploadImageStateTypes + uploadedImageListModule: UploadedImageListStateTypes + userConfigInfoModule: UserConfigInfoStateTypes + imageViewerModule: ImageViewerStateTypes + uploadAreaActiveModule: UploadAreaActiveStateTypes + uploadSettingsModule: UploadSettingsStateTypes +} diff --git a/picx/src/style/base.styl b/picx/src/style/base.styl new file mode 100644 index 0000000..acce335 --- /dev/null +++ b/picx/src/style/base.styl @@ -0,0 +1,98 @@ +@import './theme.styl' +@import './variables.styl' + +$component-interval = 16rem +$box-border-radius = 6rem +$content-max-width = 888rem +$scrollbar-size = 8rem + +:root { + font-size 1px + + +picx-tablet() { + font-size 0.9px + } + + +picx-mobile() { + font-size 0.8px + } +} + +html, body { + position relative + padding 0 + margin 0 + width 100% + height 100% + color var(--default-text-color) +} + +a { + text-decoration none + font-size 1.5rem + color var(--default-text-color) +} + +a:link { + color var(--default-text-color) + text-decoration none +} + +ul, ol, li { + list-style none +} + +* { + &::-webkit-scrollbar { + height $scrollbar-size + width $scrollbar-size + } + + &::-webkit-scrollbar-thumb { + background var(--scrollbar-color) + border-radius $box-border-radius + } + + &::-webkit-scrollbar-track { + background transparent + } +} + + +.flex-center { + display flex + justify-content center + align-items center +} + + +.flex-start { + display flex + justify-content flex-start + align-items center +} + + +.page-container { + width 100% + height 100% + box-sizing border-box + padding 30rem + background var(--background-color) + border-top-left-radius $box-border-radius + border-top-right-radius $box-border-radius + overflow-y auto +} + +.clear { + &::after { + content '' + display block + clear both + visibility hidden + overflow hidden + height 0 + } +} + + diff --git a/picx/src/style/theme.styl b/picx/src/style/theme.styl new file mode 100644 index 0000000..5f3e64c --- /dev/null +++ b/picx/src/style/theme.styl @@ -0,0 +1,25 @@ +@import './variables.styl' + +:root { + root-color('light') +} + +@media (prefers-color-scheme light) { + :root { + root-color('light') + } +} + +@media (prefers-color-scheme dark) { + :root { + root-color('dark') + } +} + +.light { + root-color('light') +} + +.dark { + root-color('dark') +} diff --git a/picx/src/style/variables.styl b/picx/src/style/variables.styl new file mode 100644 index 0000000..f43d8b4 --- /dev/null +++ b/picx/src/style/variables.styl @@ -0,0 +1,110 @@ +// ======================================================================================== +// media query +// ======================================================================================== +$media-max-width-tablet = 800px; // media query max width (tablet) +$media-max-width-mobile = 500px; // media query max width (mobile) + +picx-tablet() + @media (max-width: $media-max-width-tablet) + { block } + +picx-mobile() + @media (max-width: $media-max-width-mobile) + { block } + + +// ======================================================================================== +// z-index +// ======================================================================================== +$z-index-1 = 1001; +$z-index-2 = 1002; +$z-index-3 = 1003; +$z-index-4 = 1004; +$z-index-5 = 1005; +$z-index-6 = 1006; +$z-index-7 = 1007; +$z-index-8 = 1008; +$z-index-9 = 1009; + + +// ======================================================================================== +// light mode color +// ======================================================================================== +$primary-color = #0066CC; +$background-color = #fff; +$second-background-color = darken($background-color, 5%); +$third-background-color = darken($background-color, 10%); +$default-text-color = #50505c; +$first-text-color = darken($default-text-color, 10%); +$second-text-color = darken($default-text-color, 5%); +$third-text-color = lighten($default-text-color, 30%); +$fourth-text-color = lighten($default-text-color, 90%); +$border-color = darken($background-color, 30%); +$selection-color = lighten($primary-color, 10%); +$shadow-color = rgba(0, 0, 0, 0.2); +$shadow-hover-color = rgba(0, 0, 0, 0.28); +$scrollbar-color = darken($background-color, 20%); +$scroll-bar-bg-color = darken($background-color, 30%); +$upload-area-focus-color = #0066CC; +$await-upload-color = #E6A23C; +$uploading-color = #409EFF; +$uploaded-color = #67C23A; +$markdown-icon-color = #808080; +$markdown-icon-active-color = darken($markdown-icon-color, 30%); + + + +// ======================================================================================== +// dark mode color +// ======================================================================================== +$dark-primary-color = #0066CC; +$dark-background-color = #2a2a2f; +$dark-second-background-color = darken($dark-background-color, 10%); +$dark-third-background-color = darken($dark-background-color, 15%); +$dark-default-text-color = #bebec6; +$dark-first-text-color = lighten($dark-default-text-color, 30%); +$dark-second-text-color = lighten($dark-default-text-color, 20%); +$dark-third-text-color = darken($dark-default-text-color, 20%); +$dark-fourth-text-color = darken($dark-default-text-color, 80%); +$dark-border-color = lighten($dark-background-color, 20%); +$dark-selection-color = $selection-color; +$dark-shadow-color = rgba(128, 128, 128, 0.2); +$dark-shadow-hover-color = rgba(128, 128, 128, 0.28); +$dark-scrollbar-color = darken($dark-background-color, 20%); +$dark-scroll-bar-bg-color = lighten($dark-background-color, 30%); +$dark-upload-area-focus-color = #1070d0; +$dark-await-upload-color = #c08327; +$dark-uploading-color = #287dd5; +$dark-uploaded-color = #55b626; +$dark-markdown-icon-color = #aaa; +$dark-markdown-icon-active-color = lighten($dark-markdown-icon-color, 30%); + + + +// ======================================================================== +// light/dark mode color +// ======================================================================== +root-color(mode) { + --background-color: mode == 'light' ? $background-color : $dark-background-color; + --second-background-color: mode == 'light' ? $second-background-color : $dark-second-background-color; + --third-background-color: mode == 'light' ? $third-background-color : $dark-third-background-color; + --primary-color: mode == 'light' ? $primary-color : $dark-primary-color; + --first-text-color: mode == 'light' ? $first-text-color : $dark-first-text-color; + --second-text-color: mode == 'light' ? $second-text-color : $dark-second-text-color; + --third-text-color: mode == 'light' ? $third-text-color : $dark-third-text-color; + --fourth-text-color: mode == 'light' ? $fourth-text-color : $dark-fourth-text-color; + --default-text-color: mode == 'light' ? $default-text-color : $dark-default-text-color; + --border-color: mode == 'light' ? $border-color : $dark-border-color; + --selection-color: mode == 'light' ? $selection-color : $dark-selection-color; + --shadow-color: mode == 'light' ? $shadow-color : $dark-shadow-color; + --shadow-hover-color: mode == 'light' ? $shadow-hover-color : $dark-shadow-hover-color; + --scrollbar-color: mode == 'light' ? $scrollbar-color : $dark-scrollbar-color; + --scroll-bar-bg-color: mode == 'light' ? $scroll-bar-bg-color : $dark-scroll-bar-bg-color; + --upload-area-focus-color : mode == 'light' ? $upload-area-focus-color : $dark-upload-area-focus-color; + --await-upload-color : mode == 'light' ? $await-upload-color : $dark-await-upload-color; + --uploading-color : mode == 'light' ? $uploading-color : $dark-uploading-color; + --uploaded-color : mode == 'light' ? $uploaded-color : $dark-uploaded-color; + --markdown-icon-color : mode == 'light' ? $markdown-icon-color : $dark-markdown-icon-color; + --markdown-icon-active-color : mode == 'light' ? $markdown-icon-active-color : $dark-markdown-icon-active-color; +} + diff --git a/picx/src/utils/axios.ts b/picx/src/utils/axios.ts new file mode 100644 index 0000000..37c0b38 --- /dev/null +++ b/picx/src/utils/axios.ts @@ -0,0 +1,51 @@ +import Axios from 'axios' +import { PICX_CONFIG } from '@/common/model/storage.model' + +const baseURL = 'https://api.github.com' + +const axios = Axios.create({ + baseURL, + timeout: 300000 // request timeout 请求超时 5m +}) + +axios.defaults.headers['Content-Type'] = 'application/json' + +// 发起请求之前的拦截器(前置拦截) +axios.interceptors.request.use( + (config) => { + const userConfig = localStorage.getItem(PICX_CONFIG) + + if (userConfig) { + const { token } = JSON.parse(userConfig) + if (token) { + config.headers.Authorization = `token ${token}` + } + } + + return config + }, + (error) => { + return Promise.reject(error) + } +) + +// 响应拦截器 +axios.interceptors.response.use( + (response) => { + return response + }, + (error) => { + if (error.response && error.response.data) { + const code = error.response.status + const msg = error.response.data.message + ElMessage.error(`Code: ${code}, Message: ${msg}`) + console.error(`[PicX Error]`, error.response) + } else { + ElMessage.error(`${error}`) + } + + return error.response + } +) + +export default axios diff --git a/picx/src/utils/common-utils.ts b/picx/src/utils/common-utils.ts new file mode 100644 index 0000000..d22db32 --- /dev/null +++ b/picx/src/utils/common-utils.ts @@ -0,0 +1,27 @@ +/** + * Get JavaScript basic data types + * @param data + * @returns {string} array | string | number ... + */ +export const getType = (data: string) => { + const type = Object.prototype.toString.call(data).split(' ')[1] + return type.substring(0, type.length - 1).toLowerCase() +} + +/** + * Gets a string(uuid) that is not repeated + * @returns uuid {string} + */ +export const getUuid = () => { + return Number(Math.random().toString().substr(2, 5) + Date.now()).toString(36) +} + +/** + * get localStorage value + * @param key + * @returns {*} + */ +export const getLocalItem = (key: string) => { + const temp = window.localStorage.getItem(key) + return temp ? JSON.parse(temp) : null +} diff --git a/picx/src/utils/compress.ts b/picx/src/utils/compress.ts new file mode 100644 index 0000000..e058abb --- /dev/null +++ b/picx/src/utils/compress.ts @@ -0,0 +1,26 @@ +import Compress from '@yireen/squoosh-browser' +import { + defaultPreprocessorState, + defaultProcessorState, + encoderMap, + EncoderState +} from '@yireen/squoosh-browser/dist/client/lazy-app/feature-meta' + +export enum CompressEncoderMap { + mozJPEG = 'mozJPEG', + avif = 'avif', + webP = 'webP' +} + +export const compress = async (file: File, encoder: CompressEncoderMap) => { + const compress = new Compress(file, { + encoderState: { + type: encoder, + options: encoderMap[encoder].meta.defaultOptions + } as EncoderState, + processorState: defaultProcessorState, + preprocessorState: defaultPreprocessorState + }) + + return compress.process() +} diff --git a/picx/src/utils/create-to-upload-image.ts b/picx/src/utils/create-to-upload-image.ts new file mode 100644 index 0000000..547c489 --- /dev/null +++ b/picx/src/utils/create-to-upload-image.ts @@ -0,0 +1,42 @@ +import { ToUploadImageModel } from '../common/model/upload.model' + +export default (): ToUploadImageModel => { + return { + uuid: '', + + uploadStatus: { + progress: 0, + uploading: false + }, + + imgData: { + base64Content: '', + base64Url: '' + }, + + fileInfo: { + size: 0, + lastModified: 0 + }, + + filename: { + name: '', + hash: '', + suffix: '', + prefixName: '', + now: '', + initName: '', + newName: 'xxx', + isHashRename: true, + isRename: false, + isPrefix: false + }, + + externalLink: { + github: '', + jsdelivr: '', + staticaly: '', + cloudflare: '' + } + } +} diff --git a/picx/src/utils/delete-image-card.ts b/picx/src/utils/delete-image-card.ts new file mode 100644 index 0000000..a90852a --- /dev/null +++ b/picx/src/utils/delete-image-card.ts @@ -0,0 +1,64 @@ +import { UploadedImageModel } from '../common/model/upload.model' +import { UserConfigInfoModel } from '../common/model/user-config-info.model' +import axios from '@/utils/axios' +import { deleteStatusEnum } from '../common/model/delete.model' +import { store } from '@/store' + +let deleteIndex = 0 + +export async function deleteSingleImage( + imageObj: UploadedImageModel, + userConfigInfo: UserConfigInfoModel +): Promise { + // eslint-disable-next-line no-param-reassign + imageObj.deleting = true + const { owner, selectedRepos } = userConfigInfo + return new Promise((resolve, reject) => { + axios + .delete(`/repos/${owner}/${selectedRepos}/contents/${imageObj.path}`, { + data: { + owner, + repo: selectedRepos, + path: imageObj.path, + message: 'delete picture via PicX(https://github.com/XPoet/picx)', + sha: imageObj.sha + } + }) + .then((res) => { + if (res && res.status === 200) { + // eslint-disable-next-line no-param-reassign + imageObj.deleting = false + store.dispatch('UPLOADED_LIST_REMOVE', imageObj.uuid) + store.dispatch('DIR_IMAGE_LIST_REMOVE', imageObj) + resolve(true) + } else { + // eslint-disable-next-line no-param-reassign + imageObj.deleting = false + resolve(false) + } + }) + .catch((err) => { + reject(err) + }) + }) +} + +export async function delelteBatchImage( + imgCardArr: Array, + userConfigInfo: UserConfigInfoModel +) { + if (deleteIndex >= imgCardArr.length) { + return deleteStatusEnum.deleted + } + if (await deleteSingleImage(imgCardArr[deleteIndex], userConfigInfo)) { + if (deleteIndex < imgCardArr.length) { + deleteIndex += 1 + if (await delelteBatchImage(imgCardArr, userConfigInfo)) { + deleteIndex = 0 + return deleteStatusEnum.allDeleted + } + } + return deleteStatusEnum.deleted + } + return deleteStatusEnum.deleteFail +} diff --git a/picx/src/utils/env.ts b/picx/src/utils/env.ts new file mode 100644 index 0000000..99919a5 --- /dev/null +++ b/picx/src/utils/env.ts @@ -0,0 +1,23 @@ +/* eslint-disable no-restricted-syntax */ +import { Recordable, ViteEnv } from '@/common/model/vite-config.model' + +// Read all environment variable configuration files to process.env +export default function wrapperEnv(envConf: Recordable): ViteEnv { + const ret: any = {} + + for (const envName of Object.keys(envConf)) { + let realName = envConf[envName].replace(/\\n/g, '\n') + if (realName === 'true') { + realName = true + } else if (realName === 'false') { + realName = false + } + + if (envName === 'VITE_PORT') { + realName = Number(realName) + } + ret[envName] = realName + process.env[envName] = realName + } + return ret +} diff --git a/picx/src/utils/external-link-handler.ts b/picx/src/utils/external-link-handler.ts new file mode 100644 index 0000000..e8f4a3e --- /dev/null +++ b/picx/src/utils/external-link-handler.ts @@ -0,0 +1,178 @@ +import ExternalLinkType from '@/common/model/external-link.model' +import { UserConfigInfoModel } from '@/common/model/user-config-info.model' +import { getFilename } from '@/utils/file-handle-helper' +import { UploadedImageModel } from '@/common/model/upload.model' + +/** + * 创建承载图片外链文本的 DOM 元素 + */ +export const createExternalLinkDom = () => { + let externalLinkDom: any = document.querySelector('.temp-external-link-txt') + if (!externalLinkDom) { + externalLinkDom = document.createElement('textarea') + externalLinkDom.setAttribute('class', 'temp-external-link-txt') + externalLinkDom.style.position = 'absolute' + externalLinkDom.style.top = '-99999rem' + externalLinkDom.style.left = '-99999rem' + document.body.appendChild(externalLinkDom) + } + return externalLinkDom +} + +/** + * 生成图片外链 + * @param type + * @param content + * @param config + */ +export const generateExternalLink = ( + type: ExternalLinkType, + content: any, + config: UserConfigInfoModel +): string => { + const staticalyLink: string = `https://cdn.staticaly.com/gh/${config.owner}/${config.selectedRepos}@${config.selectedBranch}/${content.path}` + const cloudflareLink: string = `https://git.poker/${config.owner}/${config.selectedRepos}/blob/${config.selectedBranch}/${content.path}?raw=true` + const jsdelivrLink: string = `https://cdn.jsdelivr.net/gh/${config.owner}/${config.selectedRepos}@${config.selectedBranch}/${content.path}` + const githubLink: string = decodeURI(content.download_url) + + // eslint-disable-next-line default-case + switch (type) { + case ExternalLinkType.staticaly: + return staticalyLink + + case ExternalLinkType.cloudflare: + return cloudflareLink + + case ExternalLinkType.jsdelivr: + return jsdelivrLink + + case ExternalLinkType.github: + return githubLink + + default: + return githubLink + } +} + +/** + * 图片外链转换为 Markdown 格式 + * @param name 图片名 + * @param url 图片外链 + */ +export const formatMarkdown = (name: string, url: string): string => { + return `![${getFilename(name)}](${url})` +} + +/** + * 复制图片外链 + * @param img 图片对象 + * @param type CDN 类型 + */ +export const copyExternalLink = (img: UploadedImageModel, type: ExternalLinkType) => { + let externalLink = '' + let successInfo = '' + const { name, is_transform_md: isMD } = img + + switch (type) { + case ExternalLinkType.jsdelivr: + if (isMD) { + externalLink = formatMarkdown(name, img.jsdelivr_cdn_url) + successInfo = 'Markdown 格式的 jsDelivr CDN' + } else { + externalLink = img.jsdelivr_cdn_url + successInfo = 'jsDelivr CDN' + } + break + + case ExternalLinkType.staticaly: + if (isMD) { + externalLink = formatMarkdown(name, img.staticaly_cdn_url) + successInfo = 'Markdown 格式的 Staticaly CDN' + } else { + externalLink = img.staticaly_cdn_url + successInfo = 'Staticaly CDN' + } + break + + case ExternalLinkType.cloudflare: + if (isMD) { + externalLink = formatMarkdown(name, img.cloudflare_cdn_url) + successInfo = 'Markdown 格式的 Cloudflare CDN' + } else { + externalLink = img.cloudflare_cdn_url + successInfo = 'Cloudflare CDN' + } + break + + default: + if (isMD) { + externalLink = formatMarkdown(name, img.github_url) + successInfo = 'Markdown 格式的 GitHub' + } else { + externalLink = img.github_url + successInfo = 'GitHub' + } + } + + const externalLinkDom: any = createExternalLinkDom() + + externalLinkDom.value = externalLink + externalLinkDom.select() + document.execCommand('copy') + ElMessage.success(`${successInfo} 外链复制成功!`) +} + +/** + * 批量复制图片外链 + * @param imgCardList 图片列表 + * @param type 当前选择的外链类型 + */ +export const batchCopyExternalLink = ( + imgCardList: Array, + type: ExternalLinkType +) => { + let externalLink = '' + const externalLinkDom: any = createExternalLinkDom() + externalLinkDom.value = '' + if (imgCardList?.length > 0) { + imgCardList.forEach((item: UploadedImageModel, index) => { + const isMD = item.is_transform_md + switch (type) { + case ExternalLinkType.jsdelivr: + externalLink = isMD + ? formatMarkdown(item.name, item.jsdelivr_cdn_url) + : item.jsdelivr_cdn_url + break + + case ExternalLinkType.staticaly: + externalLink = isMD + ? formatMarkdown(item.name, item.staticaly_cdn_url) + : item.staticaly_cdn_url + break + + case ExternalLinkType.cloudflare: + externalLink = isMD + ? formatMarkdown(item.name, item.cloudflare_cdn_url) + : item.cloudflare_cdn_url + break + + default: + externalLink = isMD + ? formatMarkdown(item.name, item.github_url) + : item.github_url + } + + if (index < imgCardList.length - 1) { + // eslint-disable-next-line prefer-template + externalLinkDom.value += externalLink + '\n' + } else { + externalLinkDom.value += externalLink + } + }) + externalLinkDom.select() + document.execCommand('copy') + ElMessage.success(`批量复制图片链接成功`) + } else { + console.warn('请先选择图片') + } +} diff --git a/picx/src/utils/file-handle-helper.ts b/picx/src/utils/file-handle-helper.ts new file mode 100644 index 0000000..27baf52 --- /dev/null +++ b/picx/src/utils/file-handle-helper.ts @@ -0,0 +1,50 @@ +import { getUuid } from './common-utils' + +/** + * get filename + * @param filename + */ +export const getFilename = (filename: string) => { + const splitIndex = filename.indexOf('.') + return filename.substr(0, splitIndex).trim().replace(/\s+/g, '-') +} + +/** + * get filename suffix + * @param filename + */ +export const getFileSuffix = (filename: string) => { + const splitIndex = filename.lastIndexOf('.') + return filename.substr(splitIndex + 1, filename.length) +} + +export const isImage = (suffix: string): boolean => { + return /(png|jpg|gif|jpeg|webp|avif|svg\+xml|image\/x-icon)$/.test(suffix) +} + +/** + * get file size (KB) + * @param size + */ +export const getFileSize = (size: number) => { + return Number((size / 1024).toFixed(2)) +} + +/** + * filename handle + * @param filename + */ +export const filenameHandle = (filename: string | undefined) => { + if (filename) { + return { + name: getFilename(filename), + hash: getUuid(), + suffix: getFileSuffix(filename) + } + } + return { + name: '', + hash: '', + suffix: '' + } +} diff --git a/picx/src/utils/image-helper.ts b/picx/src/utils/image-helper.ts new file mode 100644 index 0000000..1ee88b2 --- /dev/null +++ b/picx/src/utils/image-helper.ts @@ -0,0 +1,42 @@ +import { computed } from 'vue' +import { UploadedImageModel } from '@/common/model/upload.model' +import { getUuid } from '@/utils/common-utils' +import { generateExternalLink } from '@/utils/external-link-handler' +import ExternalLinkType from '@/common/model/external-link.model' +import { store } from '@/store' + +const userConfigInfo = computed(() => store.getters.getUserConfigInfo).value + +export default function structureImageObject( + item: any, + selectedDir: string +): UploadedImageModel { + return { + type: 'image', + uuid: getUuid(), + dir: selectedDir, + name: item.name, + path: item.path, + sha: item.sha, + deleting: false, + is_transform_md: false, + size: item.size, + checked: false, + github_url: generateExternalLink(ExternalLinkType.github, item, userConfigInfo), + jsdelivr_cdn_url: generateExternalLink( + ExternalLinkType.jsdelivr, + item, + userConfigInfo + ), + staticaly_cdn_url: generateExternalLink( + ExternalLinkType.staticaly, + item, + userConfigInfo + ), + cloudflare_cdn_url: generateExternalLink( + ExternalLinkType.cloudflare, + item, + userConfigInfo + ) + } +} diff --git a/picx/src/utils/object-helper.ts b/picx/src/utils/object-helper.ts new file mode 100644 index 0000000..ea91287 --- /dev/null +++ b/picx/src/utils/object-helper.ts @@ -0,0 +1,59 @@ +import { getType } from './common-utils' + +/** + * 根据 object 每个 key 上值的数据类型,赋对应的初始值 + * @param object + */ +export const cleanObject = (object: any) => { + // eslint-disable-next-line guard-for-in,no-restricted-syntax + for (const key in object) { + // eslint-disable-next-line default-case + switch (getType(object[key])) { + case 'object': + cleanObject(object[key]) + break + + case 'string': + // eslint-disable-next-line no-param-reassign + object[key] = '' + break + + case 'array': + // eslint-disable-next-line no-param-reassign + object[key] = [] + break + + case 'number': + // eslint-disable-next-line no-param-reassign + object[key] = 0 + break + + case 'boolean': + // eslint-disable-next-line no-param-reassign + object[key] = false + break + } + } +} + +/** + * 将 obj2 对象的值深度赋值给 obj1 对象 + * @param obj1{Object} + * @param obj2{Object} + */ +export const deepAssignObject = (obj1: object, obj2: object): any => { + // eslint-disable-next-line no-restricted-syntax + for (const key in obj2) { + // @ts-ignore + if (getType(obj2[key]) !== 'object') { + if (obj1) { + // @ts-ignore + // eslint-disable-next-line no-param-reassign + obj1[key] = obj2[key] + } + } else { + // @ts-ignore + deepAssignObject(obj1[key], obj2[key]) + } + } +} diff --git a/picx/src/utils/paste.ts b/picx/src/utils/paste.ts new file mode 100644 index 0000000..a9b1678 --- /dev/null +++ b/picx/src/utils/paste.ts @@ -0,0 +1,28 @@ +import selectedFileHandle from './selected-file-handle' + +const onPaste = (e: any, maxsize: number): Promise | null => { + if (!(e.clipboardData && e.clipboardData.items)) { + return null + } + + // eslint-disable-next-line consistent-return + return new Promise((resolve) => { + // eslint-disable-next-line no-plusplus + for (let i = 0, len = e.clipboardData.items.length; i < len; i++) { + const item = e.clipboardData.items[i] + if (item.kind === 'file') { + const pasteFile = item.getAsFile() + + selectedFileHandle(pasteFile, maxsize)?.then((result) => { + if (!result) { + return + } + const { base64, originalFile, compressFile } = result + resolve({ base64, originalFile, compressFile }) + }) + } + } + }) +} + +export default onPaste diff --git a/picx/src/utils/register-sw.ts b/picx/src/utils/register-sw.ts new file mode 100644 index 0000000..c569ffb --- /dev/null +++ b/picx/src/utils/register-sw.ts @@ -0,0 +1,3 @@ +import { registerSW } from 'virtual:pwa-register' + +registerSW() diff --git a/picx/src/utils/rename-image.ts b/picx/src/utils/rename-image.ts new file mode 100644 index 0000000..ce25394 --- /dev/null +++ b/picx/src/utils/rename-image.ts @@ -0,0 +1,56 @@ +import { computed } from 'vue' +import { store } from '@/store' +import createToUploadImageObject from '@/utils/create-to-upload-image' +import { filenameHandle } from './file-handle-helper' + +/** + * 根据图片链接获取图片 base64 编码 + * @param url 图片路径 + * @param ext 图片格式 + */ +export function getBase64ByImageUrl(url: string, ext: string): Promise { + const canvas = document.createElement('canvas') + const ctx = canvas.getContext('2d') + const img = new Image() + img.crossOrigin = 'Anonymous' + img.src = url + return new Promise((resolve) => { + img.onload = () => { + const { width } = img + const { height } = img + canvas.width = width // 指定画板的高度,自定义 + canvas.height = height // 指定画板的宽度,自定义 + ctx?.drawImage(img, 0, 0, width, height) // 参数可自定义 + const dataURL: string = canvas.toDataURL(`image/${ext}`) + resolve(dataURL) + } + }) +} + +// 获取图片对象 +export function getImage(base64Data: string, file: any) { + const userSettings = computed(() => store.getters.getUserSettings).value + const curImg = createToUploadImageObject() + + curImg.imgData.base64Url = base64Data + // eslint-disable-next-line prefer-destructuring + curImg.imgData.base64Content = base64Data.split(',')[1] + + const { name, hash, suffix } = filenameHandle(file.name) + curImg.uuid = hash + + curImg.fileInfo.size = file.size + curImg.fileInfo.originSize = file.size + curImg.fileInfo.lastModified = file.lastModified + + curImg.filename.name = name + curImg.filename.hash = hash + curImg.filename.suffix = suffix + curImg.filename.now = userSettings.defaultHash + ? `${name}.${hash}.${suffix}` + : `${name}.${suffix}` + curImg.filename.initName = name + curImg.filename.isHashRename = userSettings.defaultHash + + return curImg +} diff --git a/picx/src/utils/selected-file-handle.ts b/picx/src/utils/selected-file-handle.ts new file mode 100644 index 0000000..7cb1f91 --- /dev/null +++ b/picx/src/utils/selected-file-handle.ts @@ -0,0 +1,67 @@ +import { store } from '@/store' +import { compress } from './compress' +import { getFileSize, isImage } from './file-handle-helper' + +export type handleResult = { base64: string; originalFile: File; compressFile?: File } + +const selectedFileHandle = async ( + file: File, + maxsize: number +): Promise => { + if (!file) { + return null + } + + if (!isImage(file.type)) { + ElMessage.error('该文件格式不支持!') + return null + } + let compressFile: NonNullable + const { isCompress, compressEncoder } = store.getters.getUserSettings + const isGif = file.type === 'image/gif' + if (!isGif && isCompress) { + const loadingInstance = ElLoading.service({ + target: '.upload-area', + text: '正在压缩图片' + }) + compressFile = await compress(file, compressEncoder) + loadingInstance.close() + } + + return new Promise((resolve) => { + const reader = new FileReader() + reader.readAsDataURL(!isGif && isCompress ? compressFile : file) + reader.onload = (e: ProgressEvent) => { + const base64: any = e.target?.result + const curImgSize = getFileSize(base64.length) + + if (curImgSize >= maxsize) { + // 给出提示,引导用户自行去压缩图片 + ElMessageBox.confirm( + `当前图片 ${(curImgSize / 1024).toFixed( + 2 + )} M,CDN 只能加速小于 50 MB 的图片,建议使用第三方工具 TinyPNG 压缩`, + '图片过大,禁止上传', + { + confirmButtonText: '前往 TinyPNG', + cancelButtonText: '放弃上传' + } + ) + .then(() => { + window.open('https://tinypng.com/') + }) + .catch(() => { + console.log('放弃上传') + }) + } else { + resolve({ + base64, + originalFile: file, + compressFile: !isGif && isCompress ? compressFile : file + }) + } + } + }) +} + +export default selectedFileHandle diff --git a/picx/src/utils/set-theme-mode.ts b/picx/src/utils/set-theme-mode.ts new file mode 100644 index 0000000..4fcb0e3 --- /dev/null +++ b/picx/src/utils/set-theme-mode.ts @@ -0,0 +1,51 @@ +import { watch, nextTick } from 'vue' +import { useStore } from '@/store' +import { UserSettingsModel } from '@/common/model/user-settings.model' + +const setThemeMode = () => { + const store = useStore() + + const setBodyClassName = async (theme: 'dark' | 'light') => { + await nextTick(() => { + const body = document.getElementsByTagName('html')[0] + if (theme === 'dark') { + body.classList.remove('light') + body.classList.add('dark') + } else { + body.classList.remove('dark') + body.classList.add('light') + } + }) + } + + const autoThemeModeTimeHandle = (autoLightThemeTime: string[]) => { + const getTimestamp = (i: number) => { + const D = new Date() + const yyyy = D.getFullYear() + const mm = D.getMonth() + 1 + const dd = D.getDate() + return new Date(`${yyyy}/${mm}/${dd} ${autoLightThemeTime[i]}:00`).getTime() + } + const now = Date.now() + return getTimestamp(0) <= now && now <= getTimestamp(1) + } + + const setThemeByConfigFn = (settings: UserSettingsModel) => { + const { themeMode, autoLightThemeTime } = settings + if (themeMode === 'auto') { + setBodyClassName(autoThemeModeTimeHandle(autoLightThemeTime) ? 'light' : 'dark') + } else { + setBodyClassName(themeMode) + } + } + + watch( + (): UserSettingsModel => store.getters.getUserSettings, + (newValue) => { + setThemeByConfigFn(newValue) + }, + { deep: true, immediate: true } + ) +} + +export default setThemeMode diff --git a/picx/src/utils/time-helper.ts b/picx/src/utils/time-helper.ts new file mode 100644 index 0000000..ba84dca --- /dev/null +++ b/picx/src/utils/time-helper.ts @@ -0,0 +1,26 @@ +export default class TimeHelper { + private static zerofill(n: number) { + return n < 10 ? `0${n}` : n + } + + static getYyyyMmDd(now: number = Date.now()) { + const date: Date = new Date(now) + const yyyy = date.getFullYear() + const MM = date.getMonth() + 1 + const DD = date.getDate() + return `${yyyy}${this.zerofill(MM)}${this.zerofill(DD)}` + } + + static formatTimestamp(now: number = Date.now()) { + const date: Date = new Date(now) + const YYYY = date.getFullYear() + const MM = date.getMonth() + 1 + const DD = date.getDate() + const hh = date.getHours() + const mm = date.getMinutes() + const ss = date.getSeconds() + return `${YYYY}-${this.zerofill(MM)}-${this.zerofill(DD)} ${this.zerofill( + hh + )}:${this.zerofill(mm)}:${this.zerofill(ss)}` + } +} diff --git a/picx/src/utils/upload-helper.ts b/picx/src/utils/upload-helper.ts new file mode 100644 index 0000000..982b85b --- /dev/null +++ b/picx/src/utils/upload-helper.ts @@ -0,0 +1,134 @@ +import { UserConfigInfoModel } from '@/common/model/user-config-info.model' +import { ToUploadImageModel, UploadedImageModel } from '@/common/model/upload.model' +import axios from '@/utils/axios' +import { store } from '@/store' +import { generateExternalLink } from '@/utils/external-link-handler' +import ExternalLinkType from '@/common/model/external-link.model' + +export const uploadUrlHandle = ( + config: UserConfigInfoModel, + filename: string +): string => { + let path = '' + if (config.selectedDir !== '/') { + path = `${config.selectedDir}/` + } + return `/repos/${config.owner}/${config.selectedRepos}/contents/${path}${filename}` +} + +export function uploadImage_single( + userConfigInfo: UserConfigInfoModel, + img: ToUploadImageModel +): Promise { + const { selectedBranch, email, owner } = userConfigInfo + // eslint-disable-next-line no-param-reassign + img.uploadStatus.uploading = true + + const data: any = { + message: 'Upload picture via PicX(https://github.com/XPoet/picx)', + branch: selectedBranch, + content: img.imgData.base64Content + } + + if (email) { + data.committer = { + name: owner, + email + } + } + + return new Promise((resolve, reject) => { + axios + .put(uploadUrlHandle(userConfigInfo, img.filename.now), data) + .then((res) => { + if (res && res.status === 201) { + // eslint-disable-next-line no-use-before-define + uploadedHandle(res, img, userConfigInfo) + store.dispatch('TO_UPLOAD_IMAGE_UPLOADED', img.uuid) + resolve(true) + } else { + // eslint-disable-next-line no-param-reassign + img.uploadStatus.uploading = false + resolve(false) + } + }) + .catch((error) => { + reject(error) + }) + }) +} + +function uploadedHandle( + res: any, + img: ToUploadImageModel, + userConfigInfo: UserConfigInfoModel +) { + const userSettings = store.getters.getUserSettings + + // 上传状态处理 + // eslint-disable-next-line no-param-reassign + img.uploadStatus.progress = 100 + // eslint-disable-next-line no-param-reassign + img.uploadStatus.uploading = false + + // 生成 GitHub 外链 + // eslint-disable-next-line no-param-reassign + img.externalLink.github = generateExternalLink( + ExternalLinkType.github, + res.data.content, + userConfigInfo + ) + + // 生成 jsDelivr CDN 外链 + // eslint-disable-next-line no-param-reassign + img.externalLink.jsdelivr = generateExternalLink( + ExternalLinkType.jsdelivr, + res.data.content, + userConfigInfo + ) + + // 生成 Staticaly CDN 外链 + // eslint-disable-next-line no-param-reassign + img.externalLink.staticaly = generateExternalLink( + ExternalLinkType.staticaly, + res.data.content, + userConfigInfo + ) + + // 生成 Cloudflare CDN 外链 + // eslint-disable-next-line no-param-reassign + img.externalLink.cloudflare = generateExternalLink( + ExternalLinkType.cloudflare, + res.data.content, + userConfigInfo + ) + + const item: UploadedImageModel = { + checked: false, + type: 'image', + uuid: img.uuid, + dir: userConfigInfo.selectedDir, + name: res.data.content.name, + path: res.data.content.path, + sha: res.data.content.sha, + github_url: img.externalLink.github, + jsdelivr_cdn_url: img.externalLink.jsdelivr, + staticaly_cdn_url: img.externalLink.staticaly, + cloudflare_cdn_url: img.externalLink.cloudflare, + is_transform_md: userSettings.defaultMarkdown, + deleting: false, + size: img.fileInfo.size + } + + // eslint-disable-next-line no-param-reassign + img.uploadedImg = item + + // uploadedList 增加图片 + store.dispatch('UPLOADED_LIST_ADD', item) + + // dirImageList 增加目录 + store.dispatch('DIR_IMAGE_LIST_ADD_DIR', userConfigInfo.selectedDir) + + // dirImageList 增加图片 + store.dispatch('DIR_IMAGE_LIST_ADD_IMAGE', item) +} diff --git a/picx/src/views/about/about.styl b/picx/src/views/about/about.styl new file mode 100644 index 0000000..99ae5ac --- /dev/null +++ b/picx/src/views/about/about.styl @@ -0,0 +1,25 @@ +.feedback-page-container { + + .help-info-item { + font-size: 16rem; + padding: 6rem; + display: flex; + align-items: center; + margin-bottom: 10rem; + + &:last-child { + margin-bottom: 0; + } + + + } + + .description { + font-weight: bold; + line-height: 28rem; + } + + .red-text { + color: #de1a1a; + } +} diff --git a/picx/src/views/about/about.vue b/picx/src/views/about/about.vue new file mode 100644 index 0000000..6b26e69 --- /dev/null +++ b/picx/src/views/about/about.vue @@ -0,0 +1,77 @@ + + + + + diff --git a/picx/src/views/config/config.styl b/picx/src/views/config/config.styl new file mode 100644 index 0000000..095223b --- /dev/null +++ b/picx/src/views/config/config.styl @@ -0,0 +1,13 @@ +.config-page-container { + .operation { + text-align right + + .el-button { + margin-left 20rem + + &:first-child { + margin-left 0 + } + } + } +} diff --git a/picx/src/views/config/config.vue b/picx/src/views/config/config.vue new file mode 100644 index 0000000..8e9b540 --- /dev/null +++ b/picx/src/views/config/config.vue @@ -0,0 +1,498 @@ + + + + + diff --git a/picx/src/views/management/management.styl b/picx/src/views/management/management.styl new file mode 100644 index 0000000..2d43c51 --- /dev/null +++ b/picx/src/views/management/management.styl @@ -0,0 +1,70 @@ +@import "../../style/base.styl" + +$infoBarHeight = 50rem + +.management-page-container { + padding-bottom 0 !important + + .content-container { + position relative + width 100% + height 100% + padding-top $infoBarHeight + box-sizing border-box + + .top { + position absolute + top 0 + left 0 + width 100% + height $infoBarHeight + box-sizing border-box + display flex + align-items center + justify-content space-between + font-size 14rem + padding-bottom 20rem + + .right { + + .btn-icon { + cursor pointer + font-size 22rem + margin-left 10rem + } + + } + } + + + .bottom { + position relative + width 100% + height 100% + box-sizing border-box + border 1rem solid var(--border-color) + + .image-list { + width 100% + //height 100% + //max-height calc(100% - 60rem) + margin 0 + padding 2rem + list-style none + overflow-y auto + box-sizing border-box + + li.image-item { + float left + box-sizing border-box + padding 10rem + + &:last-child { + margin-right 0 + } + } + } + + } + } +} diff --git a/picx/src/views/management/management.util.ts b/picx/src/views/management/management.util.ts new file mode 100644 index 0000000..4ba691a --- /dev/null +++ b/picx/src/views/management/management.util.ts @@ -0,0 +1,57 @@ +import { Store } from 'vuex' +import { DirObject } from '@/store/modules/dir-image-list/types' + +function getContent(targetContent: any, dirList: string[], n: number): any { + if (targetContent) { + if (dirList.length === n) { + return targetContent + } + return getContent( + targetContent.childrenDirs?.find((v: any) => v.dir === dirList[n]), + dirList, + // eslint-disable-next-line no-param-reassign,no-plusplus + ++n + ) + } + return null +} + +/** + * 获取当前目录下所有内容(子目录和图片) + * @param dirPath + * @param dirObj + */ +export const getDirContent = (dirPath: string, dirObj: DirObject) => { + if (dirPath === '/') { + return dirObj + } + const dirList: string[] = dirPath.split('/') + return getContent(dirObj, dirList, 0) +} + +/** + * 过滤当前目录的内容(子目录或图片) + * @param dirPath + * @param content + * @param type + */ +export const filterDirContent = (dirPath: string, content: any, type: string): any => { + if (type === 'dir') { + return content.childrenDirs?.filter((x: any) => x.type === 'dir') + } + + if (type === 'image') { + return content.imageList.filter((x: any) => x.type === 'image') + } + + return [] +} + +export const dirModeHandle = (dir: string, store: Store) => { + if (dir === '/') { + store.dispatch('SET_USER_CONFIG_INFO', { + dirMode: 'rootDir', + needPersist: false + }) + } +} diff --git a/picx/src/views/management/management.vue b/picx/src/views/management/management.vue new file mode 100644 index 0000000..4edcdb5 --- /dev/null +++ b/picx/src/views/management/management.vue @@ -0,0 +1,201 @@ + + + + + diff --git a/picx/src/views/settings/settings.styl b/picx/src/views/settings/settings.styl new file mode 100644 index 0000000..43fe857 --- /dev/null +++ b/picx/src/views/settings/settings.styl @@ -0,0 +1,44 @@ +.setting-title { + font-size 16rem + font-weight bold + margin 40rem 0 20rem 0 + + &:first-child { + margin-top 0 + } +} + +.setting-list { + padding 0 + margin 0 + + .setting-item { + margin-bottom 10rem + + &.last-child { + margin-bottom 0 + } + + .prefix-input { + width calc(100% - 50rem) + margin-left 50rem + margin-top 15rem + } + + .img-encoder-title { + margin-bottom 12rem + } + + :deep() .el-radio-group { + + display inline-block + + .el-radio { + display block + } + + } + + } + +} diff --git a/picx/src/views/settings/settings.vue b/picx/src/views/settings/settings.vue new file mode 100644 index 0000000..69834c3 --- /dev/null +++ b/picx/src/views/settings/settings.vue @@ -0,0 +1,149 @@ + + + + + diff --git a/picx/src/views/tutorials/tutorials.styl b/picx/src/views/tutorials/tutorials.styl new file mode 100644 index 0000000..3ddce1a --- /dev/null +++ b/picx/src/views/tutorials/tutorials.styl @@ -0,0 +1,15 @@ +.tutorials-page-container { + + .step-content { + padding-top 30rem + display flex + justify-content center + text-align center + } + + .btn-next-prev { + text-align center + padding-top 30rem + } + +} diff --git a/picx/src/views/tutorials/tutorials.vue b/picx/src/views/tutorials/tutorials.vue new file mode 100644 index 0000000..72ee2d3 --- /dev/null +++ b/picx/src/views/tutorials/tutorials.vue @@ -0,0 +1,64 @@ + + + + + diff --git a/picx/src/views/upload/upload.styl b/picx/src/views/upload/upload.styl new file mode 100644 index 0000000..cbc313f --- /dev/null +++ b/picx/src/views/upload/upload.styl @@ -0,0 +1,135 @@ +@import "../../style/base.styl" + +.upload-page-container { + width 100% + height 100% + display flex + justify-content space-between + + .upload-page-left { + height 100% + box-sizing border-box + margin-right $component-interval + flex-shrink 0 + + .uploaded-item { + margin-bottom 20rem + + &:last-child { + margin-bottom 0 + } + } + } + + + .upload-page-right { + width 100% + height 100% + box-sizing border-box + overflow-y auto + + .row-item { + width 100% + display flex + justify-content center + margin-bottom 16rem + box-sizing border-box + + &:last-child { + margin-bottom 0 + } + + .content-box { + width 100% + max-width $content-max-width + margin 0 auto + box-sizing border-box + } + + } + + + .upload-status { + position relative + width 100% + padding 10rem + background var(--second-background-color) + color #666 + font-size 12rem + box-sizing border-box + + .info-item { + margin-top 4rem + } + + .file-status { + display flex + justify-content space-between + align-items center + } + + .upload-tips { + + display flex + align-items center + + i { + margin-left 2rem + font-size 20rem + } + } + + .wait-upload { + color var(--await-upload-color) + } + + .uploading { + color var(--uploading-color) + } + + .uploaded { + color var(--uploaded-color) + } + + } + + + .external-link { + width 100% + + .external-link-input { + margin-bottom 10rem + + &:last-child { + margin-bottom 0 + } + + .el-input-group__append { + width 100rem + text-align-last justify + } + } + } + + + .upload-tools { + width 100% + + .repos-dir-info { + margin-bottom 20rem + font-size 12rem + + .repos-dir-info-item { + margin-right 10rem + + &:last-child { + margin-right 0 + } + } + + + } + } + } + +} diff --git a/picx/src/views/upload/upload.vue b/picx/src/views/upload/upload.vue new file mode 100644 index 0000000..5ce57f5 --- /dev/null +++ b/picx/src/views/upload/upload.vue @@ -0,0 +1,151 @@ + + + + +