mirror of
https://github.com/Kuingsmile/PicList.git
synced 2026-05-06 20:42:57 +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
|
||||
UPLOAD_BY_CLIPBOARD: Upload by Clipboard
|
||||
SHOW_PICBED_QRCODE: Show Picbed Qrcode
|
||||
SHOW_FIRST_TIME_GUIDE: Show First-Time Guide
|
||||
ENABLE: Enable
|
||||
DISABLE: Disable
|
||||
CONFIG_THING: Config ${c}
|
||||
|
||||
@@ -13,6 +13,7 @@ UPLOADING: 正在上传
|
||||
QUICK_UPLOAD: 快捷上传
|
||||
UPLOAD_BY_CLIPBOARD: 剪贴板图片上传
|
||||
SHOW_PICBED_QRCODE: 生成图床配置二维码
|
||||
SHOW_FIRST_TIME_GUIDE: 显示新手指南
|
||||
ENABLE: 启用
|
||||
DISABLE: 禁用
|
||||
CONFIG_THING: 配置${c}
|
||||
|
||||
@@ -13,6 +13,7 @@ UPLOADING: 正在上傳
|
||||
QUICK_UPLOAD: 快速上傳
|
||||
UPLOAD_BY_CLIPBOARD: 剪貼簿圖片上傳
|
||||
SHOW_PICBED_QRCODE: 產生圖床配置 QRCODE
|
||||
SHOW_FIRST_TIME_GUIDE: 顯示新手指南
|
||||
ENABLE: 啟用
|
||||
DISABLE: 禁用
|
||||
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 GET_RENAME_FILE_NAME = 'GET_RENAME_FILE_NAME'
|
||||
export const SHOW_MAIN_PAGE_QRCODE = 'SHOW_MAIN_PAGE_QRCODE'
|
||||
export const SHOW_FIRST_TIME_GUIDE = 'SHOW_FIRST_TIME_GUIDE'
|
||||
// rpc
|
||||
export const RPC_ACTIONS = 'RPC_ACTIONS'
|
||||
export const RPC_ACTIONS_INVOKE = 'RPC_ACTIONS_INVOKE'
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
PICGO_HANDLE_PLUGIN_DONE,
|
||||
PICGO_HANDLE_PLUGIN_ING,
|
||||
PICGO_TOGGLE_PLUGIN,
|
||||
SHOW_FIRST_TIME_GUIDE,
|
||||
SHOW_MAIN_PAGE_QRCODE,
|
||||
} from '~/events/constant'
|
||||
import { handlePluginUninstall, handlePluginUpdate } from '~/events/rpc/routes/plugin/utils'
|
||||
@@ -111,6 +112,12 @@ const buildMainPageMenu = (win: BrowserWindow) => {
|
||||
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'),
|
||||
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>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<FirstTimeGuide ref="guideRef" />
|
||||
|
||||
<TransitionRoot appear :show="qrcodeVisible" as="template">
|
||||
<Dialog as="div" class="qr-dialog" @close="qrcodeVisible = false">
|
||||
<div class="dialog-container">
|
||||
@@ -202,11 +205,13 @@ import { useRoute, useRouter } from 'vue-router'
|
||||
import { usePicBed } from '@/hooks/useGlobal'
|
||||
import useMessage from '@/hooks/useMessage'
|
||||
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 { IRPCActionType } from '@/utils/enum'
|
||||
|
||||
import FirstTimeGuide from './FirstTimeGuide.vue'
|
||||
import ThemeSwitcher from './ui/ThemeSwitcher.vue'
|
||||
|
||||
const version = ref(pkg.version)
|
||||
const isCollapsed = useStorage('navigation-collapsed', false)
|
||||
|
||||
@@ -220,6 +225,7 @@ const routerConfig = reactive(config)
|
||||
const qrcodeVisible = ref(false)
|
||||
const choosedPicBedForQRCode: Ref<string[]> = ref([])
|
||||
const picBedConfigString = ref('')
|
||||
const guideRef = ref<InstanceType<typeof FirstTimeGuide> | null>(null)
|
||||
|
||||
let removeIpcListener: () => void = () => {}
|
||||
|
||||
@@ -243,6 +249,10 @@ const qrCodeHandler = () => {
|
||||
qrcodeVisible.value = true
|
||||
}
|
||||
|
||||
const guideHandler = () => {
|
||||
guideRef.value?.restartGuide()
|
||||
}
|
||||
|
||||
function openMenu() {
|
||||
window.electron.sendRPC(IRPCActionType.SHOW_MAIN_PAGE_MENU)
|
||||
}
|
||||
@@ -288,6 +298,13 @@ function openGithubPage() {
|
||||
|
||||
onBeforeMount(() => {
|
||||
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(() => {
|
||||
|
||||
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",
|
||||
"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": {
|
||||
"choosePicBed": "Choose PicBed",
|
||||
"close": "Close",
|
||||
|
||||
@@ -9,6 +9,49 @@
|
||||
"submit": "提交",
|
||||
"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": {
|
||||
"choosePicBed": "选择图床",
|
||||
"close": "关闭",
|
||||
|
||||
@@ -9,6 +9,49 @@
|
||||
"submit": "提交",
|
||||
"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": {
|
||||
"choosePicBed": "選擇圖床",
|
||||
"close": "關閉",
|
||||
|
||||
@@ -101,7 +101,7 @@
|
||||
<ImageIcon :size="18" />
|
||||
<span>{{ t('pages.settings.system.chooseTheme') }}</span>
|
||||
</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">
|
||||
{{ theme.label }}
|
||||
</option>
|
||||
|
||||
@@ -9,6 +9,7 @@ export const PICGO_TOGGLE_PLUGIN = 'PICGO_TOGGLE_PLUGIN'
|
||||
export const RENAME_FILE_NAME = '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_FIRST_TIME_GUIDE = 'SHOW_FIRST_TIME_GUIDE'
|
||||
// update window
|
||||
export const SHOW_UPDATE_INFO = 'SHOW_UPDATE_INFO'
|
||||
export const UPDATE_PROGRESS = 'UPDATE_PROGRESS'
|
||||
|
||||
@@ -14,6 +14,7 @@ export interface ILocales {
|
||||
QUICK_UPLOAD: string
|
||||
UPLOAD_BY_CLIPBOARD: string
|
||||
SHOW_PICBED_QRCODE: string
|
||||
SHOW_FIRST_TIME_GUIDE: string
|
||||
ENABLE: string
|
||||
DISABLE: string
|
||||
CONFIG_THING: string
|
||||
|
||||
Reference in New Issue
Block a user