feat(homepage): add guided tour and version display functionality

- Add version API endpoint and response interface in common.ts
- Implement interactive guided tour with 4 steps for new users covering Model Management, Space Management, and User Management
- Add tour translation keys for both English and Chinese locales
- Add data-menu-id attributes to sidebar menu items for tour targeting
- Create VersionCard component to display current platform version
- Update GuideCard component with tour state management and navigation logic
- Enhance homepage dashboard with version information display
- Improve user onboarding experience with step-by-step guided navigation
This commit is contained in:
yujiangping
2026-01-12 16:32:58 +08:00
parent d6b1c2effb
commit d957e27501
9 changed files with 189 additions and 53 deletions

View File

@@ -1,31 +1,92 @@
import React from 'react';
import React, { useState, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import guideBgImg from '@/assets/images/index/guide_bg@2x.png'
import { Button } from 'antd';
import { ArrowRightOutlined } from '@ant-design/icons'
import { Button, Tour } from 'antd';
import type { TourProps } from 'antd';
import arrowRight from '@/assets/images/index/arrow_right_blue.svg'
const GuideCard: React.FC = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const [open, setOpen] = useState<boolean>(false);
const [currentStep, setCurrentStep] = useState<number>(0);
const startButtonRef = useRef<HTMLButtonElement>(null);
// Tour 步骤配置
const steps: TourProps['steps'] = [
{
title: t('indexTour.startTitle'),
description: t('indexTour.startDescription'),
target: () => startButtonRef.current!,
},
{
title: t('indexTour.stepOne'),
description: t('indexTour.stepOneDescription'),
target: () => document.querySelector('[data-menu-id="/model"]') as HTMLElement,
},
{
title: t('indexTour.stepTwo'),
description: t('indexTour.stepTwoDescription'),
target: () => document.querySelector('[data-menu-id="/space"]') as HTMLElement,
},
{
title: t('indexTour.stepThree'),
description: t('indexTour.stepThreeDescription'),
target: () => document.querySelector('[data-menu-id="/user-management"]') as HTMLElement,
}
];
// 开始引导
const handleStartGuide = () => {
setCurrentStep(0);
setOpen(true);
};
// Tour 步骤变化处理
const handleStepChange = (current: number) => {
setCurrentStep(current);
// 不再自动跳转页面,让用户通过点击菜单项来导航
};
// Tour 完成处理
const handleTourFinish = () => {
setOpen(false);
setCurrentStep(0);
// 完成后导航到模型管理页面
navigate('/model');
};
return (
<>
<div className='rb:w-full rb:h-[204px] rb:p-4' style={{ backgroundImage: `url(${guideBgImg})`, backgroundSize: '100% 100%' }}>
<div className='rb:flex rb:justify-start rb:text-white rb:text-base rb:font-semibold'>
<div className='rb:flex rb:justify-start rb:text-white rb:text-base rb:font-semibold' >
{ t('index.getStarted')}
</div>
<div className='rb:flex rb:text-xs rb:text-white rb:leading-[18px] rb:mt-3'>
{ t('index.startedDesc')}
</div>
<div className='rb:flex rb:w-full rb:items-center rb:justify-between rb:gap-3 rb:mt-4'>
<Button className='rb:gap-2 rb:flex rb:items-center rb:text-[#155EEF] '>
<Button ref={startButtonRef} className='rb:gap-2 rb:w-full rb:flex rb:items-center rb:text-[#155EEF]' onClick={handleStartGuide}>
<span className='rb:text-xs'>{ t('index.viewGuide')}</span>
<img src={arrowRight} className='rb:size-4' />
</Button>
<Button className='rb:gap-2 rb:flex rb:items-center rb:text-[#155EEF]'>
{/* <Button className='rb:gap-2 rb:flex rb:items-center rb:text-[#155EEF]'>
<span className='rb:text-xs'>{ t('index.watchVideo')}</span>
<img src={arrowRight} className='rb:size-4' />
</Button>
</Button> */}
</div>
</div>
<Tour
open={open}
onClose={() => setOpen(false)}
steps={steps}
current={currentStep}
onChange={handleStepChange}
onFinish={handleTourFinish}
/>
</>
);
};

View File

@@ -3,11 +3,7 @@ import { useTranslation } from 'react-i18next';
import modelIcon from '@/assets/images/index/model_mgt.svg'
import spaceIcon from '@/assets/images/index/space_mgt.svg'
import workflowIcon from '@/assets/images/index/workflow_mgt.svg'
import userIcon from '@/assets/images/index/user_mgt.svg'
import dataExportIcon from '@/assets/images/index/data_export.svg'
import logIcon from '@/assets/images/index/log_mgt.svg'
import noteIcon from '@/assets/images/index/note_mgt.svg'
import helpCenterIcon from '@/assets/images/index/help_center.svg'
interface QuickAction {
key: string;
@@ -22,14 +18,30 @@ interface QuickActionsProps {
}
const QuickActions: FC<QuickActionsProps> = ({ onNavigate }) => {
const { t } = useTranslation();
const { t, i18n } = useTranslation();
// 根据当前语言环境打开帮助中心
const openHelpCenter = () => {
const currentLang = i18n.language;
const lang = currentLang === 'zh' ? 'zh' : 'en';
const helpUrl = `https://docs.redbearai.com/s/${lang}-memorybear`;
// 创建隐藏的 a 标签来避免弹窗拦截
const link = document.createElement('a');
link.href = helpUrl;
link.target = '_blank';
link.rel = 'noopener noreferrer';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
const quickActions: QuickAction[] = [
{
key: 'model-management',
icon: modelIcon,
title: t('quickActions.modelManagement'),
onClick: () => onNavigate?.('/model-management')
onClick: () => onNavigate?.('/model')
},
{
key: 'space-management',
@@ -37,42 +49,42 @@ const QuickActions: FC<QuickActionsProps> = ({ onNavigate }) => {
title: t('quickActions.spaceManagement'),
onClick: () => onNavigate?.('/spce')
},
{
key: 'workflow-orchestration',
icon: workflowIcon,
title: t('quickActions.workflowOrchestration'),
onClick: () => onNavigate?.('/workflow')
},
// {
// key: 'workflow-orchestration',
// icon: workflowIcon,
// title: t('quickActions.workflowOrchestration'),
// onClick: () => onNavigate?.('/workflow')
// },
{
key: 'user-management',
icon: userIcon,
title: t('quickActions.userManagement'),
onClick: () => onNavigate?.('/user-management')
},
{
key: 'data-export',
icon: dataExportIcon,
title: t('quickActions.dataExport'),
onClick: () => onNavigate?.('/')
},
{
key: 'log-query',
icon: logIcon,
title: t('quickActions.logQuery'),
onClick: () => onNavigate?.('/log')
},
{
key: 'notification-reminder',
icon: noteIcon,
title: t('quickActions.notificationReminder'),
onClick: () => onNavigate?.('/notification-reminder')
},
// {
// key: 'data-export',
// icon: dataExportIcon,
// title: t('quickActions.dataExport'),
// onClick: () => onNavigate?.('/')
// },
// {
// key: 'log-query',
// icon: logIcon,
// title: t('quickActions.logQuery'),
// onClick: () => onNavigate?.('/log')
// },
// {
// key: 'notification-reminder',
// icon: noteIcon,
// title: t('quickActions.notificationReminder'),
// onClick: () => onNavigate?.('/notification-reminder')
// },
{
key: 'help-center',
icon: helpCenterIcon,
title: t('quickActions.helpCenter'),
onClick: () => onNavigate?.('/help-center')
onClick: openHelpCenter
}
];

View File

@@ -59,6 +59,7 @@ const list = [
]
const TopCardList: FC<{data?: DataResponse}> = ({ data }) => {
const { t } = useTranslation()
debugger
return (
<div className="rb:grid rb:grid-cols-4 rb:gap-[16px]">
{list.map((item) => {

View File

@@ -1,19 +1,48 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Button } from 'antd';
import arrowRight from '@/assets/images/index/arrow_right.svg'
import { getVersion, type versionResponse } from '@/api/common'
const GuideCard: React.FC = () => {
const { t } = useTranslation();
const [versionInfo, setVersionInfo] = useState<versionResponse | null>(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
const fetchVersion = async () => {
try {
setLoading(true);
const response = await getVersion();
setVersionInfo(response);
} catch (error) {
console.error('Failed to fetch version:', error);
} finally {
setLoading(false);
}
};
fetchVersion();
}, []);
return (
<div className='rb:w-full rb:h-[186px] rb:p-4 rb:border-1 rb:border-[#DFE4ED] rb:bg-[#FBFDFF] rb:rounded-xl'>
<div className='rb:flex rb:justify-start rb:text-[#5B6167] rb:text-base rb:font-semibold'>
<div className='rb:w-full rb:p-4 rb:border-1 rb:border-[#DFE4ED] rb:bg-[#FBFDFF] rb:rounded-xl'>
<div className='rb:flex rb:items-center rb:justify-start rb:text-[#5B6167] rb:text-base rb:font-semibold'>
{ t('index.latestUpdate')}
{versionInfo && (
<span className='rb:ml-2 rb:text-sm rb:text-[#1890FF]'>
{versionInfo.version}
</span>
)}
</div>
<div className='rb:flex rb:text-xs rb:text-[#5B6167] rb:leading-[18px] rb:mt-3 rb:pl-2'>
{ t('index.latestUpdateDesc')}
{loading ? (
t('index.loading')
) : (
versionInfo?.introduction || t('index.latestUpdateDesc')
)}
</div>
<div className='rb:flex rb:w-full rb:items-center rb:justify-between rb:gap-3 rb:mt-4'>
{/* <div className='rb:flex rb:w-full rb:items-center rb:justify-between rb:gap-3 rb:mt-4'>
<Button className='rb:gap-2 rb:flex rb:items-center rb:text-[#212332] '>
<span className='rb:text-xs'>{ t('index.viewDetails')}</span>
<img src={arrowRight} className='rb:size-4' />
@@ -22,7 +51,7 @@ const GuideCard: React.FC = () => {
<span className='rb:text-xs'>{ t('index.changeLog')}</span>
<img src={arrowRight} className='rb:size-4' />
</Button>
</div>
</div> */}
</div>
);
};

View File

@@ -1,7 +1,7 @@
import { useEffect, useState, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { Row, Col, Space, Button } from 'antd';
import { Space, Button } from 'antd';
import TopCardList from './components/TopCardList';
import GuideCard from './components/GuideCard';
import VersionCard from './components/VersionCard';
@@ -100,8 +100,8 @@ const Index = () => {
return (
<div className="rb:pb-[24px]">
<Row className="rb:mt-[16px]" gutter={16}>
<Col span={19}>
<div className="rb:mt-[16px] rb:flex rb:gap-4">
<div className='rb:flex-1'>
<div className='rb:flex-col rb:w-full rb:h-[120px] rb:mb-4 rb:p-6 rb:leading-[30px]' style={{backgroundImage: `url(${bgImg})`, backgroundSize: '100% 100%'}}>
<div className='rb:flex rb:text-[22px] rb:text-[#0041C3] rb:font-semibold'>
{ t('index.spaceTitle' )}
@@ -121,8 +121,8 @@ const Index = () => {
/>
</div>
</Col>
<Col span={5}>
</div>
<div className='rb:flex-0 rb:min-w-80'>
{/* 引导 */}
<GuideCard />
<div className='rb:w-full rb:mt-4 '>
@@ -130,10 +130,10 @@ const Index = () => {
</div>
{/* 快捷操作 */}
<div className='rb:w-full rb:mt-4'>
<QuickActions />
<QuickActions onNavigate={navigate} />
</div>
</Col>
</Row>
</div>
</div>
</div>