feat(frontend): 实现新手引导功能
- 添加 Guide 组件和引导步骤配置 - 实现 useOnboardingTour 和 useTourStepDescription composables - 添加 onboarding store 管理引导状态 - 更新多个视图和组件以支持引导功能 - 添加国际化支持(中英文) - 删除旧的实现指南文档
This commit is contained in:
@@ -200,6 +200,14 @@
|
||||
</div>
|
||||
|
||||
<div class="border-t border-gray-100 py-1 dark:border-dark-700">
|
||||
<button @click="handleReplayGuide" class="dropdown-item w-full">
|
||||
<svg class="h-4 w-4" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M12 2a10 10 0 100 20 10 10 0 000-20zm0 14a1 1 0 110 2 1 1 0 010-2zm1.07-7.75c0-.6-.49-1.25-1.32-1.25-.7 0-1.22.4-1.43 1.02a1 1 0 11-1.9-.62A3.41 3.41 0 0111.8 5c2.02 0 3.25 1.4 3.25 2.9 0 2-1.83 2.55-2.43 3.12-.43.4-.47.75-.47 1.23a1 1 0 01-2 0c0-1 .16-1.82 1.1-2.7.69-.64 1.82-1.05 1.82-2.06z"
|
||||
/>
|
||||
</svg>
|
||||
{{ $t('onboarding.restartTour') }}
|
||||
</button>
|
||||
<button
|
||||
@click="handleLogout"
|
||||
class="dropdown-item w-full text-red-600 hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-900/20"
|
||||
@@ -232,7 +240,7 @@
|
||||
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAppStore, useAuthStore } from '@/stores'
|
||||
import { useAppStore, useAuthStore, useOnboardingStore } from '@/stores'
|
||||
import LocaleSwitcher from '@/components/common/LocaleSwitcher.vue'
|
||||
import SubscriptionProgressMini from '@/components/common/SubscriptionProgressMini.vue'
|
||||
|
||||
@@ -241,6 +249,7 @@ const route = useRoute()
|
||||
const { t } = useI18n()
|
||||
const appStore = useAppStore()
|
||||
const authStore = useAuthStore()
|
||||
const onboardingStore = useOnboardingStore()
|
||||
|
||||
const user = computed(() => authStore.user)
|
||||
const dropdownOpen = ref(false)
|
||||
@@ -300,6 +309,11 @@ async function handleLogout() {
|
||||
await router.push('/login')
|
||||
}
|
||||
|
||||
function handleReplayGuide() {
|
||||
closeDropdown()
|
||||
onboardingStore.replay()
|
||||
}
|
||||
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
if (dropdownRef.value && !dropdownRef.value.contains(event.target as Node)) {
|
||||
closeDropdown()
|
||||
|
||||
@@ -23,11 +23,34 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import '@/styles/onboarding.css'
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAppStore } from '@/stores'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useOnboardingTour } from '@/composables/useOnboardingTour'
|
||||
import { getAdminSteps, getUserSteps } from '@/components/Guide/steps'
|
||||
import { useOnboardingStore } from '@/stores/onboarding'
|
||||
import AppSidebar from './AppSidebar.vue'
|
||||
import AppHeader from './AppHeader.vue'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const authStore = useAuthStore()
|
||||
const { t } = useI18n()
|
||||
const sidebarCollapsed = computed(() => appStore.sidebarCollapsed)
|
||||
const isAdmin = computed(() => authStore.user?.role === 'admin')
|
||||
|
||||
const { replayTour } = useOnboardingTour({
|
||||
steps: isAdmin.value ? getAdminSteps(t) : getUserSteps(t),
|
||||
storageKey: isAdmin.value ? 'admin_guide' : 'user_guide',
|
||||
autoStart: true
|
||||
})
|
||||
|
||||
const onboardingStore = useOnboardingStore()
|
||||
|
||||
onMounted(() => {
|
||||
onboardingStore.setReplayCallback(replayTour)
|
||||
})
|
||||
|
||||
defineExpose({ replayTour })
|
||||
</script>
|
||||
|
||||
@@ -36,7 +36,16 @@
|
||||
class="sidebar-link mb-1"
|
||||
:class="{ 'sidebar-link-active': isActive(item.path) }"
|
||||
:title="sidebarCollapsed ? item.label : undefined"
|
||||
@click="handleMenuItemClick"
|
||||
:id="
|
||||
item.path === '/admin/accounts'
|
||||
? 'sidebar-channel-manage'
|
||||
: item.path === '/admin/groups'
|
||||
? 'sidebar-group-manage'
|
||||
: item.path === '/admin/redeem'
|
||||
? 'sidebar-wallet'
|
||||
: undefined
|
||||
"
|
||||
@click="handleMenuItemClick(item.path)"
|
||||
>
|
||||
<component :is="item.icon" class="h-5 w-5 flex-shrink-0" />
|
||||
<transition name="fade">
|
||||
@@ -59,7 +68,8 @@
|
||||
class="sidebar-link mb-1"
|
||||
:class="{ 'sidebar-link-active': isActive(item.path) }"
|
||||
:title="sidebarCollapsed ? item.label : undefined"
|
||||
@click="handleMenuItemClick"
|
||||
:data-tour="item.path === '/keys' ? 'sidebar-my-keys' : undefined"
|
||||
@click="handleMenuItemClick(item.path)"
|
||||
>
|
||||
<component :is="item.icon" class="h-5 w-5 flex-shrink-0" />
|
||||
<transition name="fade">
|
||||
@@ -79,7 +89,8 @@
|
||||
class="sidebar-link mb-1"
|
||||
:class="{ 'sidebar-link-active': isActive(item.path) }"
|
||||
:title="sidebarCollapsed ? item.label : undefined"
|
||||
@click="handleMenuItemClick"
|
||||
:data-tour="item.path === '/keys' ? 'sidebar-my-keys' : undefined"
|
||||
@click="handleMenuItemClick(item.path)"
|
||||
>
|
||||
<component :is="item.icon" class="h-5 w-5 flex-shrink-0" />
|
||||
<transition name="fade">
|
||||
@@ -136,7 +147,7 @@
|
||||
import { computed, h, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAppStore, useAuthStore } from '@/stores'
|
||||
import { useAppStore, useAuthStore, useOnboardingStore } from '@/stores'
|
||||
import VersionBadge from '@/components/common/VersionBadge.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
@@ -144,6 +155,7 @@ const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
const appStore = useAppStore()
|
||||
const authStore = useAuthStore()
|
||||
const onboardingStore = useOnboardingStore()
|
||||
|
||||
const sidebarCollapsed = computed(() => appStore.sidebarCollapsed)
|
||||
const mobileOpen = computed(() => appStore.mobileOpen)
|
||||
@@ -465,12 +477,24 @@ function closeMobile() {
|
||||
appStore.setMobileOpen(false)
|
||||
}
|
||||
|
||||
function handleMenuItemClick() {
|
||||
function handleMenuItemClick(itemPath: string) {
|
||||
if (mobileOpen.value) {
|
||||
setTimeout(() => {
|
||||
appStore.setMobileOpen(false)
|
||||
}, 150)
|
||||
}
|
||||
|
||||
// Map paths to tour selectors
|
||||
const pathToSelector: Record<string, string> = {
|
||||
'/admin/groups': '#sidebar-group-manage',
|
||||
'/admin/accounts': '#sidebar-channel-manage',
|
||||
'/keys': '[data-tour="sidebar-my-keys"]'
|
||||
}
|
||||
|
||||
const selector = pathToSelector[itemPath]
|
||||
if (selector && onboardingStore.isCurrentStep(selector)) {
|
||||
onboardingStore.nextStep(500)
|
||||
}
|
||||
}
|
||||
|
||||
function isActive(path: string): boolean {
|
||||
|
||||
Reference in New Issue
Block a user