mirror of
https://github.com/Kuingsmile/PicList.git
synced 2026-05-07 08:02:45 +08:00
✨ Feature(custom): add guide page for first time use
This commit is contained in:
@@ -13,6 +13,7 @@ UPLOADING: Uploading
|
|||||||
QUICK_UPLOAD: Quick Upload
|
QUICK_UPLOAD: Quick Upload
|
||||||
UPLOAD_BY_CLIPBOARD: Upload by Clipboard
|
UPLOAD_BY_CLIPBOARD: Upload by Clipboard
|
||||||
SHOW_PICBED_QRCODE: Show Picbed Qrcode
|
SHOW_PICBED_QRCODE: Show Picbed Qrcode
|
||||||
|
SHOW_FIRST_TIME_GUIDE: Show First-Time Guide
|
||||||
ENABLE: Enable
|
ENABLE: Enable
|
||||||
DISABLE: Disable
|
DISABLE: Disable
|
||||||
CONFIG_THING: Config ${c}
|
CONFIG_THING: Config ${c}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ UPLOADING: 正在上传
|
|||||||
QUICK_UPLOAD: 快捷上传
|
QUICK_UPLOAD: 快捷上传
|
||||||
UPLOAD_BY_CLIPBOARD: 剪贴板图片上传
|
UPLOAD_BY_CLIPBOARD: 剪贴板图片上传
|
||||||
SHOW_PICBED_QRCODE: 生成图床配置二维码
|
SHOW_PICBED_QRCODE: 生成图床配置二维码
|
||||||
|
SHOW_FIRST_TIME_GUIDE: 显示新手指南
|
||||||
ENABLE: 启用
|
ENABLE: 启用
|
||||||
DISABLE: 禁用
|
DISABLE: 禁用
|
||||||
CONFIG_THING: 配置${c}
|
CONFIG_THING: 配置${c}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ UPLOADING: 正在上傳
|
|||||||
QUICK_UPLOAD: 快速上傳
|
QUICK_UPLOAD: 快速上傳
|
||||||
UPLOAD_BY_CLIPBOARD: 剪貼簿圖片上傳
|
UPLOAD_BY_CLIPBOARD: 剪貼簿圖片上傳
|
||||||
SHOW_PICBED_QRCODE: 產生圖床配置 QRCODE
|
SHOW_PICBED_QRCODE: 產生圖床配置 QRCODE
|
||||||
|
SHOW_FIRST_TIME_GUIDE: 顯示新手指南
|
||||||
ENABLE: 啟用
|
ENABLE: 啟用
|
||||||
DISABLE: 禁用
|
DISABLE: 禁用
|
||||||
CONFIG_THING: 設定${c}
|
CONFIG_THING: 設定${c}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export const PICGO_TOGGLE_PLUGIN = 'PICGO_TOGGLE_PLUGIN'
|
|||||||
export const RENAME_FILE_NAME = 'RENAME_FILE_NAME'
|
export const RENAME_FILE_NAME = 'RENAME_FILE_NAME'
|
||||||
export const GET_RENAME_FILE_NAME = 'GET_RENAME_FILE_NAME'
|
export const GET_RENAME_FILE_NAME = 'GET_RENAME_FILE_NAME'
|
||||||
export const SHOW_MAIN_PAGE_QRCODE = 'SHOW_MAIN_PAGE_QRCODE'
|
export const SHOW_MAIN_PAGE_QRCODE = 'SHOW_MAIN_PAGE_QRCODE'
|
||||||
|
export const SHOW_FIRST_TIME_GUIDE = 'SHOW_FIRST_TIME_GUIDE'
|
||||||
// rpc
|
// rpc
|
||||||
export const RPC_ACTIONS = 'RPC_ACTIONS'
|
export const RPC_ACTIONS = 'RPC_ACTIONS'
|
||||||
export const RPC_ACTIONS_INVOKE = 'RPC_ACTIONS_INVOKE'
|
export const RPC_ACTIONS_INVOKE = 'RPC_ACTIONS_INVOKE'
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
PICGO_HANDLE_PLUGIN_DONE,
|
PICGO_HANDLE_PLUGIN_DONE,
|
||||||
PICGO_HANDLE_PLUGIN_ING,
|
PICGO_HANDLE_PLUGIN_ING,
|
||||||
PICGO_TOGGLE_PLUGIN,
|
PICGO_TOGGLE_PLUGIN,
|
||||||
|
SHOW_FIRST_TIME_GUIDE,
|
||||||
SHOW_MAIN_PAGE_QRCODE,
|
SHOW_MAIN_PAGE_QRCODE,
|
||||||
} from '~/events/constant'
|
} from '~/events/constant'
|
||||||
import { handlePluginUninstall, handlePluginUpdate } from '~/events/rpc/routes/plugin/utils'
|
import { handlePluginUninstall, handlePluginUpdate } from '~/events/rpc/routes/plugin/utils'
|
||||||
@@ -111,6 +112,12 @@ const buildMainPageMenu = (win: BrowserWindow) => {
|
|||||||
win?.webContents?.send(SHOW_MAIN_PAGE_QRCODE)
|
win?.webContents?.send(SHOW_MAIN_PAGE_QRCODE)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: $t('SHOW_FIRST_TIME_GUIDE'),
|
||||||
|
click() {
|
||||||
|
win?.webContents?.send(SHOW_FIRST_TIME_GUIDE)
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: $t('OPEN_TOOLBOX'),
|
label: $t('OPEN_TOOLBOX'),
|
||||||
click() {
|
click() {
|
||||||
|
|||||||
354
src/renderer/components/FirstTimeGuide.vue
Normal file
354
src/renderer/components/FirstTimeGuide.vue
Normal file
@@ -0,0 +1,354 @@
|
|||||||
|
<template>
|
||||||
|
<TransitionRoot appear :show="isVisible" as="template">
|
||||||
|
<div class="guide-overlay">
|
||||||
|
<div class="guide-backdrop" @click="handleClose" />
|
||||||
|
|
||||||
|
<div v-if="currentStepConfig.target" class="guide-spotlight" :style="spotlightStyle" />
|
||||||
|
|
||||||
|
<!-- Guide Card -->
|
||||||
|
<div class="guide-card" :style="cardStyle">
|
||||||
|
<div class="guide-header">
|
||||||
|
<div class="guide-header-left">
|
||||||
|
<h3 class="guide-title">{{ t('guide.title') }}</h3>
|
||||||
|
<span class="guide-step-indicator">
|
||||||
|
{{ t('guide.stepIndicator', { current: currentStep + 1, total: steps.length }) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button class="guide-close" :title="t('guide.close')" @click="handleClose">
|
||||||
|
<XIcon :size="20" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="guide-content">
|
||||||
|
<div class="guide-icon">
|
||||||
|
<component :is="currentStepConfig.icon" :size="24" />
|
||||||
|
</div>
|
||||||
|
<div class="guide-text">
|
||||||
|
<h4 class="guide-content-title">{{ t(currentStepConfig.title) }}</h4>
|
||||||
|
<p class="guide-content-description">{{ t(currentStepConfig.description) }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="guide-footer">
|
||||||
|
<div class="guide-progress">
|
||||||
|
<div
|
||||||
|
v-for="(_, index) in steps"
|
||||||
|
:key="index"
|
||||||
|
class="progress-dot"
|
||||||
|
:class="{ active: index === currentStep, completed: index < currentStep }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="guide-actions">
|
||||||
|
<button v-if="currentStep > 0" class="guide-btn secondary" @click="handlePrevious">
|
||||||
|
<ChevronLeftIcon :size="16" />
|
||||||
|
{{ t('guide.previous') }}
|
||||||
|
</button>
|
||||||
|
<button class="guide-btn outline" @click="handleSkip">
|
||||||
|
{{ t('guide.skip') }}
|
||||||
|
</button>
|
||||||
|
<button v-if="currentStep < steps.length - 1" class="guide-btn primary" @click="handleNext">
|
||||||
|
{{ t('guide.next') }}
|
||||||
|
<ChevronRightIcon :size="16" />
|
||||||
|
</button>
|
||||||
|
<button v-else class="guide-btn success" @click="handleFinish">
|
||||||
|
<CheckCircleIcon :size="16" />
|
||||||
|
{{ t('guide.finish') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TransitionRoot>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { TransitionRoot } from '@headlessui/vue'
|
||||||
|
import { useStorage } from '@vueuse/core'
|
||||||
|
import {
|
||||||
|
ArrowLeftRightIcon,
|
||||||
|
CheckCircleIcon,
|
||||||
|
ChevronLeftIcon,
|
||||||
|
ChevronRightIcon,
|
||||||
|
HelpCircleIcon,
|
||||||
|
ImageIcon,
|
||||||
|
PaletteIcon,
|
||||||
|
UploadCloudIcon,
|
||||||
|
XIcon,
|
||||||
|
} from 'lucide-vue-next'
|
||||||
|
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const hasSeenGuide = useStorage('has-seen-first-time-guide', false)
|
||||||
|
const isVisible = ref(false)
|
||||||
|
const currentStep = ref(0)
|
||||||
|
const spotlightRect = ref<DOMRect | null>(null)
|
||||||
|
|
||||||
|
interface GuideStep {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
additionalInfo?: string[]
|
||||||
|
target?: string
|
||||||
|
position?: 'top' | 'bottom' | 'left' | 'right' | 'center'
|
||||||
|
icon: any
|
||||||
|
action?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const steps: GuideStep[] = [
|
||||||
|
{
|
||||||
|
id: 'welcome',
|
||||||
|
title: 'guide.steps.welcome.title',
|
||||||
|
description: 'guide.steps.welcome.description',
|
||||||
|
position: 'center',
|
||||||
|
icon: HelpCircleIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'upload',
|
||||||
|
title: 'guide.steps.upload.title',
|
||||||
|
description: 'guide.steps.upload.description',
|
||||||
|
target: '#upload-area',
|
||||||
|
position: 'bottom',
|
||||||
|
icon: UploadCloudIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'picbed',
|
||||||
|
title: 'guide.steps.picbed.title',
|
||||||
|
description: 'guide.steps.picbed.description',
|
||||||
|
target: '.provider-button',
|
||||||
|
position: 'bottom',
|
||||||
|
icon: ArrowLeftRightIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'theme',
|
||||||
|
title: 'guide.steps.theme.title',
|
||||||
|
description: 'guide.steps.theme.description',
|
||||||
|
target: '.theme-switcher',
|
||||||
|
position: 'right',
|
||||||
|
icon: PaletteIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'themeSelection',
|
||||||
|
title: 'guide.steps.themeSelection.title',
|
||||||
|
description: 'guide.steps.themeSelection.description',
|
||||||
|
target: '.theme-dropdown',
|
||||||
|
position: 'bottom',
|
||||||
|
icon: PaletteIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gallery',
|
||||||
|
title: 'guide.steps.gallery.title',
|
||||||
|
description: 'guide.steps.gallery.description',
|
||||||
|
target: 'nav .nav-item:nth-child(3)',
|
||||||
|
position: 'right',
|
||||||
|
icon: ImageIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'finish',
|
||||||
|
title: 'guide.steps.finish.title',
|
||||||
|
description: 'guide.steps.finish.description',
|
||||||
|
position: 'center',
|
||||||
|
icon: CheckCircleIcon,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const currentStepConfig = computed(() => steps[currentStep.value])
|
||||||
|
|
||||||
|
const updateSpotlight = async () => {
|
||||||
|
await nextTick()
|
||||||
|
const target = currentStepConfig.value.target
|
||||||
|
if (!target) {
|
||||||
|
spotlightRect.value = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const element = document.querySelector(target)
|
||||||
|
if (element) {
|
||||||
|
spotlightRect.value = element.getBoundingClientRect()
|
||||||
|
} else {
|
||||||
|
spotlightRect.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const spotlightStyle = computed(() => {
|
||||||
|
if (!spotlightRect.value) return {}
|
||||||
|
|
||||||
|
const padding = 8
|
||||||
|
return {
|
||||||
|
top: `${spotlightRect.value.top - padding}px`,
|
||||||
|
left: `${spotlightRect.value.left - padding}px`,
|
||||||
|
width: `${spotlightRect.value.width + padding * 2}px`,
|
||||||
|
height: `${spotlightRect.value.height + padding * 2}px`,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const cardStyle = computed(() => {
|
||||||
|
if (!spotlightRect.value || currentStepConfig.value.position === 'center') {
|
||||||
|
return {
|
||||||
|
top: '50%',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rect = spotlightRect.value
|
||||||
|
const position = currentStepConfig.value.position || 'bottom'
|
||||||
|
const offset = 16
|
||||||
|
const cardWidth = 420
|
||||||
|
const estimatedCardHeight = 260
|
||||||
|
const padding = 12
|
||||||
|
|
||||||
|
const style: Record<string, string> = {}
|
||||||
|
|
||||||
|
const centerX = rect.left + rect.width / 2
|
||||||
|
const halfCardWidth = cardWidth / 2
|
||||||
|
|
||||||
|
let adjustedCenterX = centerX
|
||||||
|
if (centerX - halfCardWidth < padding) {
|
||||||
|
adjustedCenterX = halfCardWidth + padding
|
||||||
|
} else if (centerX + halfCardWidth > window.innerWidth - padding) {
|
||||||
|
adjustedCenterX = window.innerWidth - halfCardWidth - padding
|
||||||
|
}
|
||||||
|
|
||||||
|
if (position === 'bottom') {
|
||||||
|
const spaceBelow = window.innerHeight - rect.bottom - offset - padding
|
||||||
|
const spaceAbove = rect.top - offset - padding
|
||||||
|
|
||||||
|
if (spaceBelow >= estimatedCardHeight || spaceBelow > spaceAbove) {
|
||||||
|
style.top = `${rect.bottom + offset}px`
|
||||||
|
style.left = `${adjustedCenterX}px`
|
||||||
|
style.transform = 'translateX(-50%)'
|
||||||
|
} else {
|
||||||
|
style.bottom = `${window.innerHeight - rect.top + offset}px`
|
||||||
|
style.left = `${adjustedCenterX}px`
|
||||||
|
style.transform = 'translateX(-50%)'
|
||||||
|
}
|
||||||
|
} else if (position === 'top') {
|
||||||
|
const spaceAbove = rect.top - offset - padding
|
||||||
|
const spaceBelow = window.innerHeight - rect.bottom - offset - padding
|
||||||
|
|
||||||
|
if (spaceAbove >= estimatedCardHeight || spaceAbove > spaceBelow) {
|
||||||
|
style.bottom = `${window.innerHeight - rect.top + offset}px`
|
||||||
|
style.left = `${adjustedCenterX}px`
|
||||||
|
style.transform = 'translateX(-50%)'
|
||||||
|
} else {
|
||||||
|
style.top = `${rect.bottom + offset}px`
|
||||||
|
style.left = `${adjustedCenterX}px`
|
||||||
|
style.transform = 'translateX(-50%)'
|
||||||
|
}
|
||||||
|
} else if (position === 'right') {
|
||||||
|
const spaceRight = window.innerWidth - rect.right - offset - padding
|
||||||
|
const spaceLeft = rect.left - offset - padding
|
||||||
|
|
||||||
|
const centerY = rect.top + rect.height / 2
|
||||||
|
const adjustedCenterY = Math.max(
|
||||||
|
estimatedCardHeight / 2 + padding,
|
||||||
|
Math.min(centerY, window.innerHeight - estimatedCardHeight / 2 - padding),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (spaceRight >= cardWidth || spaceRight > spaceLeft) {
|
||||||
|
style.left = `${rect.right + offset}px`
|
||||||
|
style.top = `${adjustedCenterY}px`
|
||||||
|
style.transform = 'translateY(-50%)'
|
||||||
|
} else {
|
||||||
|
style.right = `${window.innerWidth - rect.left + offset}px`
|
||||||
|
style.top = `${adjustedCenterY}px`
|
||||||
|
style.transform = 'translateY(-50%)'
|
||||||
|
}
|
||||||
|
} else if (position === 'left') {
|
||||||
|
const spaceLeft = rect.left - offset - padding
|
||||||
|
const spaceRight = window.innerWidth - rect.right - offset - padding
|
||||||
|
|
||||||
|
const centerY = rect.top + rect.height / 2
|
||||||
|
const adjustedCenterY = Math.max(
|
||||||
|
estimatedCardHeight / 2 + padding,
|
||||||
|
Math.min(centerY, window.innerHeight - estimatedCardHeight / 2 - padding),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (spaceLeft >= cardWidth || spaceLeft > spaceRight) {
|
||||||
|
style.right = `${window.innerWidth - rect.left + offset}px`
|
||||||
|
style.top = `${adjustedCenterY}px`
|
||||||
|
style.transform = 'translateY(-50%)'
|
||||||
|
} else {
|
||||||
|
style.left = `${rect.right + offset}px`
|
||||||
|
style.top = `${adjustedCenterY}px`
|
||||||
|
style.transform = 'translateY(-50%)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return style
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(currentStep, () => {
|
||||||
|
updateSpotlight()
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => route.path,
|
||||||
|
() => {
|
||||||
|
if (isVisible.value) {
|
||||||
|
updateSpotlight()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleNext = async () => {
|
||||||
|
if (currentStep.value < steps.length - 1) {
|
||||||
|
currentStep.value++
|
||||||
|
|
||||||
|
if (currentStep.value === 4) {
|
||||||
|
await router.push('/main-page/settings')
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 400))
|
||||||
|
await updateSpotlight()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePrevious = () => {
|
||||||
|
if (currentStep.value > 0) {
|
||||||
|
currentStep.value--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSkip = () => {
|
||||||
|
isVisible.value = false
|
||||||
|
hasSeenGuide.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
isVisible.value = false
|
||||||
|
hasSeenGuide.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFinish = () => {
|
||||||
|
isVisible.value = false
|
||||||
|
hasSeenGuide.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const restartGuide = () => {
|
||||||
|
currentStep.value = 0
|
||||||
|
isVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
restartGuide,
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (!hasSeenGuide.value) {
|
||||||
|
setTimeout(() => {
|
||||||
|
isVisible.value = true
|
||||||
|
updateSpotlight()
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('resize', updateSpotlight)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped src="./css/FirstTimeGuide.css"></style>
|
||||||
@@ -85,6 +85,9 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
<FirstTimeGuide ref="guideRef" />
|
||||||
|
|
||||||
<TransitionRoot appear :show="qrcodeVisible" as="template">
|
<TransitionRoot appear :show="qrcodeVisible" as="template">
|
||||||
<Dialog as="div" class="qr-dialog" @close="qrcodeVisible = false">
|
<Dialog as="div" class="qr-dialog" @close="qrcodeVisible = false">
|
||||||
<div class="dialog-container">
|
<div class="dialog-container">
|
||||||
@@ -202,11 +205,13 @@ import { useRoute, useRouter } from 'vue-router'
|
|||||||
import { usePicBed } from '@/hooks/useGlobal'
|
import { usePicBed } from '@/hooks/useGlobal'
|
||||||
import useMessage from '@/hooks/useMessage'
|
import useMessage from '@/hooks/useMessage'
|
||||||
import * as config from '@/router/config'
|
import * as config from '@/router/config'
|
||||||
import { SHOW_MAIN_PAGE_QRCODE } from '@/utils/constant'
|
import { SHOW_FIRST_TIME_GUIDE, SHOW_MAIN_PAGE_QRCODE } from '@/utils/constant'
|
||||||
import { getConfig } from '@/utils/dataSender'
|
import { getConfig } from '@/utils/dataSender'
|
||||||
import { IRPCActionType } from '@/utils/enum'
|
import { IRPCActionType } from '@/utils/enum'
|
||||||
|
|
||||||
|
import FirstTimeGuide from './FirstTimeGuide.vue'
|
||||||
import ThemeSwitcher from './ui/ThemeSwitcher.vue'
|
import ThemeSwitcher from './ui/ThemeSwitcher.vue'
|
||||||
|
|
||||||
const version = ref(pkg.version)
|
const version = ref(pkg.version)
|
||||||
const isCollapsed = useStorage('navigation-collapsed', false)
|
const isCollapsed = useStorage('navigation-collapsed', false)
|
||||||
|
|
||||||
@@ -220,6 +225,7 @@ const routerConfig = reactive(config)
|
|||||||
const qrcodeVisible = ref(false)
|
const qrcodeVisible = ref(false)
|
||||||
const choosedPicBedForQRCode: Ref<string[]> = ref([])
|
const choosedPicBedForQRCode: Ref<string[]> = ref([])
|
||||||
const picBedConfigString = ref('')
|
const picBedConfigString = ref('')
|
||||||
|
const guideRef = ref<InstanceType<typeof FirstTimeGuide> | null>(null)
|
||||||
|
|
||||||
let removeIpcListener: () => void = () => {}
|
let removeIpcListener: () => void = () => {}
|
||||||
|
|
||||||
@@ -243,6 +249,10 @@ const qrCodeHandler = () => {
|
|||||||
qrcodeVisible.value = true
|
qrcodeVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const guideHandler = () => {
|
||||||
|
guideRef.value?.restartGuide()
|
||||||
|
}
|
||||||
|
|
||||||
function openMenu() {
|
function openMenu() {
|
||||||
window.electron.sendRPC(IRPCActionType.SHOW_MAIN_PAGE_MENU)
|
window.electron.sendRPC(IRPCActionType.SHOW_MAIN_PAGE_MENU)
|
||||||
}
|
}
|
||||||
@@ -288,6 +298,13 @@ function openGithubPage() {
|
|||||||
|
|
||||||
onBeforeMount(() => {
|
onBeforeMount(() => {
|
||||||
removeIpcListener = window.electron.ipcRendererOn(SHOW_MAIN_PAGE_QRCODE, qrCodeHandler)
|
removeIpcListener = window.electron.ipcRendererOn(SHOW_MAIN_PAGE_QRCODE, qrCodeHandler)
|
||||||
|
const removeGuideListener = window.electron.ipcRendererOn(SHOW_FIRST_TIME_GUIDE, guideHandler)
|
||||||
|
|
||||||
|
const originalRemove = removeIpcListener
|
||||||
|
removeIpcListener = () => {
|
||||||
|
originalRemove()
|
||||||
|
removeGuideListener()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
|
|||||||
280
src/renderer/components/css/FirstTimeGuide.css
Normal file
280
src/renderer/components/css/FirstTimeGuide.css
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
.guide-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 9999;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-backdrop {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: rgb(0 0 0 / 10%);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-overlay.advancedAnimation .guide-backdrop {
|
||||||
|
animation: fade-in 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-spotlight {
|
||||||
|
position: absolute;
|
||||||
|
border: 2px solid var(--color-accent);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 9999px rgb(0 0 0 / 10%),
|
||||||
|
0 0 20px rgb(255 255 255 / 30%),
|
||||||
|
inset 0 0 20px rgb(255 255 255 / 10%);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 10000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-card {
|
||||||
|
position: absolute;
|
||||||
|
background: var(--color-background-tertiary);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 10px 40px rgb(0 0 0 / 25%);
|
||||||
|
width: 420px;
|
||||||
|
max-width: 90vw;
|
||||||
|
max-height: 80vh;
|
||||||
|
overflow: auto;
|
||||||
|
z-index: 10001;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-overlay.advancedAnimation .guide-card {
|
||||||
|
animation: slide-up 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 12px 18px;
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-header-left {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-step-indicator {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--color-text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-close {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 6px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-close:hover {
|
||||||
|
background: var(--color-accent-hover);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-content {
|
||||||
|
padding: 14px 18px;
|
||||||
|
display: flex;
|
||||||
|
gap: 14px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--color-accent);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-text {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-content-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
margin: 0 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-content-description {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
line-height: 1.5;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-additional-info {
|
||||||
|
background: var(--color-background-tertiary);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-additional-info p {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
line-height: 1.4;
|
||||||
|
margin: 6px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-additional-info p:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-additional-info p:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-footer {
|
||||||
|
padding: 10px 18px 12px;
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-progress {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-dot {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--color-border);
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-dot.active {
|
||||||
|
background: var(--color-primary);
|
||||||
|
width: 20px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-dot.completed {
|
||||||
|
background: var(--color-success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-btn {
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-btn.primary {
|
||||||
|
background: var(--color-accent);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-btn.primary:hover {
|
||||||
|
background: var(--color-accent-hover);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-btn.secondary {
|
||||||
|
background: var(--color-background-tertiary);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-btn.secondary:hover {
|
||||||
|
background: var(--color-accent-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-btn.outline {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-btn.outline:hover {
|
||||||
|
background: var(--color-accent);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-btn.success {
|
||||||
|
background: var(--color-success);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-btn.success:hover {
|
||||||
|
background: var(--color-success);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fade-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slide-up {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translate(-50%, -40%) scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translate(-50%, -50%) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (width <= 640px) {
|
||||||
|
.guide-card {
|
||||||
|
width: calc(100vw - 32px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-actions {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-btn {
|
||||||
|
flex: 1;
|
||||||
|
min-width: calc(50% - 4px);
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,49 @@
|
|||||||
"submit": "Submit",
|
"submit": "Submit",
|
||||||
"version": "Version"
|
"version": "Version"
|
||||||
},
|
},
|
||||||
|
"guide": {
|
||||||
|
"close": "Close",
|
||||||
|
"finish": "Get Started",
|
||||||
|
"next": "Next",
|
||||||
|
"previous": "Back",
|
||||||
|
"skip": "Skip Tour",
|
||||||
|
"stepIndicator": "Step {current} of {total}",
|
||||||
|
"steps": {
|
||||||
|
"finish": {
|
||||||
|
"description": "You're ready to start using PicList! You can always restart this guide from the menu.",
|
||||||
|
"title": "You're All Set! ✨"
|
||||||
|
},
|
||||||
|
"gallery": {
|
||||||
|
"description": "Access your uploaded images in the gallery. You can search, filter, and manage your image collection.",
|
||||||
|
"title": "View Your Gallery"
|
||||||
|
},
|
||||||
|
"picbed": {
|
||||||
|
"description": "Click here to view and configure your current image hosting service (PicBed).",
|
||||||
|
"title": "Choose Your Image Host"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"description": "Customize PicList to suit your needs. Configure upload settings, shortcuts, and more.",
|
||||||
|
"title": "Configure Settings"
|
||||||
|
},
|
||||||
|
"theme": {
|
||||||
|
"description": "Click the theme switcher in the sidebar to change the appearance of PicList.",
|
||||||
|
"title": "Customize Your Theme 🎨"
|
||||||
|
},
|
||||||
|
"themeSelection": {
|
||||||
|
"description": "Select a theme from the dropdown to customize PicList's appearance.",
|
||||||
|
"title": "Choose Your Theme"
|
||||||
|
},
|
||||||
|
"upload": {
|
||||||
|
"description": "You can upload images by dragging and dropping them here, or click to select files.",
|
||||||
|
"title": "Upload Your Images"
|
||||||
|
},
|
||||||
|
"welcome": {
|
||||||
|
"description": "Let's take a quick tour to help you get started with PicList.",
|
||||||
|
"title": "Welcome to PicList! 🎉"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "Welcome to PicList"
|
||||||
|
},
|
||||||
"navigation": {
|
"navigation": {
|
||||||
"choosePicBed": "Choose PicBed",
|
"choosePicBed": "Choose PicBed",
|
||||||
"close": "Close",
|
"close": "Close",
|
||||||
|
|||||||
@@ -9,6 +9,49 @@
|
|||||||
"submit": "提交",
|
"submit": "提交",
|
||||||
"version": "版本"
|
"version": "版本"
|
||||||
},
|
},
|
||||||
|
"guide": {
|
||||||
|
"close": "关闭",
|
||||||
|
"finish": "开始使用",
|
||||||
|
"next": "下一步",
|
||||||
|
"previous": "上一步",
|
||||||
|
"skip": "跳过教程",
|
||||||
|
"stepIndicator": "第 {current} 步,共 {total} 步",
|
||||||
|
"steps": {
|
||||||
|
"finish": {
|
||||||
|
"description": "您已经准备好开始使用 PicList 了!您可以随时从菜单重新启动本指南。",
|
||||||
|
"title": "一切就绪!✨"
|
||||||
|
},
|
||||||
|
"gallery": {
|
||||||
|
"description": "在相册中访问您上传的图片。您可以搜索、筛选和管理您的图片集。",
|
||||||
|
"title": "查看相册"
|
||||||
|
},
|
||||||
|
"picbed": {
|
||||||
|
"description": "点击这里查看和配置您当前使用的图床服务。",
|
||||||
|
"title": "选择图床服务"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"description": "自定义 PicList 以满足您的需求。配置上传设置、快捷键等。",
|
||||||
|
"title": "配置设置"
|
||||||
|
},
|
||||||
|
"theme": {
|
||||||
|
"description": "点击侧边栏中的主题切换器来改变 PicList 的外观。",
|
||||||
|
"title": "自定义主题 🎨"
|
||||||
|
},
|
||||||
|
"themeSelection": {
|
||||||
|
"description": "从下拉菜单中选择主题来自定义 PicList 的外观。",
|
||||||
|
"title": "选择主题"
|
||||||
|
},
|
||||||
|
"upload": {
|
||||||
|
"description": "您可以通过拖放图片到这里,或点击选择文件来上传图片。",
|
||||||
|
"title": "上传您的图片"
|
||||||
|
},
|
||||||
|
"welcome": {
|
||||||
|
"description": "让我们快速了解一下 PicList 的主要功能吧。",
|
||||||
|
"title": "欢迎使用 PicList!🎉"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "欢迎使用 PicList"
|
||||||
|
},
|
||||||
"navigation": {
|
"navigation": {
|
||||||
"choosePicBed": "选择图床",
|
"choosePicBed": "选择图床",
|
||||||
"close": "关闭",
|
"close": "关闭",
|
||||||
|
|||||||
@@ -9,6 +9,49 @@
|
|||||||
"submit": "提交",
|
"submit": "提交",
|
||||||
"version": "版本"
|
"version": "版本"
|
||||||
},
|
},
|
||||||
|
"guide": {
|
||||||
|
"close": "關閉",
|
||||||
|
"finish": "開始使用",
|
||||||
|
"next": "下一步",
|
||||||
|
"previous": "上一步",
|
||||||
|
"skip": "跳過教學",
|
||||||
|
"stepIndicator": "第 {current} 步,共 {total} 步",
|
||||||
|
"steps": {
|
||||||
|
"finish": {
|
||||||
|
"description": "您已經準備好開始使用 PicList 了!您可以隨時從選單重新啟動本指南。",
|
||||||
|
"title": "一切就緒!✨"
|
||||||
|
},
|
||||||
|
"gallery": {
|
||||||
|
"description": "在相簿中存取您上傳的圖片。您可以搜尋、篩選和管理您的圖片集。",
|
||||||
|
"title": "檢視相簿"
|
||||||
|
},
|
||||||
|
"picbed": {
|
||||||
|
"description": "點擊這裡檢視和設定您目前使用的圖床服務。",
|
||||||
|
"title": "選擇圖床服務"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"description": "自訂 PicList 以滿足您的需求。設定上傳設定、快捷鍵等。",
|
||||||
|
"title": "設定偏好"
|
||||||
|
},
|
||||||
|
"theme": {
|
||||||
|
"description": "點擊側邊欄中的主題切換器來變更 PicList 的外觀。",
|
||||||
|
"title": "自訂主題 🎨"
|
||||||
|
},
|
||||||
|
"themeSelection": {
|
||||||
|
"description": "從下拉選單中選擇主題來自訂 PicList 的外觀。",
|
||||||
|
"title": "選擇主題"
|
||||||
|
},
|
||||||
|
"upload": {
|
||||||
|
"description": "您可以透過拖放圖片到這裡,或點擊選擇檔案來上傳圖片。",
|
||||||
|
"title": "上傳您的圖片"
|
||||||
|
},
|
||||||
|
"welcome": {
|
||||||
|
"description": "讓我們快速了解一下 PicList 的主要功能吧。",
|
||||||
|
"title": "歡迎使用 PicList!🎉"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "歡迎使用 PicList"
|
||||||
|
},
|
||||||
"navigation": {
|
"navigation": {
|
||||||
"choosePicBed": "選擇圖床",
|
"choosePicBed": "選擇圖床",
|
||||||
"close": "關閉",
|
"close": "關閉",
|
||||||
|
|||||||
@@ -101,7 +101,7 @@
|
|||||||
<ImageIcon :size="18" />
|
<ImageIcon :size="18" />
|
||||||
<span>{{ t('pages.settings.system.chooseTheme') }}</span>
|
<span>{{ t('pages.settings.system.chooseTheme') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<select v-model="currentTheme" class="form-select">
|
<select v-model="currentTheme" class="form-select theme-dropdown">
|
||||||
<option v-for="theme in themeList" :key="theme.key" :value="theme.key">
|
<option v-for="theme in themeList" :key="theme.key" :value="theme.key">
|
||||||
{{ theme.label }}
|
{{ theme.label }}
|
||||||
</option>
|
</option>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export const PICGO_TOGGLE_PLUGIN = 'PICGO_TOGGLE_PLUGIN'
|
|||||||
export const RENAME_FILE_NAME = 'RENAME_FILE_NAME'
|
export const RENAME_FILE_NAME = 'RENAME_FILE_NAME'
|
||||||
export const GET_RENAME_FILE_NAME = 'GET_RENAME_FILE_NAME'
|
export const GET_RENAME_FILE_NAME = 'GET_RENAME_FILE_NAME'
|
||||||
export const SHOW_MAIN_PAGE_QRCODE = 'SHOW_MAIN_PAGE_QRCODE'
|
export const SHOW_MAIN_PAGE_QRCODE = 'SHOW_MAIN_PAGE_QRCODE'
|
||||||
|
export const SHOW_FIRST_TIME_GUIDE = 'SHOW_FIRST_TIME_GUIDE'
|
||||||
// update window
|
// update window
|
||||||
export const SHOW_UPDATE_INFO = 'SHOW_UPDATE_INFO'
|
export const SHOW_UPDATE_INFO = 'SHOW_UPDATE_INFO'
|
||||||
export const UPDATE_PROGRESS = 'UPDATE_PROGRESS'
|
export const UPDATE_PROGRESS = 'UPDATE_PROGRESS'
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export interface ILocales {
|
|||||||
QUICK_UPLOAD: string
|
QUICK_UPLOAD: string
|
||||||
UPLOAD_BY_CLIPBOARD: string
|
UPLOAD_BY_CLIPBOARD: string
|
||||||
SHOW_PICBED_QRCODE: string
|
SHOW_PICBED_QRCODE: string
|
||||||
|
SHOW_FIRST_TIME_GUIDE: string
|
||||||
ENABLE: string
|
ENABLE: string
|
||||||
DISABLE: string
|
DISABLE: string
|
||||||
CONFIG_THING: string
|
CONFIG_THING: string
|
||||||
|
|||||||
Reference in New Issue
Block a user