mirror of
https://github.com/Mujinniao/Anonima.git
synced 2025-12-17 23:44:40 +08:00
Add files via upload
This commit is contained in:
74
picx/src/App.vue
Normal file
74
picx/src/App.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<el-config-provider :size="size" :z-index="zIndex" :locale="locale">
|
||||
<main-container />
|
||||
</el-config-provider>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, onMounted, reactive, toRefs } from 'vue'
|
||||
import { ElConfigProvider } from 'element-plus'
|
||||
import zhCn from 'element-plus/lib/locale/lang/zh-cn'
|
||||
import mainContainer from '@/components/main-container/main-container.vue'
|
||||
import setTheme from '@/utils/set-theme-mode'
|
||||
import { useStore } from '@/store'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'App',
|
||||
components: {
|
||||
ElConfigProvider,
|
||||
mainContainer
|
||||
},
|
||||
setup() {
|
||||
const store = useStore()
|
||||
|
||||
const data = reactive({
|
||||
zIndex: 3000,
|
||||
size: 'small', // large | default | small
|
||||
locale: zhCn
|
||||
})
|
||||
|
||||
const elementPlusSizeHandle = (width: number) => {
|
||||
if (width <= 600) {
|
||||
store.dispatch('SET_USER_SETTINGS', {
|
||||
elementPlusSize: 'small'
|
||||
})
|
||||
data.size = 'small'
|
||||
} else if (width <= 800) {
|
||||
store.dispatch('SET_USER_SETTINGS', {
|
||||
elementPlusSize: 'default'
|
||||
})
|
||||
data.size = 'default'
|
||||
} else {
|
||||
store.dispatch('SET_USER_SETTINGS', {
|
||||
elementPlusSize: 'large'
|
||||
})
|
||||
data.size = 'large'
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
setTheme()
|
||||
elementPlusSizeHandle(window.innerWidth)
|
||||
window.addEventListener('resize', (e: any) => {
|
||||
elementPlusSizeHandle(e.target.innerWidth)
|
||||
})
|
||||
})
|
||||
|
||||
return {
|
||||
...toRefs(data)
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
#app {
|
||||
font-family Avenir, Helvetica, Arial, sans-serif
|
||||
-webkit-font-smoothing antialiased
|
||||
-moz-osx-font-smoothing grayscale
|
||||
box-sizing border-box
|
||||
position relative
|
||||
width 100%
|
||||
height 100%
|
||||
}
|
||||
</style>
|
||||
BIN
picx/src/assets/logo.png
Normal file
BIN
picx/src/assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
82
picx/src/common/api/index.ts
Normal file
82
picx/src/common/api/index.ts
Normal file
@@ -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)
|
||||
})
|
||||
})
|
||||
}
|
||||
6
picx/src/common/model/delete.model.ts
Normal file
6
picx/src/common/model/delete.model.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
export enum deleteStatusEnum {
|
||||
deleted = 'deleted',
|
||||
allDeleted = 'allDeleted',
|
||||
deleteFail = 'deleteFail'
|
||||
}
|
||||
11
picx/src/common/model/dir.model.ts
Normal file
11
picx/src/common/model/dir.model.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export interface DirModel {
|
||||
value: string
|
||||
label: string
|
||||
}
|
||||
|
||||
export enum DirModeEnum {
|
||||
autoDir = 'autoDir',
|
||||
newDir = 'newDir',
|
||||
rootDir = 'rootDir',
|
||||
reposDir = 'reposDir'
|
||||
}
|
||||
8
picx/src/common/model/external-link.model.ts
Normal file
8
picx/src/common/model/external-link.model.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
enum ExternalLinkType {
|
||||
staticaly = 'staticaly',
|
||||
jsdelivr = 'jsdelivr',
|
||||
github = 'github',
|
||||
cloudflare = 'cloudflare'
|
||||
}
|
||||
|
||||
export default ExternalLinkType
|
||||
6
picx/src/common/model/storage.model.ts
Normal file
6
picx/src/common/model/storage.model.ts
Normal file
@@ -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`
|
||||
68
picx/src/common/model/upload.model.ts
Normal file
68
picx/src/common/model/upload.model.ts
Normal file
@@ -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
|
||||
}
|
||||
35
picx/src/common/model/user-config-info.model.ts
Normal file
35
picx/src/common/model/user-config-info.model.ts
Normal file
@@ -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
|
||||
}
|
||||
15
picx/src/common/model/user-settings.model.ts
Normal file
15
picx/src/common/model/user-settings.model.ts
Normal file
@@ -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
|
||||
}
|
||||
11
picx/src/common/model/vite-config.model.ts
Normal file
11
picx/src/common/model/vite-config.model.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export declare type Recordable<T = any> = Record<string, T>
|
||||
|
||||
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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
104
picx/src/components/copy-external-link/copy-external-link.vue
Normal file
104
picx/src/components/copy-external-link/copy-external-link.vue
Normal file
@@ -0,0 +1,104 @@
|
||||
<template>
|
||||
<div class="copy-external-link-box">
|
||||
<div>
|
||||
<el-tooltip
|
||||
:content="img.is_transform_md ? '点击转换普通外链' : '点击转换 Markdown 格式外链'"
|
||||
placement="top"
|
||||
>
|
||||
<div
|
||||
class="markdown-icon-box flex-center"
|
||||
@click="img.is_transform_md = !img.is_transform_md"
|
||||
>
|
||||
<svg
|
||||
v-if="!img.is_transform_md"
|
||||
t="1631782798077"
|
||||
class="markdown-icon"
|
||||
viewBox="0 0 1024 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
p-id="2861"
|
||||
width="26"
|
||||
height="26"
|
||||
>
|
||||
<path
|
||||
d="M92 192C42.24 192 0 232.128 0 282.016v459.968C0 791.904 42.24 832 92 832h840C981.76 832 1024 791.872 1024 741.984V282.016C1024 232.16 981.76 192 932 192z m0 64h840c16.512 0 28 12.256 28 26.016v459.968c0 13.76-11.52 26.016-28 26.016H92C75.488 768 64 755.744 64 741.984V282.016c0-13.76 11.52-25.984 28-25.984zM160 352v320h96v-212.992l96 127.008 96-127.04V672h96V352h-96l-96 128-96-128z m544 0v160h-96l144 160 144-160h-96v-160z"
|
||||
p-id="2862"
|
||||
fill="#808080"
|
||||
></path>
|
||||
</svg>
|
||||
<svg
|
||||
v-if="img.is_transform_md"
|
||||
t="1631784688556"
|
||||
class="markdown-icon active"
|
||||
viewBox="0 0 1280 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
p-id="3242"
|
||||
width="26"
|
||||
height="26"
|
||||
>
|
||||
<path
|
||||
d="M1187.6 118.2H92.4C41.4 118.2 0 159.6 0 210.4v603c0 51 41.4 92.4 92.4 92.4h1095.4c51 0 92.4-41.4 92.2-92.2V210.4c0-50.8-41.4-92.2-92.4-92.2zM677 721.2H554v-240l-123 153.8-123-153.8v240H184.6V302.8h123l123 153.8 123-153.8h123v418.4z m270.6 6.2L763 512H886V302.8h123V512H1132z"
|
||||
p-id="3243"
|
||||
fill="#3c3c3c"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div class="btn-box">
|
||||
<el-tooltip content="点击复制 Staticaly CDN 外链" placement="top">
|
||||
<span
|
||||
class="btn-item copy-url flex-center"
|
||||
@click="copyLink(externalLinkType.staticaly)"
|
||||
>
|
||||
Staticaly
|
||||
</span>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="点击复制 Cloudflare CDN 外链" placement="top">
|
||||
<span
|
||||
class="btn-item copy-url flex-center"
|
||||
@click="copyLink(externalLinkType.cloudflare)"
|
||||
>
|
||||
Cloudflare
|
||||
</span>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, computed, ref, onUpdated } from 'vue'
|
||||
import ExternalLinkType from '@/common/model/external-link.model'
|
||||
import { store } from '@/store'
|
||||
import { copyExternalLink } from '@/utils/external-link-handler'
|
||||
import { UploadedImageModel } from '@/common/model/upload.model'
|
||||
|
||||
const props = defineProps({
|
||||
imgObj: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
})
|
||||
|
||||
const userSettings = computed(() => store.getters.getUserSettings).value
|
||||
|
||||
let img = ref(props.imgObj as UploadedImageModel).value
|
||||
const externalLinkType = ExternalLinkType
|
||||
|
||||
const copyLink = (type: ExternalLinkType) => {
|
||||
copyExternalLink(img, type)
|
||||
}
|
||||
|
||||
onUpdated(() => {
|
||||
img = props.imgObj
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
img.is_transform_md = userSettings.defaultMarkdown
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="stylus">
|
||||
@import "copy-external-link.styl"
|
||||
</style>
|
||||
47
picx/src/components/folder-card/folder-card.styl
Normal file
47
picx/src/components/folder-card/folder-card.styl
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
116
picx/src/components/folder-card/folder-card.vue
Normal file
116
picx/src/components/folder-card/folder-card.vue
Normal file
@@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<div class="folder-card" @dblclick="dblclickFolder">
|
||||
<el-tooltip
|
||||
v-if="mode === 'dir'"
|
||||
effect="dark"
|
||||
content="双击进入下一级目录"
|
||||
placement="top"
|
||||
>
|
||||
<div class="icon">
|
||||
<svg
|
||||
t="1639999626518"
|
||||
class="icon"
|
||||
viewBox="0 0 1228 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
p-id="3575"
|
||||
width="200"
|
||||
height="200"
|
||||
>
|
||||
<path
|
||||
d="M1196.987733 212.5824v540.0576c0 39.594667-34.474667 71.3728-76.765866 71.3728H323.242667c-51.780267 0-88.746667-46.762667-73.250134-92.808533l126.737067-375.808H70.417067C31.675733 355.362133 0 326.4512 0 291.089067V98.372267C0 63.044267 31.675733 34.0992 70.417067 34.0992h378.811733c26.7264 0 51.029333 13.9264 63.010133 35.703467l39.048534 71.406933H1120.256c42.257067 0 76.8 32.119467 76.8 71.3728"
|
||||
fill="#5398DF"
|
||||
p-id="3576"
|
||||
></path>
|
||||
<path
|
||||
d="M1128.721067 997.853867H68.266667a68.266667 68.266667 0 0 1-68.266667-68.266667V280.3712a68.266667 68.266667 0 0 1 68.266667-68.266667h1060.4544a68.266667 68.266667 0 0 1 68.266666 68.266667V929.5872a68.266667 68.266667 0 0 1-68.266666 68.266667"
|
||||
fill="#85BCFF"
|
||||
p-id="3577"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
|
||||
<div class="icon" v-if="mode === 'back'">
|
||||
<svg
|
||||
t="1640264285200"
|
||||
class="icon"
|
||||
viewBox="0 0 1024 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
p-id="29312"
|
||||
width="200"
|
||||
height="200"
|
||||
>
|
||||
<path
|
||||
d="M426.666667 384V213.333333l-298.666667 298.666667 298.666667 298.666667v-174.933334c213.333333 0 362.666667 68.266667 469.333333 217.6-42.666667-213.333333-170.666667-426.666667-469.333333-469.333333z"
|
||||
p-id="29313"
|
||||
fill="#85BCFF"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="text" v-if="mode === 'dir'">{{ folderObj.dir }}</div>
|
||||
<div class="text" v-if="mode === 'back'">双击返回</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import { useStore } from '@/store'
|
||||
|
||||
const store = useStore()
|
||||
|
||||
const userConfigInfo = computed(() => store.getters.getUserConfigInfo).value
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
const props = defineProps({
|
||||
folderObj: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'dir'
|
||||
}
|
||||
})
|
||||
|
||||
const emits = defineEmits(['update:modelValue'])
|
||||
|
||||
const dblclickFolder = () => {
|
||||
const { folderObj, mode } = props
|
||||
|
||||
if (mode === 'back') {
|
||||
const currentDir = userConfigInfo.selectedDir
|
||||
|
||||
if (currentDir === '/') {
|
||||
return
|
||||
}
|
||||
|
||||
const currentDirList = currentDir.split('/')
|
||||
|
||||
if (currentDirList.length === 1) {
|
||||
userConfigInfo.selectedDir = '/'
|
||||
} else if (currentDirList.length > 1) {
|
||||
currentDirList.length -= 1
|
||||
userConfigInfo.selectedDir = currentDirList.join('/')
|
||||
}
|
||||
} else {
|
||||
userConfigInfo.selectedDir = folderObj.dirPath
|
||||
}
|
||||
|
||||
const { selectedDir } = userConfigInfo
|
||||
|
||||
if (selectedDir === '/') {
|
||||
userConfigInfo.selectedDirList = []
|
||||
userConfigInfo.dirMode = 'rootDir'
|
||||
} else {
|
||||
userConfigInfo.selectedDirList = selectedDir.split('/')
|
||||
userConfigInfo.dirMode = 'reposDir'
|
||||
}
|
||||
store.dispatch('USER_CONFIG_INFO_NOT_PERSIST')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="stylus">
|
||||
@import 'folder-card.styl';
|
||||
</style>
|
||||
92
picx/src/components/header-content/header-content.styl
Normal file
92
picx/src/components/header-content/header-content.styl
Normal file
@@ -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%;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
122
picx/src/components/header-content/header-content.vue
Normal file
122
picx/src/components/header-content/header-content.vue
Normal file
@@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<header class="header">
|
||||
<div class="header-left">
|
||||
<div class="brand" @click="router.push('/')">
|
||||
<div class="logo">
|
||||
<img src="../../assets/logo.png" alt="PicX" />
|
||||
</div>
|
||||
<div class="title">PicX</div>
|
||||
</div>
|
||||
<div class="website-count" @click="goGitHubRepo">
|
||||
<el-tooltip content="感觉好用,点 Star 支持作者(* ̄︶ ̄)" placement="bottom">
|
||||
<site-count :isuv="false" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
<div class="user-info" @click="onUserInfoClick">
|
||||
<div class="username">
|
||||
{{ userConfigInfo.owner ? userConfigInfo.owner : defaultUsername }}
|
||||
</div>
|
||||
|
||||
<div class="avatar" v-if="!userConfigInfo?.avatarUrl">
|
||||
<el-icon :size="22"><UserFilled /></el-icon>
|
||||
</div>
|
||||
|
||||
<el-dropdown
|
||||
trigger="click"
|
||||
@command="handleCommand"
|
||||
v-if="userConfigInfo?.avatarUrl"
|
||||
>
|
||||
<span class="el-dropdown-link">
|
||||
<span class="avatar">
|
||||
<img :src="userConfigInfo?.avatarUrl" :alt="userConfigInfo?.owner" />
|
||||
</span>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="logout"> 退出登录 </el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, reactive, computed, toRefs } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useStore } from '@/store'
|
||||
import siteCount from '@/components/site-count/site-count.vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'header-content',
|
||||
|
||||
components: {
|
||||
siteCount
|
||||
},
|
||||
|
||||
setup() {
|
||||
const router = useRouter()
|
||||
const store = useStore()
|
||||
|
||||
const reactiveData = reactive({
|
||||
defaultUsername: '未登录',
|
||||
userConfigInfo: computed(() => store.state.userConfigInfoModule.userConfigInfo)
|
||||
})
|
||||
|
||||
const onUserInfoClick = () => {
|
||||
if (
|
||||
!reactiveData.userConfigInfo.loggingStatus &&
|
||||
router.currentRoute.value.path !== '/config'
|
||||
) {
|
||||
router.push('/config')
|
||||
}
|
||||
}
|
||||
|
||||
const logout = () => {
|
||||
store.dispatch('LOGOUT')
|
||||
router.push('/config')
|
||||
}
|
||||
|
||||
const handleCommand = (command: string) => {
|
||||
// eslint-disable-next-line default-case
|
||||
switch (command) {
|
||||
case 'upload':
|
||||
router.push('/')
|
||||
break
|
||||
|
||||
case 'config':
|
||||
router.push('/config')
|
||||
break
|
||||
|
||||
case 'management':
|
||||
router.push('/management')
|
||||
break
|
||||
|
||||
case 'logout':
|
||||
logout()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const goGitHubRepo = () => {
|
||||
window.open('https://github.com/XPoet/picx')
|
||||
}
|
||||
|
||||
return {
|
||||
...toRefs(reactiveData),
|
||||
router,
|
||||
onUserInfoClick,
|
||||
handleCommand,
|
||||
goGitHubRepo
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="stylus">
|
||||
@import "header-content.styl"
|
||||
</style>
|
||||
142
picx/src/components/image-card/image-card.styl
Normal file
142
picx/src/components/image-card/image-card.styl
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
287
picx/src/components/image-card/image-card.vue
Normal file
287
picx/src/components/image-card/image-card.vue
Normal file
@@ -0,0 +1,287 @@
|
||||
<template>
|
||||
<div
|
||||
class="image-card"
|
||||
:class="{ listing: listing, checked: imageObj.checked }"
|
||||
v-loading="imageObj.deleting"
|
||||
element-loading-text="删除中..."
|
||||
@mouseenter="isShowDelBtn = true"
|
||||
@mouseleave="isShowDelBtn = false"
|
||||
>
|
||||
<div class="image-box">
|
||||
<img data-fancybox="gallery" :src="imgUrl" />
|
||||
</div>
|
||||
<div class="info-box">
|
||||
<div class="image-info">
|
||||
<el-input
|
||||
size="small"
|
||||
v-if="renameValue && props.modelValue === props.index"
|
||||
class="rename-input"
|
||||
v-model="renameValue"
|
||||
@blur="renameInputBlur"
|
||||
@keydown.enter.prevent="updateRename"
|
||||
ref="renameInputRef"
|
||||
></el-input>
|
||||
<div class="filename" v-else>{{ imageObj.name }}</div>
|
||||
<div class="image-operation">
|
||||
<copy-external-link :img-obj="imageObj" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="operation-box"
|
||||
v-show="isShowDelBtn || dropdownVisible || imageObj.checked"
|
||||
>
|
||||
<div class="operation-left">
|
||||
<div
|
||||
v-if="isManagementPage"
|
||||
:class="[imageObj.checked ? 'picked-btn' : 'pick-btn', 'operation-btn']"
|
||||
@click="trigglePick(imageObj)"
|
||||
>
|
||||
<el-icon v-if="imageObj.checked"><Check /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div class="operation-right">
|
||||
<el-dropdown size="default" trigger="click" @visible-change="visibleChange">
|
||||
<div class="operation-btn">
|
||||
<el-icon><MoreFilled /></el-icon>
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="deleteImageTips(imageObj)">
|
||||
删除
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click.self="renameImage(imageObj)">
|
||||
重命名
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="viewImageProperties(imageObj)">
|
||||
属性
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
import type { ElInput } from 'element-plus'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useStore } from '@/store'
|
||||
import axios from '@/utils/axios'
|
||||
import { UploadedImageModel } from '@/common/model/upload.model'
|
||||
import { getBase64ByImageUrl, getImage } from '@/utils/rename-image'
|
||||
import { uploadImage_single } from '@/utils/upload-helper'
|
||||
import { getFilename, getFileSize, getFileSuffix } from '@/utils/file-handle-helper'
|
||||
import ExternalLinkType from '@/common/model/external-link.model'
|
||||
|
||||
const props = defineProps({
|
||||
listing: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
imageObj: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
isUploaded: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
index: {
|
||||
type: Number
|
||||
},
|
||||
modelValue: {
|
||||
type: Number
|
||||
}
|
||||
})
|
||||
|
||||
const emits = defineEmits(['update:modelValue'])
|
||||
|
||||
const store = useStore()
|
||||
const router = useRoute()
|
||||
const userConfigInfo = computed(() => store.getters.getUserConfigInfo).value
|
||||
const userSettings = computed(() => store.getters.getUserSettings).value
|
||||
const isManagementPage = computed(() => {
|
||||
return router.path === '/management'
|
||||
})
|
||||
|
||||
const imgUrl = computed(() => {
|
||||
switch (userSettings.externalLinkType) {
|
||||
case ExternalLinkType.jsdelivr:
|
||||
return props.imageObj.jsdelivr_cdn_url
|
||||
case ExternalLinkType.staticaly:
|
||||
return props.imageObj.staticaly_cdn_url
|
||||
case ExternalLinkType.cloudflare:
|
||||
return props.imageObj.cloudflare_cdn_url
|
||||
default:
|
||||
return props.imageObj.github_url
|
||||
}
|
||||
})
|
||||
|
||||
const renameInputRef = ref<InstanceType<typeof ElInput>>()
|
||||
|
||||
const isShowDelBtn = ref(false)
|
||||
|
||||
const renameValue = ref('')
|
||||
|
||||
const dropdownVisible = ref<Boolean>(false)
|
||||
|
||||
const doDeleteImage = (
|
||||
imageObj: UploadedImageModel,
|
||||
isRename: boolean = false
|
||||
): Promise<Boolean> => {
|
||||
if (!isRename) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
imageObj.deleting = true
|
||||
}
|
||||
const { owner, selectedRepos } = userConfigInfo
|
||||
|
||||
return new Promise((resolve) => {
|
||||
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) => {
|
||||
console.log('[deleteImage] ', res)
|
||||
if (res && res.status === 200) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
imageObj.deleting = false
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
ElMessage.success(`${isRename ? '更新' : '删除'}成功!`)
|
||||
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
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const deleteImageTips = (imageObj: UploadedImageModel) => {
|
||||
ElMessageBox.confirm(
|
||||
`
|
||||
<div>此操作将会永久删除图片:</div>
|
||||
<strong>${imageObj.name}</strong>
|
||||
`,
|
||||
`删除提示`,
|
||||
{
|
||||
dangerouslyUseHTMLString: true,
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
doDeleteImage(imageObj)
|
||||
})
|
||||
.catch(() => {
|
||||
console.log('取消删除')
|
||||
})
|
||||
}
|
||||
|
||||
const trigglePick = (imageObj: UploadedImageModel) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
imageObj.checked = !imageObj.checked
|
||||
store.commit('IMAGE_CARD', { imageObj })
|
||||
}
|
||||
|
||||
const renameImage = async (imgObj: UploadedImageModel) => {
|
||||
emits('update:modelValue', props.index)
|
||||
renameValue.value = getFilename(imgObj.name)
|
||||
await nextTick(() => {
|
||||
const temp = setTimeout(() => {
|
||||
renameInputRef.value?.focus()
|
||||
clearTimeout(temp)
|
||||
}, 150)
|
||||
})
|
||||
}
|
||||
|
||||
const renameInputBlur = () => {
|
||||
emits('update:modelValue', undefined)
|
||||
}
|
||||
|
||||
const updateRename = async () => {
|
||||
renameInputBlur()
|
||||
|
||||
const { imageObj }: any = props
|
||||
|
||||
if (renameValue.value === getFilename(imageObj.name) || !renameValue.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const renameFn = async () => {
|
||||
const loading = ElLoading.service({
|
||||
lock: true,
|
||||
text: '正在重命名...'
|
||||
})
|
||||
|
||||
const suffix = getFileSuffix(imageObj.name)
|
||||
|
||||
const imgInfo = {
|
||||
name: renameValue.value + imageObj.name.substring(imageObj.name.indexOf('.')),
|
||||
size: imageObj.size,
|
||||
lastModified: Date.now(),
|
||||
type: `image/${suffix}`
|
||||
}
|
||||
|
||||
const base64 = await getBase64ByImageUrl(imgUrl.value, suffix)
|
||||
|
||||
if (base64) {
|
||||
const newImgObj = getImage(base64, imgInfo)
|
||||
if (newImgObj) {
|
||||
const isUploadSuccess = await uploadImage_single(userConfigInfo, newImgObj)
|
||||
|
||||
if (isUploadSuccess) {
|
||||
renameValue.value = ''
|
||||
await doDeleteImage(imageObj, true)
|
||||
await store.dispatch('UPLOADED_LIST_REMOVE', newImgObj.uuid)
|
||||
}
|
||||
}
|
||||
}
|
||||
loading.close()
|
||||
}
|
||||
|
||||
ElMessageBox.confirm(`该图片重命名为 ${renameValue.value} ?`, `提示`, {
|
||||
type: 'warning'
|
||||
})
|
||||
.then(async () => {
|
||||
await renameFn()
|
||||
})
|
||||
.catch(() => {
|
||||
console.log('取消图片重命名')
|
||||
})
|
||||
}
|
||||
|
||||
const visibleChange = (e: boolean) => {
|
||||
dropdownVisible.value = e
|
||||
}
|
||||
|
||||
const viewImageProperties = (imgObj: UploadedImageModel) => {
|
||||
ElMessageBox.confirm(
|
||||
`
|
||||
<div>图片名称:<strong>${imgObj.name}</strong></div>
|
||||
<div>图片大小:<strong>${getFileSize(imgObj.size)} KB</strong></div>
|
||||
`,
|
||||
`属性`,
|
||||
{
|
||||
showCancelButton: false,
|
||||
showConfirmButton: false,
|
||||
dangerouslyUseHTMLString: true,
|
||||
type: 'info'
|
||||
}
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="stylus">
|
||||
@import 'image-card.styl';
|
||||
</style>
|
||||
52
picx/src/components/image-selector/image-selector.styl
Normal file
52
picx/src/components/image-selector/image-selector.styl
Normal file
@@ -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
|
||||
}
|
||||
103
picx/src/components/image-selector/image-selector.vue
Normal file
103
picx/src/components/image-selector/image-selector.vue
Normal file
@@ -0,0 +1,103 @@
|
||||
<template>
|
||||
<div class="selector-wrapper" v-if="getImageCardCheckedNum">
|
||||
<div class="selector-left-box">
|
||||
<el-checkbox
|
||||
:label="checked ? '取消全选' : '全选'"
|
||||
v-model="checked"
|
||||
@change="triggleFullCheck"
|
||||
></el-checkbox>
|
||||
<div class="item">已选择 {{ getImageCardCheckedNum }} 张图片</div>
|
||||
<div class="item cancel-select-btn" @click="cancelPick">取消选择</div>
|
||||
</div>
|
||||
<div class="selector-right-box">
|
||||
<el-tooltip placement="top" content="批量复制外链">
|
||||
<el-icon class="btn-icon" @click="copyLink"><CopyDocument /></el-icon>
|
||||
</el-tooltip>
|
||||
<el-tooltip placement="top" content="批量删除图片">
|
||||
<el-icon class="btn-icon" @click="batchDeleteImage"><Delete /></el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, watch, ref } from 'vue'
|
||||
import { useStore } from '@/store'
|
||||
import { UploadedImageModel } from '@/common/model/upload.model'
|
||||
import { batchCopyExternalLink } from '@/utils/external-link-handler'
|
||||
import { delelteBatchImage } from '@/utils/delete-image-card'
|
||||
import { deleteStatusEnum } from '@/common/model/delete.model'
|
||||
|
||||
const props = defineProps({
|
||||
currentDirImageList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
|
||||
const emits = defineEmits(['update:initImageList'])
|
||||
|
||||
const store = useStore()
|
||||
const checked = ref(false)
|
||||
|
||||
const getImageCardCheckedArr = computed(() => store.getters.getImageCardCheckedArr)
|
||||
const userConfigInfo = computed(() => store.getters.getUserConfigInfo).value
|
||||
const userSettings = computed(() => store.getters.getUserSettings).value
|
||||
const getImageCardCheckedNum = computed(() => getImageCardCheckedArr.value.length || 0)
|
||||
|
||||
watch(
|
||||
() => getImageCardCheckedNum.value,
|
||||
(newVal) => {
|
||||
const newValCheckedNum = props.currentDirImageList.length
|
||||
checked.value = newVal === newValCheckedNum
|
||||
}
|
||||
)
|
||||
|
||||
function copyLink() {
|
||||
batchCopyExternalLink(getImageCardCheckedArr.value, userSettings.externalLinkType)
|
||||
}
|
||||
|
||||
function cancelPick() {
|
||||
props.currentDirImageList.forEach((item: UploadedImageModel) => {
|
||||
if (item.checked) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
item.checked = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function batchDeleteImage() {
|
||||
if (getImageCardCheckedArr.value?.length > 0) {
|
||||
ElMessageBox.confirm('是否批量删除已选中的图片?', '删除提示', {
|
||||
type: 'warning'
|
||||
})
|
||||
.then(async () => {
|
||||
const res = await delelteBatchImage(getImageCardCheckedArr.value, userConfigInfo)
|
||||
if (res === deleteStatusEnum.allDeleted) {
|
||||
ElMessage.success('批量删除成功!')
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
console.log('取消批量删除')
|
||||
})
|
||||
} else {
|
||||
ElMessage.warning('请先选择图片')
|
||||
}
|
||||
}
|
||||
|
||||
function triggleFullCheck() {
|
||||
let checkedImgArr: Array<UploadedImageModel> = []
|
||||
props.currentDirImageList.forEach((item: UploadedImageModel) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
item.checked = checked.value
|
||||
})
|
||||
checkedImgArr = props.currentDirImageList as any
|
||||
store.commit('REPLACE_IMAGE_CARD', { checkedImgArr })
|
||||
}
|
||||
onMounted(() => {
|
||||
triggleFullCheck()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="stylus">
|
||||
@import 'image-selector.styl';
|
||||
</style>
|
||||
75
picx/src/components/image-viewer/image-viewer.styl
Normal file
75
picx/src/components/image-viewer/image-viewer.styl
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
44
picx/src/components/image-viewer/image-viewer.vue
Normal file
44
picx/src/components/image-viewer/image-viewer.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<div
|
||||
class="image-viewer"
|
||||
:class="{ active: imageViewer.isShow }"
|
||||
@click="imageViewer.isShow = false"
|
||||
>
|
||||
<div class="image-box" v-if="imageViewer?.imgInfo?.url">
|
||||
<img class="img" :src="imageViewer?.imgInfo?.url" />
|
||||
<div class="image-info" v-if="imageViewer.imgInfo">
|
||||
<el-tag class="item" size="small" v-if="imageViewer.imgInfo.name">
|
||||
图片名:{{ imageViewer.imgInfo.name }}
|
||||
</el-tag>
|
||||
<el-tag class="item" size="small" v-if="imageViewer.imgInfo.size">
|
||||
图片大小:{{ parseFileSize(imageViewer.imgInfo.size) }}
|
||||
</el-tag>
|
||||
<el-tag class="item" size="small" v-if="imageViewer.imgInfo.lastModified">
|
||||
最后修改时间:{{ formatLastModified(imageViewer.imgInfo.lastModified) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import { useStore } from '@/store'
|
||||
import { getFileSize } from '@/utils/file-handle-helper'
|
||||
import TimeHelper from '@/utils/time-helper'
|
||||
|
||||
const store = useStore()
|
||||
const imageViewer = computed(() => store.getters.getImageViewer)
|
||||
|
||||
function parseFileSize(size: number) {
|
||||
return `${getFileSize(size)} KB`
|
||||
}
|
||||
|
||||
function formatLastModified(t: number) {
|
||||
return TimeHelper.formatTimestamp(t)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="stylus">
|
||||
@import "image-viewer.styl"
|
||||
</style>
|
||||
73
picx/src/components/main-container/main-container.styl
Normal file
73
picx/src/components/main-container/main-container.styl
Normal file
@@ -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%
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
64
picx/src/components/main-container/main-container.vue
Normal file
64
picx/src/components/main-container/main-container.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<main class="main-container" @click="changeUploadAreaActive">
|
||||
<div class="top">
|
||||
<header-content />
|
||||
</div>
|
||||
|
||||
<div class="bottom">
|
||||
<div class="container">
|
||||
<div class="left">
|
||||
<nav-content />
|
||||
</div>
|
||||
|
||||
<div class="right">
|
||||
<div class="content">
|
||||
<router-view />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<image-viewer></image-viewer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, onMounted } from 'vue'
|
||||
import headerContent from '@/components/header-content/header-content.vue'
|
||||
import navContent from '@/components/nav-content/nav-content.vue'
|
||||
import imageViewer from '@/components/image-viewer/image-viewer.vue'
|
||||
import { useStore } from '@/store'
|
||||
import userConfigInfoModel from '@/utils/set-theme-mode'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'main-container',
|
||||
|
||||
components: {
|
||||
headerContent,
|
||||
navContent,
|
||||
imageViewer
|
||||
},
|
||||
|
||||
setup() {
|
||||
const store = useStore()
|
||||
|
||||
const changeUploadAreaActive = (e: any) => {
|
||||
store.commit(
|
||||
'CHANGE_UPLOAD_AREA_ACTIVE',
|
||||
e.target.classList.contains('active-upload')
|
||||
)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
userConfigInfoModel()
|
||||
})
|
||||
|
||||
return {
|
||||
changeUploadAreaActive
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="stylus">
|
||||
@import "main-container.styl"
|
||||
</style>
|
||||
40
picx/src/components/nav-content/nav-content.styl
Normal file
40
picx/src/components/nav-content/nav-content.styl
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
152
picx/src/components/nav-content/nav-content.vue
Normal file
152
picx/src/components/nav-content/nav-content.vue
Normal file
@@ -0,0 +1,152 @@
|
||||
<template>
|
||||
<aside class="nav">
|
||||
<ul class="nav-list">
|
||||
<li
|
||||
class="nav-item flex-center"
|
||||
v-for="(navItem, index) in navList"
|
||||
:key="index"
|
||||
:class="{ active: navItem.isActive }"
|
||||
@click="navClick(navItem)"
|
||||
v-show="navItem.isShow"
|
||||
>
|
||||
<div class="nav-content">
|
||||
<el-icon :size="navIconSize">
|
||||
<component :is="navItem.icon"></component>
|
||||
</el-icon>
|
||||
<span class="nav-name">{{ navItem.name }}</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, watch, computed, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useStore } from '@/store'
|
||||
|
||||
const router = useRouter()
|
||||
const store = useStore()
|
||||
|
||||
const userConfigInfo = computed(() => store.getters.getUserConfigInfo).value
|
||||
const userSettings = computed(() => store.getters.getUserSettings).value
|
||||
|
||||
const navIconSize = computed(() => {
|
||||
switch (userSettings.elementPlusSize) {
|
||||
case 'small':
|
||||
return 22
|
||||
case 'large':
|
||||
return 30
|
||||
default:
|
||||
return 26
|
||||
}
|
||||
})
|
||||
|
||||
const navList = ref([
|
||||
{
|
||||
name: '图床配置',
|
||||
icon: 'edit',
|
||||
isActive: false,
|
||||
path: '/config',
|
||||
isShow: true
|
||||
},
|
||||
{
|
||||
name: '上传图片',
|
||||
icon: 'upload',
|
||||
isActive: false,
|
||||
path: '/upload',
|
||||
isShow: true
|
||||
},
|
||||
{
|
||||
name: '图床管理',
|
||||
icon: 'box',
|
||||
isActive: false,
|
||||
path: '/management',
|
||||
isShow: true
|
||||
},
|
||||
{
|
||||
name: '我的设置',
|
||||
icon: 'setting',
|
||||
isActive: false,
|
||||
path: '/settings',
|
||||
isShow: true
|
||||
},
|
||||
{
|
||||
name: '使用教程',
|
||||
icon: 'magic-stick',
|
||||
isActive: false,
|
||||
path: '/tutorials',
|
||||
isShow: true
|
||||
},
|
||||
{
|
||||
name: '帮助反馈',
|
||||
icon: 'chat-dot-round',
|
||||
isActive: false,
|
||||
path: '/about',
|
||||
isShow: true
|
||||
}
|
||||
])
|
||||
|
||||
const navClick = (e: any) => {
|
||||
const { path } = e
|
||||
|
||||
if (path === '/management') {
|
||||
if (userConfigInfo.selectedRepos === '') {
|
||||
ElMessage.warning('请选择一个仓库!')
|
||||
router.push('/config')
|
||||
return
|
||||
}
|
||||
|
||||
if (userConfigInfo.selectedDir === '') {
|
||||
ElMessage.warning('目录不能为空!')
|
||||
router.push('/config')
|
||||
return
|
||||
}
|
||||
}
|
||||
router.push(path)
|
||||
}
|
||||
|
||||
const changeNavActive = (currentPath: string) => {
|
||||
navList.value.forEach((v) => {
|
||||
const temp = v
|
||||
temp.isActive = v.path === currentPath
|
||||
return temp
|
||||
})
|
||||
}
|
||||
|
||||
watch(
|
||||
() => router.currentRoute.value,
|
||||
(_n) => {
|
||||
changeNavActive(_n.path)
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => userConfigInfo.loggingStatus,
|
||||
(_n) => {
|
||||
navList.value.forEach((v: any) => {
|
||||
// eslint-disable-next-line default-case
|
||||
switch (v.path) {
|
||||
case '/management':
|
||||
case '/settings':
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
v.isShow = _n
|
||||
}
|
||||
})
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
router.isReady().then(() => {
|
||||
changeNavActive(router.currentRoute.value.path)
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="stylus">
|
||||
@import "nav-content.styl"
|
||||
</style>
|
||||
16
picx/src/components/selected-info-bar/selected-info-bar.styl
Normal file
16
picx/src/components/selected-info-bar/selected-info-bar.styl
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
35
picx/src/components/selected-info-bar/selected-info-bar.vue
Normal file
35
picx/src/components/selected-info-bar/selected-info-bar.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<div class="selected-info-bar-box" v-if="userConfigInfo.selectedRepos">
|
||||
<span class="info-item">
|
||||
仓库:
|
||||
<el-tag>
|
||||
{{ userConfigInfo.selectedRepos }}
|
||||
</el-tag>
|
||||
</span>
|
||||
<span class="info-item" v-if="userConfigInfo.selectedBranch">
|
||||
分支:
|
||||
<el-tag>
|
||||
{{ userConfigInfo.selectedBranch }}
|
||||
</el-tag>
|
||||
</span>
|
||||
<span class="info-item" v-if="userConfigInfo.selectedDir">
|
||||
目录:
|
||||
<el-tag>
|
||||
{{ userConfigInfo.selectedDir }}
|
||||
</el-tag>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import { useStore } from '@/store'
|
||||
|
||||
const store = useStore()
|
||||
const userConfigInfo = computed(() => store.getters.getUserConfigInfo)
|
||||
const userSettings = computed(() => store.getters.getUserSettings)
|
||||
</script>
|
||||
|
||||
<style scoped lang="stylus">
|
||||
@import "selected-info-bar.styl"
|
||||
</style>
|
||||
58
picx/src/components/site-count/site-count.vue
Normal file
58
picx/src/components/site-count/site-count.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<span class="site-count" ref="siteCountDom" v-show="isShow">
|
||||
超过
|
||||
<span id="busuanzi_value_site_uv" class="uv" v-show="isuv"></span>
|
||||
<span id="busuanzi_value_site_pv" class="pv" v-show="!isuv"></span>
|
||||
次被使用
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, onMounted, ref, Ref } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'site-count',
|
||||
|
||||
props: {
|
||||
isuv: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
setup(props, ctx) {
|
||||
const siteCountDom: Ref = ref<null | HTMLElement>(null)
|
||||
const isShow: Ref<boolean> = ref(false)
|
||||
|
||||
const getInnerText = (dom, isuv) => {
|
||||
return dom.querySelector(`.${isuv ? 'u' : 'p'}v`).innerText
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const script: any = document.createElement('script')
|
||||
script.async = true
|
||||
script.src = '//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js'
|
||||
siteCountDom.value.appendChild(script)
|
||||
|
||||
script.onload = () => {
|
||||
const tempT = setTimeout(() => {
|
||||
if (getInnerText(siteCountDom.value, props.isuv)) {
|
||||
isShow.value = true
|
||||
}
|
||||
clearTimeout(tempT)
|
||||
}, 1500)
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
siteCountDom,
|
||||
isShow
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style lang="stylus">
|
||||
.site-count {
|
||||
transition all 0.2s ease-in
|
||||
}
|
||||
</style>
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
<template>
|
||||
<div
|
||||
class="to-upload-image-list-card"
|
||||
v-if="toUploadImage.list.length || userConfigInfo.selectedRepos"
|
||||
>
|
||||
<div class="header">
|
||||
<div>
|
||||
<selected-info-bar />
|
||||
</div>
|
||||
<div>
|
||||
<span v-if="toUploadImage.list.length">
|
||||
已上传:{{ toUploadImage.uploadedNumber }} / {{ toUploadImage.list.length }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="body" v-if="toUploadImage.list.length">
|
||||
<ul class="image-uploading-info-box">
|
||||
<li
|
||||
class="image-uploading-info-item"
|
||||
:class="{ disable: loadingAllImage }"
|
||||
v-for="(imgItem, index) in toUploadImage.list"
|
||||
:key="index"
|
||||
>
|
||||
<div class="left-image-box">
|
||||
<img data-fancybox="gallery" :src="imgItem.imgData.base64Url" />
|
||||
</div>
|
||||
|
||||
<div class="right-operation-box">
|
||||
<div class="top">
|
||||
<div class="image-name">
|
||||
{{ imgItem.filename.now }}
|
||||
</div>
|
||||
<div class="image-info">
|
||||
<span class="file-size item" v-if="userSettings.isCompress">
|
||||
<del>
|
||||
{{ getFileSize(imgItem.fileInfo.originSize) }}
|
||||
</del>
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="file-size item"
|
||||
:class="{ compressed: userSettings.isCompress }"
|
||||
>
|
||||
{{ getFileSize(imgItem.fileInfo.size) }}
|
||||
</span>
|
||||
|
||||
<span class="last-modified item">
|
||||
{{ formatLastModified(imgItem.fileInfo.lastModified) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="bottom rename-operation"
|
||||
v-if="
|
||||
!imgItem.uploadStatus.uploading && imgItem.uploadStatus.progress !== 100
|
||||
"
|
||||
>
|
||||
<!-- 哈希化 -->
|
||||
<el-checkbox
|
||||
label="哈希化"
|
||||
v-model="imgItem.filename.isHashRename"
|
||||
@change="hashRename($event, imgItem)"
|
||||
></el-checkbox>
|
||||
|
||||
<!-- 重命名 -->
|
||||
<el-checkbox
|
||||
label="重命名"
|
||||
v-model="imgItem.filename.isRename"
|
||||
@change="rename($event, imgItem)"
|
||||
></el-checkbox>
|
||||
<el-input
|
||||
class="rename-input"
|
||||
size="small"
|
||||
v-if="imgItem.filename.isRename"
|
||||
v-model="imgItem.filename.newName"
|
||||
@input="rename($event, imgItem)"
|
||||
clearable
|
||||
></el-input>
|
||||
|
||||
<!-- 命名前缀 -->
|
||||
<el-checkbox
|
||||
label="命名前缀"
|
||||
v-if="
|
||||
!imgItem.filename.isRename &&
|
||||
userConfigInfo.defaultPrefix &&
|
||||
userConfigInfo.prefixName
|
||||
"
|
||||
v-model="imgItem.filename.isPrefix"
|
||||
@change="prefixName($event, imgItem)"
|
||||
></el-checkbox>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="bottom rename-operation"
|
||||
v-if="
|
||||
!imgItem.uploadStatus.uploading && imgItem.uploadStatus.progress === 100
|
||||
"
|
||||
>
|
||||
<copy-externalLink :img-obj="imgItem.uploadedImg" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="upload-status-box"
|
||||
:class="{
|
||||
'wait-upload':
|
||||
!imgItem.uploadStatus.uploading && imgItem.uploadStatus.progress !== 100,
|
||||
uploading:
|
||||
imgItem.uploadStatus.uploading && imgItem.uploadStatus.progress !== 100,
|
||||
uploaded:
|
||||
!imgItem.uploadStatus.uploading && imgItem.uploadStatus.progress === 100
|
||||
}"
|
||||
>
|
||||
<el-icon
|
||||
v-if="
|
||||
!imgItem.uploadStatus.uploading && imgItem.uploadStatus.progress !== 100
|
||||
"
|
||||
><Upload
|
||||
/></el-icon>
|
||||
|
||||
<el-icon
|
||||
class="is-loading"
|
||||
v-if="
|
||||
imgItem.uploadStatus.uploading && imgItem.uploadStatus.progress !== 100
|
||||
"
|
||||
><Loading
|
||||
/></el-icon>
|
||||
|
||||
<el-icon
|
||||
v-if="
|
||||
!imgItem.uploadStatus.uploading && imgItem.uploadStatus.progress === 100
|
||||
"
|
||||
><Check
|
||||
/></el-icon>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="remove-to-upload-image"
|
||||
v-if="
|
||||
imgItem.uploadStatus.progress !== 100 && !imgItem.uploadStatus.uploading
|
||||
"
|
||||
@click="removeToUploadImage(imgItem)"
|
||||
>
|
||||
<el-tooltip effect="dark" content="移除" placement="top">
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, reactive, toRefs, onMounted } from 'vue'
|
||||
import { useStore } from '@/store'
|
||||
import { getFileSize } from '@/utils/file-handle-helper'
|
||||
import { UserConfigInfoModel } from '@/common/model/user-config-info.model'
|
||||
import { ToUploadImageModel, UploadStatusEnum } from '@/common/model/upload.model'
|
||||
import TimeHelper from '@/utils/time-helper'
|
||||
import copyExternalLink from '@/components/copy-external-link/copy-external-link.vue'
|
||||
import selectedInfoBar from '@/components/selected-info-bar/selected-info-bar.vue'
|
||||
import { uploadImage_single } from '@/utils/upload-helper'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'to-upload-image-card',
|
||||
|
||||
components: {
|
||||
copyExternalLink,
|
||||
selectedInfoBar
|
||||
},
|
||||
|
||||
props: {
|
||||
loadingAllImage: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
setup() {
|
||||
const store = useStore()
|
||||
|
||||
const reactiveData = reactive({
|
||||
isShowDialog: false,
|
||||
curImgInfo: {
|
||||
size: ''
|
||||
},
|
||||
|
||||
userConfigInfo: computed(() => store.getters.getUserConfigInfo).value,
|
||||
userSettings: computed(() => store.getters.getUserSettings).value,
|
||||
toUploadImage: computed(() => store.getters.getToUploadImage).value,
|
||||
|
||||
hashRename(e: boolean, img: any) {
|
||||
if (e) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
img.filename.now = `${img.filename.name}.${img.filename.hash}.${img.filename.suffix}`
|
||||
} else {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
img.filename.now = `${img.filename.name}.${img.filename.suffix}`
|
||||
}
|
||||
},
|
||||
|
||||
prefixName(e: boolean, img: any) {
|
||||
if (e) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
img.filename.name = `${img.filename.prefixName}${img.filename.initName}`
|
||||
} else {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
img.filename.name = `${img.filename.initName}`
|
||||
}
|
||||
if (img.filename.isHashRename) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
img.filename.now = `${img.filename.name}.${img.filename.hash}.${img.filename.suffix}`
|
||||
} else {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
img.filename.now = `${img.filename.name}.${img.filename.suffix}`
|
||||
}
|
||||
},
|
||||
|
||||
rename(e: boolean, img: any) {
|
||||
if (e) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
img.filename.name = img.filename.newName.trim().replace(/\s+/g, '-')
|
||||
} else {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
reactiveData.prefixName(img.filename.isPrefix, img) // 恢复列表prefix选项
|
||||
}
|
||||
|
||||
if (img.filename.isHashRename) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
img.filename.now = `${img.filename.name}.${img.filename.hash}.${img.filename.suffix}`
|
||||
} else {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
img.filename.now = `${img.filename.name}.${img.filename.suffix}`
|
||||
}
|
||||
},
|
||||
|
||||
getFileSize(size: number) {
|
||||
return `${getFileSize(size)} KB`
|
||||
},
|
||||
|
||||
formatLastModified(t: number) {
|
||||
return TimeHelper.formatTimestamp(t)
|
||||
},
|
||||
|
||||
async uploadImage_all(userConfigInfo: UserConfigInfoModel) {
|
||||
const uploadIndex = this.toUploadImage.uploadedNumber
|
||||
|
||||
if (uploadIndex >= this.toUploadImage.list.length) {
|
||||
return UploadStatusEnum.uploaded
|
||||
}
|
||||
|
||||
if (
|
||||
await uploadImage_single(userConfigInfo, this.toUploadImage.list[uploadIndex])
|
||||
) {
|
||||
if (uploadIndex < this.toUploadImage.list.length) {
|
||||
await this.uploadImage_all(userConfigInfo)
|
||||
return UploadStatusEnum.allUploaded
|
||||
}
|
||||
return UploadStatusEnum.uploaded
|
||||
}
|
||||
return UploadStatusEnum.uploadFail
|
||||
}
|
||||
})
|
||||
|
||||
const removeToUploadImage = (imgItem: ToUploadImageModel) => {
|
||||
store.dispatch('TO_UPLOAD_IMAGE_LIST_REMOVE', imgItem.uuid)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const {
|
||||
defaultHash: isHash,
|
||||
defaultPrefix: isPrefix,
|
||||
prefixName
|
||||
} = reactiveData.userSettings
|
||||
reactiveData.toUploadImage.list.forEach((v: ToUploadImageModel) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
v.filename.isPrefix = isPrefix
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
v.filename.prefixName = prefixName
|
||||
reactiveData.prefixName(isPrefix, v)
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
v.filename.isHashRename = isHash
|
||||
reactiveData.hashRename(isHash, v)
|
||||
})
|
||||
})
|
||||
|
||||
return {
|
||||
...toRefs(reactiveData),
|
||||
removeToUploadImage
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
@import "to-upload-image-card.styl"
|
||||
</style>
|
||||
40
picx/src/components/tutorials-step/step1.vue
Normal file
40
picx/src/components/tutorials-step/step1.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div class="tutorials-step-1">
|
||||
<h3>
|
||||
创建一个用来存储图片的
|
||||
<span class="go-create-repo" @click="goCreateRepo"> GitHub 仓库</span>
|
||||
</h3>
|
||||
<img
|
||||
src="https://cdn.staticaly.com/gh/XPoet/image-hosting@master/PicX/image.j1486dtk68n.png"
|
||||
alt="Create GitHub Repository"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
function goCreateRepo() {
|
||||
window.open('https://github.com/new')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
|
||||
.tutorials-step-1 {
|
||||
|
||||
width 800rem
|
||||
|
||||
.go-create-repo {
|
||||
cursor pointer
|
||||
color #1c81e9
|
||||
|
||||
&:hover {
|
||||
color #085fb8
|
||||
border-bottom 1rem solid #085fb8
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
width 100%
|
||||
}
|
||||
}
|
||||
</style>
|
||||
54
picx/src/components/tutorials-step/step2.vue
Normal file
54
picx/src/components/tutorials-step/step2.vue
Normal file
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<div class="tutorials-step-2">
|
||||
<h3>
|
||||
创建一个有 repo 权限的
|
||||
<span class="go-create-token" @click="goCreateToken"> GitHub Token</span>
|
||||
</h3>
|
||||
<img
|
||||
src="https://cdn.staticaly.com/gh/XPoet/image-hosting@master/PicX/image.lpt1xl9fu.png"
|
||||
alt="Create GitHub Token"
|
||||
/>
|
||||
<p>然后点击 Generate token 按钮,即可生成一个token,如下图:</p>
|
||||
<img
|
||||
src="https://cdn.staticaly.com/gh/XPoet/image-hosting@master/PicX/image.pzmcp6b80fk.png"
|
||||
alt="token-demo"
|
||||
/>
|
||||
<p style="color: red">
|
||||
<em>新生成的 Token 只会显示一次,请妥善保存!如有遗失,重新生成即可。</em>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
function goCreateToken() {
|
||||
window.open('https://github.com/settings/tokens/new')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
|
||||
.tutorials-step-2 {
|
||||
|
||||
width 800rem
|
||||
|
||||
.go-create-token {
|
||||
cursor pointer
|
||||
color #1c81e9
|
||||
|
||||
&:hover {
|
||||
color #085fb8
|
||||
border-bottom 1rem solid #085fb8
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
img {
|
||||
width 100%
|
||||
}
|
||||
|
||||
p {
|
||||
font-weight bold
|
||||
padding-top 20rem
|
||||
}
|
||||
}
|
||||
</style>
|
||||
62
picx/src/components/tutorials-step/step3.vue
Normal file
62
picx/src/components/tutorials-step/step3.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<div class="tutorials-step-3">
|
||||
<h3>进行图床配置(绑定 GitHub Token、存储图片的仓库和目录)</h3>
|
||||
<p>1、填写 Token,自动获取该用户下的仓库</p>
|
||||
<img
|
||||
src="https://cdn.staticaly.com/gh/XPoet/image-hosting@master/PicX/image.4g8q5m7c8sq0.png"
|
||||
/>
|
||||
<br />
|
||||
<p>2、在仓库的下拉列表中,选择一个作为图床的仓库</p>
|
||||
<img
|
||||
src="https://cdn.staticaly.com/gh/XPoet/image-hosting@master/PicX/image.746g75olruk0.png"
|
||||
/>
|
||||
<br />
|
||||
<p>3、选择一种目录方式(目录即仓库里存放图片的文件夹)</p>
|
||||
<img
|
||||
src="https://cdn.staticaly.com/gh/XPoet/image-hosting@master/PicX/image.5ydmhgxjhgo0.png"
|
||||
/>
|
||||
|
||||
<div class="dir-desc-box">
|
||||
<ul>
|
||||
<li>新建目录:需手动输入一个新目录。</li>
|
||||
<li>根目录:图片将直接存储在仓库根目录下。</li>
|
||||
<li>自动目录:自动生成日期格式 YYYYMMDD 的目录。例如:20200909</li>
|
||||
<li>选择仓库目录:自动获取仓库下所有目录,选择一个即可。</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="stylus">
|
||||
|
||||
.tutorials-step-3 {
|
||||
|
||||
width 800rem
|
||||
|
||||
img {
|
||||
width 100%
|
||||
}
|
||||
|
||||
p {
|
||||
font-weight bold
|
||||
padding-top 20rem
|
||||
}
|
||||
|
||||
|
||||
.dir-desc-box {
|
||||
display flex
|
||||
justify-content center
|
||||
|
||||
ul {
|
||||
width 100%
|
||||
padding 0
|
||||
|
||||
li {
|
||||
padding-top 10rem
|
||||
text-align left
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
57
picx/src/components/upload-area/upload-area.styl
Normal file
57
picx/src/components/upload-area/upload-area.styl
Normal file
@@ -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%;
|
||||
}
|
||||
|
||||
}
|
||||
148
picx/src/components/upload-area/upload-area.vue
Normal file
148
picx/src/components/upload-area/upload-area.vue
Normal file
@@ -0,0 +1,148 @@
|
||||
<template>
|
||||
<div
|
||||
class="upload-area active-upload"
|
||||
:class="{ focus: uploadAreaActive }"
|
||||
@dragover.prevent
|
||||
@drop.stop.prevent="onDrop"
|
||||
@paste="onPaste"
|
||||
v-loading="imageLoading"
|
||||
element-loading-text="图片上传中..."
|
||||
element-loading-background="rgba(0, 0, 0, 0.5)"
|
||||
>
|
||||
<label for="uploader" class="active-upload" v-if="uploadAreaActive"></label>
|
||||
<input id="uploader" type="file" @change="onSelect" multiple="multiple" />
|
||||
<div class="tips active-upload" v-if="!toUploadImage.curImgBase64Url">
|
||||
<el-icon class="icon active-upload"><UploadFilled /></el-icon>
|
||||
<div class="text active-upload">拖拽、粘贴、或点击此处上传</div>
|
||||
</div>
|
||||
<img
|
||||
class="active-upload"
|
||||
v-if="toUploadImage.curImgBase64Url"
|
||||
:src="toUploadImage.curImgBase64Url"
|
||||
alt="Pictures to be uploaded"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, reactive, toRefs } from 'vue'
|
||||
import { useStore } from '@/store'
|
||||
import { filenameHandle } from '@/utils/file-handle-helper'
|
||||
import selectedFileHandle, { handleResult } from '@/utils/selected-file-handle'
|
||||
import createToUploadImageObject from '@/utils/create-to-upload-image'
|
||||
import paste from '@/utils/paste'
|
||||
import Upload from '@/views/upload/upload.vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'upload-area',
|
||||
props: {
|
||||
imageLoading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
setup() {
|
||||
const store = useStore()
|
||||
|
||||
const reactiveData = reactive({
|
||||
userConfigInfo: computed(() => store.getters.getUserConfigInfo).value,
|
||||
userSettings: computed(() => store.getters.getUserSettings).value,
|
||||
uploadAreaActive: computed((): boolean => store.getters.getUploadAreaActive),
|
||||
uploadSettings: computed(() => store.getters.getUploadSettings).value,
|
||||
toUploadImage: computed(() => store.getters.getToUploadImage).value,
|
||||
|
||||
// 选择图片
|
||||
onSelect(e: any) {
|
||||
store.commit('CHANGE_UPLOAD_AREA_ACTIVE', true)
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const file of e.target.files) {
|
||||
selectedFileHandle(file, this.uploadSettings.imageMaxSize)?.then((result) => {
|
||||
if (!result) {
|
||||
return
|
||||
}
|
||||
const { base64, originalFile, compressFile } = result
|
||||
this.getImage(base64, originalFile, compressFile)
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// 拖拽图片
|
||||
onDrop(e: any) {
|
||||
store.commit('CHANGE_UPLOAD_AREA_ACTIVE', true)
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const file of e.dataTransfer.files) {
|
||||
selectedFileHandle(file, this.uploadSettings.imageMaxSize)?.then((result) => {
|
||||
if (!result) {
|
||||
return
|
||||
}
|
||||
const { base64, originalFile, compressFile } = result
|
||||
this.getImage(base64, originalFile, compressFile)
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// 复制图片
|
||||
async onPaste(e: any) {
|
||||
const { base64, originalFile, compressFile }: handleResult = await paste(
|
||||
e,
|
||||
this.uploadSettings.imageMaxSize
|
||||
)
|
||||
this.getImage(base64, originalFile, compressFile)
|
||||
},
|
||||
|
||||
// 获取图片对象
|
||||
getImage(base64Data: string, originFile: File, compressFile?: File) {
|
||||
if (
|
||||
this.toUploadImage.list.length === this.toUploadImage.uploadedNumber &&
|
||||
this.toUploadImage.list.length > 0 &&
|
||||
this.toUploadImage.uploadedNumber > 0
|
||||
) {
|
||||
store.dispatch('TO_UPLOAD_IMAGE_CLEAN_LIST')
|
||||
store.dispatch('TO_UPLOAD_IMAGE_CLEAN_UPLOADED_NUMBER')
|
||||
}
|
||||
|
||||
const { defaultHash, isCompress, defaultPrefix, prefixName } = this.userSettings
|
||||
const file = isCompress ? compressFile : originFile
|
||||
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.compressedSize = compressFile?.size
|
||||
curImg.fileInfo.originSize = originFile.size
|
||||
curImg.fileInfo.size = file?.size
|
||||
curImg.fileInfo.lastModified = file?.lastModified
|
||||
|
||||
curImg.filename.initName = name
|
||||
curImg.filename.name = defaultPrefix ? `${prefixName}${name}` : name
|
||||
curImg.filename.prefixName = prefixName
|
||||
curImg.filename.hash = hash
|
||||
curImg.filename.suffix = suffix
|
||||
curImg.filename.now = defaultHash
|
||||
? `${curImg.filename.name}.${hash}.${suffix}`
|
||||
: `${curImg.filename.name}.${suffix}`
|
||||
curImg.filename.isHashRename = defaultHash
|
||||
curImg.filename.isPrefix = defaultPrefix
|
||||
|
||||
store.dispatch('TO_UPLOAD_IMAGE_LIST_ADD', JSON.parse(JSON.stringify(curImg)))
|
||||
store.dispatch('TO_UPLOAD_IMAGE_SET_CURRENT', {
|
||||
uuid: hash,
|
||||
base64Url: base64Data
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
...toRefs(reactiveData)
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="stylus">
|
||||
@import "upload-area.styl"
|
||||
</style>
|
||||
21
picx/src/main.ts
Normal file
21
picx/src/main.ts
Normal file
@@ -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')
|
||||
32
picx/src/plugins/index.ts
Normal file
32
picx/src/plugins/index.ts
Normal file
@@ -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
|
||||
}
|
||||
24
picx/src/plugins/pwa.ts
Normal file
24
picx/src/plugins/pwa.ts
Normal file
@@ -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'
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
}
|
||||
82
picx/src/router/index.ts
Normal file
82
picx/src/router/index.ts
Normal file
@@ -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<RouteRecordRaw> = [
|
||||
{
|
||||
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) (<any>window).document.title = to.meta.title
|
||||
if (from.path === '/management') {
|
||||
store.dispatch('USER_CONFIG_INFO_RESET')
|
||||
}
|
||||
next()
|
||||
})
|
||||
|
||||
export default router
|
||||
6
picx/src/shims-vue.d.ts
vendored
Normal file
6
picx/src/shims-vue.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
declare module '*.vue' {
|
||||
import { DefineComponent } from 'vue'
|
||||
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
57
picx/src/store/index.ts
Normal file
57
picx/src/store/index.ts
Normal file
@@ -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<RootStateTypes>({
|
||||
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<Store<RootStateTypes>> = Symbol('vuex-store')
|
||||
|
||||
export function useStore<T = AllStateTypes>() {
|
||||
return baseUseStore<T>(key)
|
||||
}
|
||||
260
picx/src/store/modules/dir-image-list/index.ts
Normal file
260
picx/src/store/modules/dir-image-list/index.ts
Normal file
@@ -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<DirImageListStateTypes, RootStateTypes> = {
|
||||
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
|
||||
14
picx/src/store/modules/dir-image-list/types.ts
Normal file
14
picx/src/store/modules/dir-image-list/types.ts
Normal file
@@ -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
|
||||
}
|
||||
73
picx/src/store/modules/dir-image-list/utils.ts
Normal file
73
picx/src/store/modules/dir-image-list/utils.ts
Normal file
@@ -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
|
||||
}
|
||||
41
picx/src/store/modules/image-card/index.ts
Normal file
41
picx/src/store/modules/image-card/index.ts
Normal file
@@ -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<ImageCardStateTypes, RootStateTypes> = {
|
||||
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
|
||||
5
picx/src/store/modules/image-card/types.ts
Normal file
5
picx/src/store/modules/image-card/types.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { UploadedImageModel } from '@/common/model/upload.model'
|
||||
|
||||
export interface ImageCardStateTypes {
|
||||
imgCardArr: UploadedImageModel[]
|
||||
}
|
||||
29
picx/src/store/modules/image-viewer/index.ts
Normal file
29
picx/src/store/modules/image-viewer/index.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Module } from 'vuex'
|
||||
import ImageViewerStateTypes from './types'
|
||||
import RootStateTypes from '../../types'
|
||||
|
||||
const imageViewerModule: Module<ImageViewerStateTypes, RootStateTypes> = {
|
||||
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
|
||||
12
picx/src/store/modules/image-viewer/types.ts
Normal file
12
picx/src/store/modules/image-viewer/types.ts
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
92
picx/src/store/modules/to-upload-image/index.ts
Normal file
92
picx/src/store/modules/to-upload-image/index.ts
Normal file
@@ -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<ToUploadImageStateTypes, RootStateTypes> = {
|
||||
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
|
||||
6
picx/src/store/modules/to-upload-image/types.ts
Normal file
6
picx/src/store/modules/to-upload-image/types.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export default interface ToUploadImageStateTypes {
|
||||
curImgBase64Url: string
|
||||
curImgUuid: string
|
||||
list: any[]
|
||||
uploadedNumber: number
|
||||
}
|
||||
25
picx/src/store/modules/upload-area-active/index.ts
Normal file
25
picx/src/store/modules/upload-area-active/index.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Module } from 'vuex'
|
||||
import UploadAreaActiveStateTypes from './types'
|
||||
import RootStateTypes from '../../types'
|
||||
|
||||
const uploadAreaActiveModule: Module<UploadAreaActiveStateTypes, RootStateTypes> = {
|
||||
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
|
||||
3
picx/src/store/modules/upload-area-active/types.ts
Normal file
3
picx/src/store/modules/upload-area-active/types.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default interface UploadAreaActiveStateTypes {
|
||||
uploadAreaActive: boolean
|
||||
}
|
||||
24
picx/src/store/modules/upload-settings/index.ts
Normal file
24
picx/src/store/modules/upload-settings/index.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Module } from 'vuex'
|
||||
import UploadAreaActiveStateTypes from './types'
|
||||
import RootStateTypes from '../../types'
|
||||
|
||||
const uploadSettingsModule: Module<UploadAreaActiveStateTypes, RootStateTypes> = {
|
||||
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
|
||||
6
picx/src/store/modules/upload-settings/types.ts
Normal file
6
picx/src/store/modules/upload-settings/types.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export default interface UploadSettingsStateTypes {
|
||||
uploadSettings: {
|
||||
isSetMaxSize: boolean
|
||||
imageMaxSize: number
|
||||
}
|
||||
}
|
||||
53
picx/src/store/modules/uploaded-image-list/index.ts
Normal file
53
picx/src/store/modules/uploaded-image-list/index.ts
Normal file
@@ -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<UploadedImageListStateTypes, RootStateTypes> = {
|
||||
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
|
||||
5
picx/src/store/modules/uploaded-image-list/types.ts
Normal file
5
picx/src/store/modules/uploaded-image-list/types.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { UploadedImageModel } from '@/common/model/upload.model'
|
||||
|
||||
export default interface UploadedImageListStateTypes {
|
||||
uploadedImageList: UploadedImageModel[]
|
||||
}
|
||||
155
picx/src/store/modules/user-config-info/index.ts
Normal file
155
picx/src/store/modules/user-config-info/index.ts
Normal file
@@ -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<UserConfigInfoStateTypes, RootStateTypes> = {
|
||||
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
|
||||
5
picx/src/store/modules/user-config-info/types.ts
Normal file
5
picx/src/store/modules/user-config-info/types.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { UserConfigInfoModel } from '@/common/model/user-config-info.model'
|
||||
|
||||
export default interface UserConfigInfoStateTypes {
|
||||
userConfigInfo: UserConfigInfoModel
|
||||
}
|
||||
67
picx/src/store/modules/user-settings/index.ts
Normal file
67
picx/src/store/modules/user-settings/index.ts
Normal file
@@ -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<UserSettingsStateTypes, RootStateTypes> = {
|
||||
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
|
||||
5
picx/src/store/modules/user-settings/types.ts
Normal file
5
picx/src/store/modules/user-settings/types.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { UserSettingsModel } from '@/common/model/user-settings.model'
|
||||
|
||||
export default interface UserSettingsStateTypes {
|
||||
userSettings: UserSettingsModel
|
||||
}
|
||||
21
picx/src/store/types.ts
Normal file
21
picx/src/store/types.ts
Normal file
@@ -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
|
||||
}
|
||||
98
picx/src/style/base.styl
Normal file
98
picx/src/style/base.styl
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
25
picx/src/style/theme.styl
Normal file
25
picx/src/style/theme.styl
Normal file
@@ -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')
|
||||
}
|
||||
110
picx/src/style/variables.styl
Normal file
110
picx/src/style/variables.styl
Normal file
@@ -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;
|
||||
}
|
||||
|
||||
51
picx/src/utils/axios.ts
Normal file
51
picx/src/utils/axios.ts
Normal file
@@ -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
|
||||
27
picx/src/utils/common-utils.ts
Normal file
27
picx/src/utils/common-utils.ts
Normal file
@@ -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
|
||||
}
|
||||
26
picx/src/utils/compress.ts
Normal file
26
picx/src/utils/compress.ts
Normal file
@@ -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()
|
||||
}
|
||||
42
picx/src/utils/create-to-upload-image.ts
Normal file
42
picx/src/utils/create-to-upload-image.ts
Normal file
@@ -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: ''
|
||||
}
|
||||
}
|
||||
}
|
||||
64
picx/src/utils/delete-image-card.ts
Normal file
64
picx/src/utils/delete-image-card.ts
Normal file
@@ -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<boolean> {
|
||||
// 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<UploadedImageModel>,
|
||||
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
|
||||
}
|
||||
23
picx/src/utils/env.ts
Normal file
23
picx/src/utils/env.ts
Normal file
@@ -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
|
||||
}
|
||||
178
picx/src/utils/external-link-handler.ts
Normal file
178
picx/src/utils/external-link-handler.ts
Normal file
@@ -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 ``
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制图片外链
|
||||
* @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<UploadedImageModel>,
|
||||
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('请先选择图片')
|
||||
}
|
||||
}
|
||||
50
picx/src/utils/file-handle-helper.ts
Normal file
50
picx/src/utils/file-handle-helper.ts
Normal file
@@ -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: ''
|
||||
}
|
||||
}
|
||||
42
picx/src/utils/image-helper.ts
Normal file
42
picx/src/utils/image-helper.ts
Normal file
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
59
picx/src/utils/object-helper.ts
Normal file
59
picx/src/utils/object-helper.ts
Normal file
@@ -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])
|
||||
}
|
||||
}
|
||||
}
|
||||
28
picx/src/utils/paste.ts
Normal file
28
picx/src/utils/paste.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import selectedFileHandle from './selected-file-handle'
|
||||
|
||||
const onPaste = (e: any, maxsize: number): Promise<any> | 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
|
||||
3
picx/src/utils/register-sw.ts
Normal file
3
picx/src/utils/register-sw.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { registerSW } from 'virtual:pwa-register'
|
||||
|
||||
registerSW()
|
||||
56
picx/src/utils/rename-image.ts
Normal file
56
picx/src/utils/rename-image.ts
Normal file
@@ -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<string | null> {
|
||||
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
|
||||
}
|
||||
67
picx/src/utils/selected-file-handle.ts
Normal file
67
picx/src/utils/selected-file-handle.ts
Normal file
@@ -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<handleResult | null> => {
|
||||
if (!file) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!isImage(file.type)) {
|
||||
ElMessage.error('该文件格式不支持!')
|
||||
return null
|
||||
}
|
||||
let compressFile: NonNullable<File>
|
||||
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<FileReader>) => {
|
||||
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
|
||||
51
picx/src/utils/set-theme-mode.ts
Normal file
51
picx/src/utils/set-theme-mode.ts
Normal file
@@ -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
|
||||
26
picx/src/utils/time-helper.ts
Normal file
26
picx/src/utils/time-helper.ts
Normal file
@@ -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)}`
|
||||
}
|
||||
}
|
||||
134
picx/src/utils/upload-helper.ts
Normal file
134
picx/src/utils/upload-helper.ts
Normal file
@@ -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<Boolean> {
|
||||
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)
|
||||
}
|
||||
25
picx/src/views/about/about.styl
Normal file
25
picx/src/views/about/about.styl
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
77
picx/src/views/about/about.vue
Normal file
77
picx/src/views/about/about.vue
Normal file
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<div class="page-container feedback-page-container">
|
||||
<div class="help-info-item description">
|
||||
PicX 是一款基于 GitHub API & jsDelivr 开发的具有 CDN 加速功能的图床工具。
|
||||
<br />
|
||||
无需下载!无需安装!打开网站即用!免费!极速!稳定!
|
||||
</div>
|
||||
|
||||
<div class="help-info-item">建议将本站添加至浏览器收藏夹,方便下次使用 😊</div>
|
||||
|
||||
<div class="help-info-item">
|
||||
作者:
|
||||
<el-link type="primary" href="https://xpoet.cn/" target="_blank">@XPoet</el-link>
|
||||
</div>
|
||||
|
||||
<div class="help-info-item">
|
||||
仓库:
|
||||
<el-link type="primary" href="https://github.com/XPoet/picx" target="_blank">
|
||||
https://github.com/XPoet/picx
|
||||
</el-link>
|
||||
</div>
|
||||
|
||||
<div class="help-info-item">
|
||||
教程:
|
||||
<el-link
|
||||
type="primary"
|
||||
href="https://github.com/XPoet/picx/blob/master/README.md"
|
||||
target="_blank"
|
||||
>
|
||||
https://github.com/XPoet/picx/blob/master/README.md
|
||||
</el-link>
|
||||
</div>
|
||||
|
||||
<div class="help-info-item">
|
||||
在使用过程中遇到问题,请仔细阅读文档,或者给作者提
|
||||
<el-link
|
||||
type="primary"
|
||||
style="font-size: 16rem"
|
||||
href="https://github.com/XPoet/picx/issues"
|
||||
target="_blank"
|
||||
>
|
||||
Issue
|
||||
</el-link>
|
||||
。
|
||||
</div>
|
||||
|
||||
<div class="help-info-item red-text">
|
||||
图片上传缓慢或加载不出来等情况,可借助
|
||||
<el-link
|
||||
style="font-size: 16rem"
|
||||
type="primary"
|
||||
href="https://github.com/Alvin9999/new-pac/wiki"
|
||||
target="_blank"
|
||||
>VPN 工具
|
||||
</el-link>
|
||||
。
|
||||
</div>
|
||||
|
||||
<div class="help-info-item">
|
||||
<strong>
|
||||
郑重声明:请勿通过本站上传违反你当地法律的图片,所造成的一切后果与本站无关。
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'about'
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="stylus">
|
||||
@import "about.styl"
|
||||
</style>
|
||||
13
picx/src/views/config/config.styl
Normal file
13
picx/src/views/config/config.styl
Normal file
@@ -0,0 +1,13 @@
|
||||
.config-page-container {
|
||||
.operation {
|
||||
text-align right
|
||||
|
||||
.el-button {
|
||||
margin-left 20rem
|
||||
|
||||
&:first-child {
|
||||
margin-left 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
498
picx/src/views/config/config.vue
Normal file
498
picx/src/views/config/config.vue
Normal file
@@ -0,0 +1,498 @@
|
||||
<template>
|
||||
<div class="page-container config-page-container">
|
||||
<!-- Token -->
|
||||
<el-form label-width="70rem" :label-position="labelPosition" class="config-form">
|
||||
<el-form-item label="Token">
|
||||
<el-input
|
||||
v-model="userConfigInfo.token"
|
||||
clearable
|
||||
:autofocus="!userConfigInfo.token"
|
||||
type="password"
|
||||
show-password
|
||||
placeholder="请输入 GitHub Token"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item class="operation">
|
||||
<el-button
|
||||
plain
|
||||
type="primary"
|
||||
native-type="submit"
|
||||
@click.prevent="getUserInfo()"
|
||||
>
|
||||
确认 Token
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 基本信息 -->
|
||||
<el-form
|
||||
label-width="70rem"
|
||||
:label-position="labelPosition"
|
||||
v-if="userConfigInfo.token"
|
||||
v-loading="loading"
|
||||
element-loading-text="加载中..."
|
||||
>
|
||||
<el-form-item v-if="userConfigInfo.owner" label="用户名">
|
||||
<el-input v-model="userConfigInfo.owner" readonly></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="userConfigInfo.email" label="邮箱">
|
||||
<el-input v-model="userConfigInfo.email" readonly></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="userConfigInfo.reposList.length" label="选择仓库">
|
||||
<el-select
|
||||
v-model="userConfigInfo.selectedRepos"
|
||||
filterable
|
||||
style="width: 100%"
|
||||
placeholder="请选择图床仓库..."
|
||||
@change="selectRepos"
|
||||
>
|
||||
<el-option
|
||||
v-for="(repos, index) in userConfigInfo.reposList"
|
||||
:key="index"
|
||||
:label="repos.label"
|
||||
:value="repos.value"
|
||||
>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 分支 -->
|
||||
<el-form
|
||||
label-width="70rem"
|
||||
:label-position="labelPosition"
|
||||
v-if="userConfigInfo.selectedRepos && userConfigInfo.branchList.length"
|
||||
v-loading="branchLoading"
|
||||
element-loading-text="加载中..."
|
||||
>
|
||||
<!-- 因未验证 API 是否能创建空分支,暂时不开启分支选择方式 && 0 -->
|
||||
<el-form-item v-if="userConfigInfo.selectedRepos && 0" label="分支方式">
|
||||
<el-radio-group v-model="userConfigInfo.branchMode" @change="branchModeChange">
|
||||
<el-tooltip
|
||||
v-if="userConfigInfo.branchList.length"
|
||||
:content="'选择 ' + userConfigInfo.selectedRepos + ' 仓库下的一个分支'"
|
||||
placement="top"
|
||||
>
|
||||
<el-radio label="reposBranch">
|
||||
选择 {{ userConfigInfo.selectedRepos }} 仓库下的分支
|
||||
</el-radio>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="手动创建一个新分支" placement="top">
|
||||
<el-radio label="newBranch">新建分支</el-radio>
|
||||
</el-tooltip>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item
|
||||
v-if="
|
||||
userConfigInfo.branchList.length > 1 &&
|
||||
userConfigInfo.branchMode === 'reposBranch'
|
||||
"
|
||||
label="选择分支"
|
||||
>
|
||||
<el-select
|
||||
v-model="userConfigInfo.selectedBranch"
|
||||
filterable
|
||||
style="width: 100%"
|
||||
placeholder="请选择分支..."
|
||||
@change="selectBranch"
|
||||
>
|
||||
<el-option
|
||||
v-for="(repos, reposIndex) in userConfigInfo.branchList"
|
||||
:key="reposIndex"
|
||||
:label="repos.label"
|
||||
:value="repos.value"
|
||||
>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="userConfigInfo.branchMode === 'newBranch'" label="新建分支">
|
||||
<el-input
|
||||
v-model="userConfigInfo.selectedBranch"
|
||||
@input="persistUserConfigInfo()"
|
||||
clearable
|
||||
placeholder="请输入新建的分支..."
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 目录 -->
|
||||
<el-form
|
||||
label-width="70rem"
|
||||
:label-position="labelPosition"
|
||||
v-if="userConfigInfo.selectedBranch"
|
||||
v-loading="dirLoading"
|
||||
element-loading-text="加载中..."
|
||||
>
|
||||
<el-form-item v-if="userConfigInfo.selectedBranch" label="目录方式">
|
||||
<el-radio-group v-model="userConfigInfo.dirMode" @change="dirModeChange">
|
||||
<el-tooltip content="手动输入一个新目录" placement="top" :offset="-1">
|
||||
<el-radio label="newDir">新建目录</el-radio>
|
||||
</el-tooltip>
|
||||
|
||||
<el-tooltip
|
||||
:content="'图片存储在 ' + userConfigInfo.selectedBranch + ' 分支的根目录下'"
|
||||
placement="top"
|
||||
:offset="-1"
|
||||
>
|
||||
<el-radio label="rootDir">根目录</el-radio>
|
||||
</el-tooltip>
|
||||
|
||||
<el-tooltip
|
||||
:content="'根据日期自动创建格式 YYYYMMDD 的目录'"
|
||||
placement="top"
|
||||
:offset="-1"
|
||||
>
|
||||
<el-radio label="autoDir">自动目录</el-radio>
|
||||
</el-tooltip>
|
||||
|
||||
<el-tooltip
|
||||
v-if="
|
||||
userConfigInfo.dirList.length && userConfigInfo.branchMode !== 'newBranch'
|
||||
"
|
||||
:content="'选择 ' + userConfigInfo.selectedBranch + ' 分支下的一个目录'"
|
||||
placement="top"
|
||||
:offset="-1"
|
||||
>
|
||||
<el-radio label="reposDir">
|
||||
选择 {{ userConfigInfo.selectedRepos }} 仓库目录
|
||||
</el-radio>
|
||||
</el-tooltip>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="userConfigInfo.dirMode === 'autoDir'" label="自动目录">
|
||||
<el-input v-model="userConfigInfo.selectedDir" readonly></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="userConfigInfo.dirMode === 'rootDir'" label="根目录">
|
||||
<el-input v-model="userConfigInfo.selectedDir" readonly></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="userConfigInfo.dirMode === 'newDir'" label="新建目录">
|
||||
<el-input
|
||||
v-model="userConfigInfo.selectedDir"
|
||||
@input="persistUserConfigInfo()"
|
||||
clearable
|
||||
placeholder="请输入新建的目录..."
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item
|
||||
v-if="
|
||||
userConfigInfo.dirList.length &&
|
||||
userConfigInfo.dirMode === 'reposDir' &&
|
||||
userConfigInfo.branchMode !== 'newBranch'
|
||||
"
|
||||
label="选择目录"
|
||||
>
|
||||
<el-cascader
|
||||
style="width: 100%"
|
||||
:props="cascaderProps"
|
||||
:key="elCascaderKey"
|
||||
v-model="userConfigInfo.selectedDirList"
|
||||
filterable
|
||||
placeholder="请选择一个目录..."
|
||||
clearable
|
||||
@change="cascaderChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 操作(重置、完成配置) -->
|
||||
<el-form label-width="70rem" :label-position="labelPosition">
|
||||
<el-form-item class="operation">
|
||||
<el-button plain type="warning" @click="reset()" v-if="userConfigInfo.owner">
|
||||
重置
|
||||
</el-button>
|
||||
<el-button
|
||||
plain
|
||||
type="success"
|
||||
@click="goUpload"
|
||||
v-if="userConfigInfo.selectedRepos"
|
||||
>
|
||||
完成配置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useStore } from '@/store'
|
||||
import { DirModeEnum } from '@/common/model/dir.model'
|
||||
import { BranchModeEnum } from '@/common/model/user-config-info.model'
|
||||
import axios from '@/utils/axios'
|
||||
import TimeHelper from '@/utils/time-helper'
|
||||
import { getDirListByPath } from '@/common/api'
|
||||
|
||||
const router = useRouter()
|
||||
const store = useStore()
|
||||
|
||||
const userConfigInfo = computed(() => store.getters.getUserConfigInfo).value
|
||||
const loggingStatus = computed(() => store.getters.getUserConfigInfo).value
|
||||
const userSettings = computed(() => store.getters.getUserSettings).value
|
||||
|
||||
const loading = ref(false)
|
||||
const dirLoading = ref(false)
|
||||
const branchLoading = ref(false)
|
||||
|
||||
const labelPosition = computed(() => {
|
||||
return userSettings.elementPlusSize === 'default' ? 'top' : 'right'
|
||||
})
|
||||
|
||||
const elCascaderKey = ref<string>('elCascaderKey')
|
||||
|
||||
function persistUserConfigInfo() {
|
||||
store.dispatch('USER_CONFIG_INFO_PERSIST')
|
||||
}
|
||||
|
||||
function saveUserInfo(res: any) {
|
||||
userConfigInfo.loggingStatus = true
|
||||
userConfigInfo.owner = res.data.login
|
||||
userConfigInfo.name = res.data.name
|
||||
userConfigInfo.email = res.data.email
|
||||
userConfigInfo.avatarUrl = res.data.avatar_url
|
||||
persistUserConfigInfo()
|
||||
}
|
||||
|
||||
function getReposList(reposUrl: string) {
|
||||
axios
|
||||
.get(reposUrl, {
|
||||
params: {
|
||||
type: 'public',
|
||||
sort: 'created',
|
||||
per_page: 100
|
||||
}
|
||||
})
|
||||
.then((res: any) => {
|
||||
console.log('[getReposList] ', res)
|
||||
if (res.status === 200 && res.data.length > 0) {
|
||||
userConfigInfo.reposList = []
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const repos of res.data) {
|
||||
if (!repos.fork && !repos.private) {
|
||||
userConfigInfo.reposList.push({
|
||||
value: repos.name,
|
||||
label: repos.name,
|
||||
desc: repos.description
|
||||
})
|
||||
}
|
||||
}
|
||||
loading.value = false
|
||||
persistUserConfigInfo()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function getDirList() {
|
||||
dirLoading.value = true
|
||||
userConfigInfo.dirList = await getDirListByPath()
|
||||
persistUserConfigInfo()
|
||||
dirLoading.value = false
|
||||
}
|
||||
|
||||
function dirModeChange(dirMode: DirModeEnum) {
|
||||
switch (dirMode) {
|
||||
case DirModeEnum.rootDir:
|
||||
// 根目录
|
||||
userConfigInfo.selectedDir = '/'
|
||||
break
|
||||
|
||||
case DirModeEnum.autoDir:
|
||||
// 自动目录,根据当天日期自动生成
|
||||
userConfigInfo.selectedDir = TimeHelper.getYyyyMmDd()
|
||||
break
|
||||
|
||||
case DirModeEnum.newDir:
|
||||
// 手动输入的新建目录
|
||||
userConfigInfo.selectedDir = 'xxx'
|
||||
break
|
||||
|
||||
case DirModeEnum.reposDir:
|
||||
// 仓库目录
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
const { dirList } = userConfigInfo
|
||||
if (dirList.length) {
|
||||
userConfigInfo.selectedDir = dirList[0].value
|
||||
} else {
|
||||
userConfigInfo.selectedDir = ''
|
||||
}
|
||||
break
|
||||
|
||||
default:
|
||||
userConfigInfo.selectedDir = '/'
|
||||
break
|
||||
}
|
||||
persistUserConfigInfo()
|
||||
}
|
||||
|
||||
function getBranchList(repos: string) {
|
||||
branchLoading.value = true
|
||||
axios.get(`/repos/${userConfigInfo.owner}/${repos}/branches`).then((res: any) => {
|
||||
console.log('[getBranchList] ', res)
|
||||
if (res && res.status === 200) {
|
||||
branchLoading.value = false
|
||||
if (res.data.length > 0) {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const item of res.data) {
|
||||
userConfigInfo.branchList.push({
|
||||
value: item.name,
|
||||
label: item.name
|
||||
})
|
||||
}
|
||||
userConfigInfo.branchList.reverse()
|
||||
userConfigInfo.selectedBranch = userConfigInfo.branchList[0].value
|
||||
userConfigInfo.branchMode = BranchModeEnum.reposBranch
|
||||
getDirList()
|
||||
} else {
|
||||
userConfigInfo.selectedBranch = 'master'
|
||||
userConfigInfo.branchMode = BranchModeEnum.newBranch
|
||||
}
|
||||
dirModeChange(userConfigInfo.dirMode)
|
||||
persistUserConfigInfo()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function getUserInfo() {
|
||||
if (userConfigInfo.token) {
|
||||
loading.value = true
|
||||
axios
|
||||
.get('/user', {
|
||||
headers: { Authorization: `token ${userConfigInfo.token}` }
|
||||
})
|
||||
.then((res: any) => {
|
||||
console.log('[getUserInfo] ', res)
|
||||
if (res && res.status === 200) {
|
||||
saveUserInfo(res)
|
||||
getReposList(res.data.repos_url)
|
||||
} else {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
} else {
|
||||
ElMessage.warning('Token 不能为空!')
|
||||
}
|
||||
}
|
||||
|
||||
function selectRepos(repos: string) {
|
||||
userConfigInfo.branchList = []
|
||||
userConfigInfo.dirList = []
|
||||
getBranchList(repos)
|
||||
persistUserConfigInfo()
|
||||
}
|
||||
|
||||
async function selectBranch(branch: string) {
|
||||
userConfigInfo.selectedBranch = branch
|
||||
await getDirList()
|
||||
elCascaderKey.value = userConfigInfo.selectedBranch
|
||||
userConfigInfo.selectedDir = userConfigInfo.dirList[0].value
|
||||
userConfigInfo.selectedDirList = [userConfigInfo.selectedDir]
|
||||
persistUserConfigInfo()
|
||||
}
|
||||
|
||||
function branchModeChange(mode: BranchModeEnum) {
|
||||
const selBranch = userConfigInfo.selectedBranch
|
||||
const bv = userConfigInfo.branchList[0].value
|
||||
|
||||
switch (mode) {
|
||||
case BranchModeEnum.newBranch:
|
||||
userConfigInfo.selectedBranch = 'xxx'
|
||||
userConfigInfo.dirMode = DirModeEnum.newDir
|
||||
userConfigInfo.selectedDir = 'xxx'
|
||||
break
|
||||
|
||||
case BranchModeEnum.reposBranch:
|
||||
if (selBranch !== bv) {
|
||||
userConfigInfo.selectedBranch = bv
|
||||
getDirList()
|
||||
}
|
||||
break
|
||||
|
||||
default:
|
||||
userConfigInfo.selectedBranch = ''
|
||||
break
|
||||
}
|
||||
persistUserConfigInfo()
|
||||
}
|
||||
|
||||
function reset() {
|
||||
loading.value = false
|
||||
dirLoading.value = false
|
||||
store.dispatch('LOGOUT')
|
||||
}
|
||||
|
||||
function goUpload() {
|
||||
const { selectedDir, dirMode } = userConfigInfo
|
||||
let warningMessage: string = '目录不能为空!'
|
||||
|
||||
if (selectedDir === '') {
|
||||
switch (dirMode) {
|
||||
case DirModeEnum.newDir:
|
||||
warningMessage = '请在输入框输入一个新目录!'
|
||||
break
|
||||
case DirModeEnum.reposDir:
|
||||
warningMessage = `请选择 ${userConfigInfo.selectedRepos} 仓库下的一个目录!`
|
||||
break
|
||||
default:
|
||||
warningMessage = '请在输入框输入一个新目录!'
|
||||
break
|
||||
}
|
||||
ElMessage.warning(warningMessage)
|
||||
} else {
|
||||
router.push('/upload')
|
||||
}
|
||||
}
|
||||
|
||||
const cascaderProps = {
|
||||
lazy: true,
|
||||
checkStrictly: true,
|
||||
async lazyLoad(node: any, resolve: any) {
|
||||
const { level, pathLabels } = node
|
||||
let dirs: any
|
||||
if (level === 0) {
|
||||
dirs = userConfigInfo.dirList
|
||||
} else {
|
||||
dirs = await getDirListByPath(pathLabels.join('/'))
|
||||
}
|
||||
if (dirs) {
|
||||
resolve(
|
||||
dirs.map((x: any) => ({
|
||||
value: x.value,
|
||||
label: x.label,
|
||||
leaf: level >= 2
|
||||
}))
|
||||
)
|
||||
} else {
|
||||
resolve([])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function cascaderChange(e: string[]) {
|
||||
userConfigInfo.selectedDirList = e
|
||||
userConfigInfo.selectedDir = e.join('/')
|
||||
persistUserConfigInfo()
|
||||
}
|
||||
|
||||
watch(
|
||||
() => loggingStatus,
|
||||
(_n) => {
|
||||
if (!_n) {
|
||||
loading.value = false
|
||||
dirLoading.value = false
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped lang="stylus">
|
||||
@import "config.styl"
|
||||
</style>
|
||||
70
picx/src/views/management/management.styl
Normal file
70
picx/src/views/management/management.styl
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
57
picx/src/views/management/management.util.ts
Normal file
57
picx/src/views/management/management.util.ts
Normal file
@@ -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<any>) => {
|
||||
if (dir === '/') {
|
||||
store.dispatch('SET_USER_CONFIG_INFO', {
|
||||
dirMode: 'rootDir',
|
||||
needPersist: false
|
||||
})
|
||||
}
|
||||
}
|
||||
201
picx/src/views/management/management.vue
Normal file
201
picx/src/views/management/management.vue
Normal file
@@ -0,0 +1,201 @@
|
||||
<template>
|
||||
<div class="page-container management-page-container">
|
||||
<div class="content-container">
|
||||
<div class="top">
|
||||
<div class="left">
|
||||
<selected-info-bar />
|
||||
</div>
|
||||
<div class="right flex-start">
|
||||
<el-tooltip
|
||||
placement="top"
|
||||
:content="listing ? '切换方块展示' : '切换列表展示'"
|
||||
>
|
||||
<el-icon class="btn-icon" @click.stop="toggleListing">
|
||||
<Tickets v-if="listing" />
|
||||
<Menu v-if="!listing" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
<el-tooltip placement="top" content="重新加载图片">
|
||||
<el-icon class="btn-icon" @click.stop="reloadCurrentDirContent">
|
||||
<Refresh />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bottom" v-loading="loadingImageList" element-loading-text="加载中...">
|
||||
<image-selector
|
||||
v-if="currentPathImageList.length"
|
||||
:currentDirImageList="currentPathImageList"
|
||||
@update:initImageList="currentPathImageList"
|
||||
:key="renderKey"
|
||||
></image-selector>
|
||||
<ul
|
||||
class="image-list"
|
||||
:style="{
|
||||
height: isShowBatchTools ? 'calc(100% - 50rem)' : '100%'
|
||||
}"
|
||||
>
|
||||
<li class="image-item" v-if="userConfigInfo.selectedDir !== '/'">
|
||||
<folder-card mode="back" />
|
||||
</li>
|
||||
<li class="image-item" v-for="(dir, index) in currentPathDirList" :key="index">
|
||||
<folder-card :folder-obj="dir" />
|
||||
</li>
|
||||
<div class="clear"></div>
|
||||
<li
|
||||
class="image-item"
|
||||
v-for="(image, index) in currentPathImageList"
|
||||
:key="index"
|
||||
:style="{
|
||||
width: listing ? '50%' : '230rem',
|
||||
height: listing ? '80rem' : '240rem'
|
||||
}"
|
||||
>
|
||||
<image-card
|
||||
:image-obj="image"
|
||||
:listing="listing"
|
||||
v-model="activeIndex"
|
||||
:index="index"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, watch, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useStore } from '@/store'
|
||||
import { getContentByReposPath } from '@/common/api'
|
||||
import {
|
||||
dirModeHandle,
|
||||
filterDirContent,
|
||||
getDirContent
|
||||
} from '@/views/management/management.util'
|
||||
|
||||
import imageCard from '@/components/image-card/image-card.vue'
|
||||
import selectedInfoBar from '@/components/selected-info-bar/selected-info-bar.vue'
|
||||
import folderCard from '@/components/folder-card/folder-card.vue'
|
||||
import imageSelector from '@/components/image-selector/image-selector.vue'
|
||||
import { UploadedImageModel } from '@/common/model/upload.model'
|
||||
|
||||
const store = useStore()
|
||||
const router = useRouter()
|
||||
|
||||
const userConfigInfo = computed(() => store.getters.getUserConfigInfo).value
|
||||
const loggingStatus = computed(() => store.getters.getUserLoggingStatus).value
|
||||
const dirObject = computed(() => store.getters.getDirObject).value
|
||||
|
||||
const renderKey = ref(new Date().getTime()) // key for update image-selector component
|
||||
const loadingImageList = ref(false)
|
||||
const listing = ref(false)
|
||||
const activeIndex = ref<number>()
|
||||
|
||||
const currentPathDirList = ref([])
|
||||
const currentPathImageList = ref([])
|
||||
|
||||
async function dirContentHandle(dir: string) {
|
||||
loadingImageList.value = true
|
||||
|
||||
const dirContent = getDirContent(dir, dirObject)
|
||||
if (dirContent) {
|
||||
const dirs = filterDirContent(dir, dirContent, 'dir')
|
||||
const images = filterDirContent(dir, dirContent, 'image')
|
||||
if (!dirs.length && !images.length) {
|
||||
await getContentByReposPath(dir)
|
||||
} else {
|
||||
currentPathDirList.value = dirs
|
||||
currentPathImageList.value = images
|
||||
store.commit('REPLACE_IMAGE_CARD', { checkedImgArr: currentPathImageList.value })
|
||||
}
|
||||
} else {
|
||||
await getContentByReposPath(dir)
|
||||
}
|
||||
loadingImageList.value = false
|
||||
}
|
||||
|
||||
async function initDirImageList() {
|
||||
const { selectedDir, dirMode } = userConfigInfo
|
||||
|
||||
if (
|
||||
(dirMode === 'newDir' || dirMode === 'autoDir') &&
|
||||
!getDirContent(selectedDir, dirObject)
|
||||
) {
|
||||
userConfigInfo.selectedDir = '/'
|
||||
userConfigInfo.dirMode = 'rootDir'
|
||||
}
|
||||
|
||||
if (!dirObject.imageList.length && !dirObject.childrenDirs.length) {
|
||||
await getContentByReposPath(userConfigInfo.selectedDir)
|
||||
return
|
||||
}
|
||||
|
||||
await dirContentHandle(userConfigInfo.selectedDir)
|
||||
}
|
||||
|
||||
function toggleListing() {
|
||||
listing.value = !listing.value
|
||||
}
|
||||
|
||||
// 重新加载当前目录内容(网络请求)
|
||||
async function reloadCurrentDirContent() {
|
||||
const { selectedDir } = userConfigInfo
|
||||
await store.dispatch('DIR_IMAGE_LIST_INIT_DIR', selectedDir)
|
||||
loadingImageList.value = true
|
||||
await getContentByReposPath(selectedDir)
|
||||
loadingImageList.value = false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initDirImageList()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => loggingStatus,
|
||||
(nv) => {
|
||||
if (nv === false) {
|
||||
router.push('/config')
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => userConfigInfo.selectedDir,
|
||||
async (nDir) => {
|
||||
dirModeHandle(nDir, store)
|
||||
await dirContentHandle(nDir)
|
||||
renderKey.value += 1
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
() => dirObject,
|
||||
(nv: any) => {
|
||||
const { selectedDir } = userConfigInfo
|
||||
const dirContent = getDirContent(selectedDir, nv)
|
||||
if (dirContent) {
|
||||
currentPathDirList.value = filterDirContent(selectedDir, dirContent, 'dir')
|
||||
currentPathImageList.value = filterDirContent(selectedDir, dirContent, 'image')
|
||||
store.commit('REPLACE_IMAGE_CARD', { checkedImgArr: currentPathImageList.value })
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
const isShowBatchTools = ref(false)
|
||||
watch(
|
||||
() => currentPathImageList.value,
|
||||
(nv: UploadedImageModel[]) => {
|
||||
isShowBatchTools.value = nv.filter((x) => x.checked).length > 0
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped lang="stylus">
|
||||
@import 'management.styl';
|
||||
</style>
|
||||
44
picx/src/views/settings/settings.styl
Normal file
44
picx/src/views/settings/settings.styl
Normal file
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
149
picx/src/views/settings/settings.vue
Normal file
149
picx/src/views/settings/settings.vue
Normal file
@@ -0,0 +1,149 @@
|
||||
<template>
|
||||
<div class="page-container settings-page-container">
|
||||
<div class="setting-title">个性设置:</div>
|
||||
<ul class="setting-list">
|
||||
<li class="setting-item">
|
||||
<el-switch
|
||||
v-model="userSettings.defaultHash"
|
||||
@change="persistUserSettings"
|
||||
active-text="上传时给图片名加上哈希码(确保图片名唯一,强烈建议开启)"
|
||||
></el-switch>
|
||||
</li>
|
||||
<li class="setting-item">
|
||||
<el-switch
|
||||
v-model="userSettings.defaultPrefix"
|
||||
@change="persistUserSettings"
|
||||
active-text="上传时给图片名加上配置的前缀(示例:abc-image.jpg,abc- 为前缀)"
|
||||
></el-switch>
|
||||
<el-input
|
||||
class="prefix-input"
|
||||
v-if="userSettings.defaultPrefix"
|
||||
v-model="userSettings.prefixName"
|
||||
placeholder="请输入命名前缀"
|
||||
@input="persistUserSettings"
|
||||
clearable
|
||||
autofocus
|
||||
></el-input>
|
||||
</li>
|
||||
<li class="setting-item">
|
||||
<el-switch
|
||||
v-model="userSettings.defaultMarkdown"
|
||||
@change="persistUserSettings"
|
||||
active-text="上传成功后复制的图片外链启用 Markdown 格式()"
|
||||
></el-switch>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="setting-title">CDN 提供商:</div>
|
||||
<ul class="setting-list">
|
||||
<li class="setting-item">
|
||||
<el-select
|
||||
v-model="userSettings.externalLinkType"
|
||||
placeholder="选择 CDN 提供商"
|
||||
@change="saveUserSettings"
|
||||
>
|
||||
<el-option label="Staticaly" value="staticaly"></el-option>
|
||||
<el-option label="Cloudflare" value="cloudflare"></el-option>
|
||||
<el-option label="jsDelivr" value="jsdelivr"></el-option>
|
||||
</el-select>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="setting-title">压缩设置:</div>
|
||||
<ul class="setting-list">
|
||||
<li class="setting-item">
|
||||
<el-switch
|
||||
v-model="userSettings.isCompress"
|
||||
@change="persistUserSettings"
|
||||
active-text="是否压缩图片"
|
||||
></el-switch>
|
||||
</li>
|
||||
<li class="setting-item">
|
||||
<div class="img-encoder-title">选择图像编码器(压缩算法):</div>
|
||||
<el-radio-group
|
||||
:disabled="!userSettings.isCompress"
|
||||
v-model="userSettings.compressEncoder"
|
||||
@change="persistUserSettings"
|
||||
>
|
||||
<el-radio :label="compressEncoder.webP">
|
||||
{{ compressEncoder.webP }} (压缩后图片格式为 webp,大多数现代浏览器支持)
|
||||
</el-radio>
|
||||
<el-radio :label="compressEncoder.mozJPEG">
|
||||
{{ compressEncoder.mozJPEG }} (压缩后图片格式为 jpg,兼容性最好)
|
||||
</el-radio>
|
||||
<el-radio :label="compressEncoder.avif">
|
||||
{{ compressEncoder.avif }}
|
||||
(压缩后图片格式为 avif,压缩比最高,目前仅谷歌浏览器支持)
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="setting-title">主题设置:</div>
|
||||
<ul class="setting-list">
|
||||
<li class="setting-item">
|
||||
<el-select
|
||||
v-model="userSettings.themeMode"
|
||||
placeholder="主题模式"
|
||||
@change="saveUserSettings"
|
||||
>
|
||||
<el-option label="自动设置" value="auto"></el-option>
|
||||
<el-option label="暗夜主题" value="dark"></el-option>
|
||||
<el-option label="白昼主题" value="light"></el-option>
|
||||
</el-select>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="setting-title" v-if="userSettings.themeMode === 'auto'">
|
||||
设置白昼模式时间区间:
|
||||
</div>
|
||||
<ul class="setting-list" v-if="userSettings.themeMode === 'auto'">
|
||||
<li class="setting-item">
|
||||
<el-form ref="form">
|
||||
<el-form-item>
|
||||
<el-time-select
|
||||
v-model="userSettings.autoLightThemeTime[0]"
|
||||
start="00:00"
|
||||
step="00:30"
|
||||
end="23:59"
|
||||
@change="saveUserSettings"
|
||||
></el-time-select>
|
||||
<span class="time-middle-space"> ~ </span>
|
||||
<el-time-select
|
||||
v-model="userSettings.autoLightThemeTime[1]"
|
||||
:start="userSettings.autoLightThemeTime[0]"
|
||||
step="00:30"
|
||||
end="23:59"
|
||||
@change="saveUserSettings"
|
||||
></el-time-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import { store } from '@/store'
|
||||
import { CompressEncoderMap } from '@/utils/compress'
|
||||
|
||||
const userSettings = computed(() => store.getters.getUserSettings).value
|
||||
|
||||
const persistUserSettings = () => {
|
||||
store.dispatch('USER_SETTINGS_PERSIST')
|
||||
}
|
||||
|
||||
const compressEncoder = CompressEncoderMap
|
||||
|
||||
const saveUserSettings = () => {
|
||||
store.dispatch('SET_USER_SETTINGS', {
|
||||
...userSettings
|
||||
})
|
||||
persistUserSettings()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="stylus">
|
||||
@import "settings.styl"
|
||||
</style>
|
||||
15
picx/src/views/tutorials/tutorials.styl
Normal file
15
picx/src/views/tutorials/tutorials.styl
Normal file
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
64
picx/src/views/tutorials/tutorials.vue
Normal file
64
picx/src/views/tutorials/tutorials.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<div class="page-container tutorials-page-container">
|
||||
<el-steps :active="stepNum" finish-status="success" align-center>
|
||||
<el-step title="创建 GitHub 仓库"></el-step>
|
||||
<el-step title="获取 GitHub Token"></el-step>
|
||||
<el-step title="配置图床"></el-step>
|
||||
</el-steps>
|
||||
<div class="step-content">
|
||||
<Step1 v-if="stepNum === 0"></Step1>
|
||||
<Step2 v-if="stepNum === 1"></Step2>
|
||||
<Step3 v-if="stepNum === 2"></Step3>
|
||||
<div v-if="stepNum === 3">
|
||||
<h2>👍 恭喜你完成图床使用教程 🌟 快去试试吧~</h2>
|
||||
<h2>https://github.com/XPoet/picx</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-next-prev">
|
||||
<el-button type="primary" @click="step('prev')" v-if="stepNum > 0">上一步 </el-button>
|
||||
<el-button type="primary" @click="step('next')" v-if="stepNum < 3">下一步 </el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, Ref } from 'vue'
|
||||
import Step1 from '@/components/tutorials-step/step1.vue'
|
||||
import Step2 from '@/components/tutorials-step/step2.vue'
|
||||
import Step3 from '@/components/tutorials-step/step3.vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'tutorials',
|
||||
|
||||
components: {
|
||||
Step1,
|
||||
Step2,
|
||||
Step3
|
||||
},
|
||||
|
||||
setup() {
|
||||
const stepNum: Ref<number> = ref(0)
|
||||
|
||||
const step = (type: string) => {
|
||||
if (type === 'prev') {
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
stepNum.value < 0 ? (stepNum.value = 0) : (stepNum.value -= 1)
|
||||
}
|
||||
|
||||
if (type === 'next') {
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
stepNum.value > 3 ? (stepNum.value = 3) : (stepNum.value += 1)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
stepNum,
|
||||
step
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="stylus">
|
||||
@import "tutorials.styl"
|
||||
</style>
|
||||
135
picx/src/views/upload/upload.styl
Normal file
135
picx/src/views/upload/upload.styl
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
151
picx/src/views/upload/upload.vue
Normal file
151
picx/src/views/upload/upload.vue
Normal file
@@ -0,0 +1,151 @@
|
||||
<template>
|
||||
<div class="upload-page-container">
|
||||
<div
|
||||
class="upload-page-left page-container"
|
||||
v-if="uploadedImageList.length && userSettings.elementPlusSize === 'large'"
|
||||
:style="{
|
||||
width: '280rem'
|
||||
}"
|
||||
>
|
||||
<div class="uploaded-item" v-for="(item, index) in uploadedImageList" :key="index">
|
||||
<image-card :image-obj="item" :is-uploaded="true" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="upload-page-right page-container">
|
||||
<!-- 上传区域 -->
|
||||
<div class="row-item">
|
||||
<div class="content-box">
|
||||
<upload-area :image-loading="imageLoading" ref="uploadAreaDom"></upload-area>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 待上传的图片列表 -->
|
||||
<div class="row-item">
|
||||
<div class="content-box">
|
||||
<to-upload-image-card
|
||||
ref="toUploadImageCardDom"
|
||||
:loading-all-image="imageLoading"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 重置 & 上传 -->
|
||||
<div class="row-item">
|
||||
<div class="content-box" style="text-align: right">
|
||||
<el-button
|
||||
plain
|
||||
type="warning"
|
||||
@click="resetUploadInfo"
|
||||
v-if="toUploadImage.list.length"
|
||||
>重置
|
||||
</el-button>
|
||||
<el-button plain type="primary" @click="uploadImage"> 上传 </el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, watch, ref, Ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useStore } from '@/store'
|
||||
import imageCard from '@/components/image-card/image-card.vue'
|
||||
import toUploadImageCard from '@/components/to-upload-image-card/to-upload-image-card.vue'
|
||||
import uploadArea from '@/components/upload-area/upload-area.vue'
|
||||
import { UploadStatusEnum } from '@/common/model/upload.model'
|
||||
|
||||
const store = useStore()
|
||||
const router = useRouter()
|
||||
|
||||
const GitHubExternalLinkInput: Ref = ref<null | HTMLElement>(null)
|
||||
const CDNExternalLinkInput: Ref = ref<null | HTMLElement>(null)
|
||||
const toUploadImageCardDom: Ref = ref<null | HTMLElement>(null)
|
||||
const uploadAreaDom: Ref = ref<null | HTMLElement>(null)
|
||||
|
||||
const userConfigInfo = computed(() => store.getters.getUserConfigInfo)
|
||||
const userSettings = computed(() => store.getters.getUserSettings)
|
||||
const logoutStatus = computed(() => store.getters.getUserLoggingStatus)
|
||||
const uploadedImageList = computed(() => store.getters.getUploadedImageList)
|
||||
const toUploadImage = computed(() => store.getters.getToUploadImage)
|
||||
const imageLoading = ref(false)
|
||||
|
||||
const uploadImage = () => {
|
||||
const { token, selectedRepos, selectedDir } = userConfigInfo.value
|
||||
|
||||
if (!token) {
|
||||
ElMessage.error('请先完成图床配置!')
|
||||
router.push('/config')
|
||||
return
|
||||
}
|
||||
|
||||
if (!selectedRepos) {
|
||||
ElMessage.error('请选择一个仓库!')
|
||||
router.push('/config')
|
||||
return
|
||||
}
|
||||
|
||||
if (!selectedDir) {
|
||||
ElMessage.error('目录不能为空!')
|
||||
router.push('/config')
|
||||
return
|
||||
}
|
||||
|
||||
if (toUploadImage.value.list.length === 0) {
|
||||
ElMessage.error('图片不能为空!')
|
||||
return
|
||||
}
|
||||
|
||||
if (toUploadImage.value.list.length === toUploadImage.value.uploadedNumber) {
|
||||
ElMessage.error('请选择要上传的图片!')
|
||||
return
|
||||
}
|
||||
|
||||
imageLoading.value = true
|
||||
toUploadImageCardDom.value
|
||||
.uploadImage_all(userConfigInfo.value)
|
||||
.then((v: UploadStatusEnum) => {
|
||||
// eslint-disable-next-line default-case
|
||||
switch (v) {
|
||||
// 单张图片上传成功
|
||||
case UploadStatusEnum.uploaded:
|
||||
|
||||
// 所有图片上传成功
|
||||
// eslint-disable-next-line no-fallthrough
|
||||
case UploadStatusEnum.allUploaded:
|
||||
imageLoading.value = false
|
||||
store.dispatch('TO_UPLOAD_IMAGE_CLEAN_URL')
|
||||
break
|
||||
|
||||
// 上传失败(网络错误等原因)
|
||||
case UploadStatusEnum.uploadFail:
|
||||
imageLoading.value = false
|
||||
store.dispatch('TO_UPLOAD_IMAGE_LIST_FAIL')
|
||||
break
|
||||
}
|
||||
})
|
||||
.catch((e: any) => {
|
||||
console.error('upload error: ', e)
|
||||
imageLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
const resetUploadInfo = () => {
|
||||
imageLoading.value = false
|
||||
store.dispatch('TO_UPLOAD_IMAGE_LOGOUT')
|
||||
}
|
||||
|
||||
watch(
|
||||
() => logoutStatus,
|
||||
(_n) => {
|
||||
// 如果退出登录,清空信息
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
!_n && resetUploadInfo()
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
@import "upload.styl"
|
||||
</style>
|
||||
Reference in New Issue
Block a user