Files
PicList/src/renderer/manage/pages/ManageMain.vue
2026-01-23 21:57:49 +08:00

581 lines
20 KiB
Vue

<template>
<div class="manage-container">
<!-- Header Card -->
<div class="manage-card header-card">
<div class="card-header">
<div class="header-content">
<div class="header-icon">
<img :src="`./assets/${currentPagePicBedConfig.picBedName}.webp`" class="header-icon-img" />
</div>
<div class="header-text">
<h2 class="header-title">
{{ supportedPicBedList[currentPagePicBedConfig.picBedName].name }}
</h2>
<p class="header-subtitle">
{{ menuTitleMap[currentPicBedName] }}
</p>
</div>
</div>
<div class="header-actions">
<button class="action-button secondary" @click="openPicBedUrl">
<ExternalLinkIcon class="button-icon" />
{{ t('pages.manage.main.openPicBedUrl') }}
</button>
<button
v-if="showNewIconList.includes(currentPicBedName)"
class="action-button primary"
@click="openNewBucketDrawer"
>
<PlusIcon class="button-icon" />
{{ t('pages.manage.main.newBucket') }}
</button>
</div>
</div>
</div>
<!-- Main Content Card -->
<div class="manage-card main-card">
<div class="main-layout">
<div class="sidebar" :style="{ width: sidebarWidth + 'px' }">
<div class="sidebar-header">
<h3 class="sidebar-title">
{{ menuTitleMap[currentPicBedName] }}
</h3>
</div>
<div class="sidebar-content">
<div v-if="isLoadingBucketList" class="loading-container">
<div class="loading-spinner" />
<span class="loading-text">{{ t('pages.manage.main.loading') }}</span>
</div>
<div v-else class="menu-list">
<div
v-for="item in bucketNameList"
:key="item"
class="menu-item"
:class="{ active: item === currentSelectedBucket }"
@click="handleSelectMenu(item)"
>
<FolderIcon
v-if="currentSelectedBucket === item && currentPicBedName !== 'github'"
class="menu-icon active"
/>
<FolderIcon v-else-if="currentPicBedName !== 'github'" class="menu-icon" />
<GitBranchIcon v-else-if="currentPicBedName === 'github'" class="menu-icon" />
<span class="menu-text" :title="item">
{{ truncateText(item, currentPicBedName) }}
</span>
</div>
</div>
</div>
<div class="sidebar-footer">
<div class="footer-actions">
<button class="footer-action-item" @click="switchPicBed('main')">
<HomeIcon class="action-icon" />
<span class="action-text">{{ t('pages.manage.main.backToHome') }}</span>
</button>
<button class="footer-action-item" @click="changePicBed">
<ArrowLeftRightIcon class="action-icon" />
<span class="action-text">{{ t('pages.manage.main.switchPicBed') }}</span>
</button>
<button class="footer-action-item" @click="openBucketPageSetting">
<SettingsIcon class="action-icon" />
<span class="action-text">{{ t('pages.manage.main.settings') }}</span>
</button>
</div>
</div>
</div>
<!-- Resize Handle -->
<div class="resize-handle" @mousedown="startResize">
<div class="resize-line" />
</div>
<div class="content-area">
<router-view />
</div>
</div>
</div>
<!-- PicBed Switch Dialog -->
<transition
name="modal"
enter-active-class="transition-all duration-200 ease-apple"
leave-active-class="transition-all duration-200 ease-apple"
enter-from-class="opacity-0"
leave-to-class="opacity-0"
>
<div v-if="picBedSwitchDialogVisible" class="dialog-overlay" @click="picBedSwitchDialogVisible = false">
<div class="dialog-container" @click.stop>
<div class="dialog-header">
<h3 class="dialog-title">
{{ t('pages.manage.main.switchPicBed') }}
</h3>
<button class="dialog-close" @click="picBedSwitchDialogVisible = false">
<XIcon class="close-icon" />
</button>
</div>
<div class="dialog-content">
<div class="choice-cos">
<!-- Back to main card -->
<div class="picbed-card main-card" @click="switchPicBed('main')">
<div class="card-icon">
<HomeIcon class="main-icon" />
</div>
<div class="card-content">
<div class="card-title main-title">
{{ $t('pages.manage.main.backToHome') }}
</div>
</div>
</div>
<!-- PicBed cards -->
<div
v-for="(config, alias) in allPicBedConfigure"
:key="String(alias)"
class="picbed-card"
:class="{ active: String(alias) === currentAlias }"
@click="switchPicBed(String(alias))"
>
<div class="card-icon">
<img :src="`./assets/${config.picBedName}.webp`" class="picbed-icon" />
</div>
<div class="card-content">
<div class="card-title">
{{ config.alias }}
</div>
</div>
<div v-if="String(alias) === currentAlias" class="check-icon">
<CheckIcon />
</div>
</div>
</div>
</div>
</div>
</div>
</transition>
<!-- New Bucket Drawer -->
<div v-if="nweBucketDrawerVisible" class="drawer-overlay" @click="nweBucketDrawerVisible = false">
<div class="drawer-container" @click.stop>
<div class="drawer-header">
<h3 class="drawer-title">
{{ t('pages.manage.main.newBucket') }}
</h3>
<button class="drawer-close" @click="nweBucketDrawerVisible = false">
<XIcon class="close-icon" />
</button>
</div>
<div class="drawer-content">
<form @submit.prevent="createNewBucket(currentPicBedName)">
<div class="form-header">
<div class="form-icon">
<img :src="`./assets/${currentPicBedName}.webp`" class="picbed-form-icon" />
</div>
</div>
<div class="form-divider" />
<div v-for="option in newBucketConfig[currentPicBedName].options" :key="option" class="form-group">
<label class="form-label">
{{ newBucketConfig[currentPicBedName].configOptions[option].description }}
</label>
<!-- Input field -->
<input
v-if="
newBucketConfig[currentPicBedName].configOptions[option].component === 'input' &&
currentPicBedName !== 'tcyun'
"
v-model.trim="newBucketConfigResult[currentPicBedName + '.' + option]"
type="text"
class="form-input"
:placeholder="newBucketConfig[currentPicBedName].configOptions[option].placeholder"
/>
<!-- TCyun special input with append -->
<div
v-if="
currentPicBedName === 'tcyun' &&
newBucketConfig[currentPicBedName].configOptions[option].component === 'input'
"
class="input-group"
>
<input
v-model.trim="newBucketConfigResult[currentPicBedName + '.' + option]"
type="text"
class="form-input group-input"
:placeholder="newBucketConfig[currentPicBedName].configOptions[option].placeholder"
/>
<span class="input-append">{{ '-' + currentPagePicBedConfig.appId }}</span>
</div>
<!-- Select field -->
<div
v-if="newBucketConfig[currentPicBedName].configOptions[option].component === 'select'"
class="select-wrapper"
>
<select v-model="newBucketConfigResult[currentPicBedName + '.' + option]" class="form-select">
<option
v-for="(label, value) in newBucketConfig[currentPicBedName].configOptions[option].options"
:key="value"
:value="value"
>
{{ label }}
</option>
</select>
<ChevronDownIcon class="select-arrow" />
</div>
<!-- Switch field -->
<label
v-if="newBucketConfig[currentPicBedName].configOptions[option].component === 'switch'"
class="switch-label"
>
<input
v-model="newBucketConfigResult[currentPicBedName + '.' + option]"
type="checkbox"
class="switch-input"
:true-value="true"
:false-value="false"
/>
<span class="switch-slider">
<span class="switch-button" />
</span>
</label>
</div>
<div class="form-actions">
<button type="button" class="action-button secondary" @click="nweBucketDrawerVisible = false">
{{ $t('common.cancel') }}
</button>
<button type="submit" class="action-button primary">
<CheckIcon class="button-icon" />
{{ t('common.submit') }}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import {
ArrowLeftRightIcon,
CheckIcon,
ChevronDownIcon,
ExternalLinkIcon,
FolderIcon,
GitBranchIcon,
HomeIcon,
PlusIcon,
SettingsIcon,
XIcon,
} from 'lucide-vue-next'
import { computed, onBeforeMount, reactive, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router'
import useMessage from '@/hooks/useMessage'
import { useManageStore } from '@/manage/store/manageStore'
import { supportedPicBedList } from '@/manage/utils/constants'
import { newBucketConfig } from '@/manage/utils/newBucketConfig'
import { IRPCActionType } from '@/utils/enum'
const { t } = useI18n()
const manageStore = useManageStore() as any
const route = useRoute()
const router = useRouter()
const message = useMessage()
const currentAlias = ref(route.query.alias as string)
const currentPicBedName = ref(route.query.picBedName as string)
const sidebarWidth = ref(160)
const isResizing = ref(false)
let allPicBedConfigure = JSON.parse(route.query.allPicBedConfigure as string)
let currentPagePicBedConfig = reactive(JSON.parse(route.query.config as string))
const newBucketConfigResult: IStringKeyMap = reactive({})
const bucketList = ref({} as IStringKeyMap)
const currentSelectedBucket = ref('')
const bucketNameList = ref([] as string[])
const isLoadingBucketList = ref(false)
const nweBucketDrawerVisible = ref(false)
const picBedSwitchDialogVisible = ref(false)
const maxTextLength = computed(() => {
const fixedSpace = 16 + 12 + 24 + 8
const availableWidth = sidebarWidth.value - fixedSpace
const estimatedCharWidth = 14 * 0.6
const maxChars = Math.floor(availableWidth / estimatedCharWidth)
return Math.max(6, Math.min(maxChars, 60))
})
const truncateText = (text: string, picBedName: string): string => {
if (!text) return ''
if (picBedName === 'tcyun') {
const baseName = text.slice(0, text.length - 11)
if (baseName.length <= maxTextLength.value) {
return baseName
}
return `${baseName.slice(0, maxTextLength.value - 3)}...`
} else if (picBedName === 'github') {
if (text.length <= maxTextLength.value) {
return text
}
const minSideLength = 3
const totalEllipsis = 2 // '..'
const availableForContent = maxTextLength.value - totalEllipsis
if (availableForContent < minSideLength * 2) {
return `${text.slice(0, maxTextLength.value - 3)}...`
}
const prefixLength = Math.ceil(availableForContent / 2)
const suffixLength = availableForContent - prefixLength
return `${text.slice(0, prefixLength)}..${text.slice(-suffixLength)}`
} else {
if (text.length <= maxTextLength.value) {
return text
}
return `${text.slice(0, maxTextLength.value - 3)}...`
}
}
watch(
route,
async newRoute => {
if (newRoute.fullPath.split('?')[0] === '/main-page/manage-main-page') {
currentAlias.value = newRoute.query.alias as string
currentPicBedName.value = newRoute.query.picBedName as string
allPicBedConfigure = JSON.parse(newRoute.query.allPicBedConfigure as string)
currentPagePicBedConfig = reactive(JSON.parse(newRoute.query.config as string))
await getBucketList()
}
},
{ deep: true },
)
watch(sidebarWidth, () => {}, { immediate: false })
const urlMap: IStringKeyMap = {
aliyun: 'https://oss.console.aliyun.com',
github: 'https://github.com',
imgur: 'https://imgur.com',
local: 'https://piclist.cn',
qiniu: 'https://portal.qiniu.com',
s3plist: 'https://aws.amazon.com/cn/s3/',
sftp: 'https://github.com/imba97/picgo-plugin-sftp-uploader',
smms: 'https://smms.app',
tcyun: 'https://console.cloud.tencent.com/cos',
upyun: 'https://console.upyun.com',
webdavplist: 'https://baike.baidu.com/item/WebDAV/4610909',
}
const showNewIconList = ['aliyun', 'qiniu', 'tcyun', 's3plist']
const bucketT = t('pages.manage.main.bucket')
const galleryT = t('pages.manage.main.gallery')
const repositoryT = t('pages.manage.main.repo')
const menuTitleMap: IStringKeyMap = {
aliyun: bucketT,
qiniu: bucketT,
tcyun: bucketT,
upyun: bucketT,
s3plist: bucketT,
sftp: '',
smms: galleryT,
imgur: galleryT,
github: repositoryT,
webdavplist: '',
local: '',
}
const openPicBedUrl = () => window.electron.sendRPC(IRPCActionType.OPEN_URL, urlMap[currentPagePicBedConfig.picBedName])
function openNewBucketDrawer() {
nweBucketDrawerVisible.value = true
}
function createNewBucket(picBedName: string) {
const configOptions = newBucketConfig[picBedName].configOptions
const resultMap: IStringKeyMap = Object.keys(configOptions).reduce((result, key) => {
const resultKey = `${picBedName}.${key}`
const defaultValue = configOptions[key].default
const resultValue = newBucketConfigResult[resultKey]
result[key] =
resultValue === '' && defaultValue !== undefined ? defaultValue : resultValue === undefined ? '' : resultValue
return result
}, {} as IStringKeyMap)
if (currentPicBedName.value === 'tcyun') {
resultMap.BucketName = `${resultMap.BucketName}-${currentPagePicBedConfig.appId}`
}
resultMap.endpoint = currentPagePicBedConfig.endpoint
window.electron.triggerRPC(IRPCActionType.MANAGE_CREATE_BUCKET, currentAlias.value, resultMap).then((result: any) => {
if (result) {
// Show success notification
message.success(t('pages.manage.main.createSuccess'))
nweBucketDrawerVisible.value = false
setTimeout(() => {
getBucketList()
}, 2000)
} else {
// Show error notification
message.error(t('pages.manage.main.createFailed'))
}
})
}
async function getBucketList() {
bucketList.value = {}
bucketNameList.value = []
isLoadingBucketList.value = true
const result = await window.electron.triggerRPC<any>(IRPCActionType.MANAGE_GET_BUCKET_LIST, currentAlias.value)
isLoadingBucketList.value = false
if (result.length > 0) {
result.forEach((item: any) => {
bucketList.value[item.Name] = item
bucketNameList.value.push(item.Name)
})
}
}
function transPathToUnix(filePath: string | undefined) {
if (!filePath) return ''
return window.electron.platform === 'win32'
? filePath
.split(window.node.path.sep)
.join(window.node.path.posix.sep)
.replace(/^\/+|\/+$/g, '')
: filePath.replace(/^\/+|\/+$/g, '')
}
function handleSelectMenu(bucketName: string) {
const currentPicBedConfig = manageStore.config.picBed[currentAlias.value]
const transformedConfig = JSON.parse(currentPicBedConfig.transformedConfig ?? '{}')
let prefix = transformedConfig[bucketName]?.baseDir || '/'
const cpicBedName = currentPicBedConfig.picBedName ?? currentPicBedName.value
if (cpicBedName === 'local') {
prefix = `/${transPathToUnix(prefix)}/`
} else {
prefix = prefix.startsWith('/') ? prefix : `/${prefix}`
prefix = prefix.endsWith('/') ? prefix : `${prefix}/`
}
const configMap = {
prefix,
bucketName,
customUrl: transformedConfig[bucketName]?.customUrl ?? '',
picBedName: cpicBedName,
alias: currentAlias.value,
bucketConfig: bucketList.value[bucketName],
cdnUrl: currentPicBedConfig.customUrl,
baseDir: prefix,
webPath: currentPicBedConfig.webPath || '',
}
currentSelectedBucket.value = bucketName
router.push({
path: '/main-page/manage-main-page/manage-bucket-page',
query: {
configMap: JSON.stringify(configMap),
alias: currentAlias.value,
picBedName: currentPicBedName.value,
config: JSON.stringify(currentPagePicBedConfig),
allPicBedConfigure: JSON.stringify(allPicBedConfigure),
},
})
}
function switchPicBed(picBedAlias: string) {
if (picBedAlias === 'main') {
router.push({
path: '/main-page/manage-login-page',
})
return
}
if (
route.fullPath.startsWith('/main-page/manage-main-page/manage-bucket-page') ||
route.fullPath.startsWith('/main-page/manage-main-page/manage-setting-page')
) {
picBedSwitchDialogVisible.value = false
router.push({
path: '/main-page/manage-main-page',
query: {
alias: picBedAlias,
picBedName: allPicBedConfigure[picBedAlias].picBedName,
config: JSON.stringify(allPicBedConfigure[picBedAlias]),
allPicBedConfigure: JSON.stringify(allPicBedConfigure),
},
})
} else {
currentAlias.value = picBedAlias
currentPicBedName.value = allPicBedConfigure[picBedAlias].picBedName
currentPagePicBedConfig = allPicBedConfigure[picBedAlias]
picBedSwitchDialogVisible.value = false
currentSelectedBucket.value = ''
getBucketList()
}
}
function changePicBed() {
picBedSwitchDialogVisible.value = true
}
function openBucketPageSetting() {
router.push({
path: '/main-page/manage-main-page/manage-setting-page',
query: {
alias: currentAlias.value,
picBedName: currentPicBedName.value,
config: JSON.stringify(currentPagePicBedConfig),
allPicBedConfigure: JSON.stringify(allPicBedConfigure),
},
})
}
function startResize(event: MouseEvent) {
isResizing.value = true
const startX = event.clientX
const startWidth = sidebarWidth.value
const handleMouseMove = (e: MouseEvent) => {
if (!isResizing.value) return
const deltaX = e.clientX - startX
const newWidth = Math.max(120, Math.min(400, startWidth + deltaX))
sidebarWidth.value = newWidth
}
const handleMouseUp = () => {
isResizing.value = false
document.removeEventListener('mousemove', handleMouseMove)
document.removeEventListener('mouseup', handleMouseUp)
document.body.style.cursor = ''
document.body.style.userSelect = ''
}
document.addEventListener('mousemove', handleMouseMove)
document.addEventListener('mouseup', handleMouseUp)
document.body.style.cursor = 'col-resize'
document.body.style.userSelect = 'none'
}
onBeforeMount(() => {
getBucketList()
})
</script>
<style src="./css/ManageMain.css" scoped></style>