/* * @Author: ZhaoYing * @Date: 2026-04-14 11:34:42 * @Last Modified by: ZhaoYing * @Last Modified time: 2026-04-21 15:45:30 */ /** * Package Component * * Package management page with: * - Tabs for SaaS Personal and Commercial Deployment * - Package cards showing features and pricing * - Edit and delete actions * * @component */ import { useRef, useMemo, useState, useEffect, type FC, type ComponentType, type SVGProps } from 'react'; import { useTranslation } from 'react-i18next'; import { Flex, Tooltip, Divider, Button, type SegmentedProps } from 'antd'; import clsx from 'clsx'; import Icon from '@ant-design/icons' import type { Package } from './types' import { getPackageList } from '@/api/package'; import PageTabs from '@/components/PageTabs' import { billingUnits } from './constant' import RbCard from '@/components/RbCard/Card' import BodyWrapper from '@/components/Empty/BodyWrapper' import { useI18n } from '@/store/locale' import SpaceSvg from '@/assets/images/package/space.svg?react' import SkillSvg from '@/assets/images/package/skill.svg?react' import AppSvg from '@/assets/images/package/app.svg?react' import KnowledgeSvg from '@/assets/images/package/knowledge.svg?react' import MemoryConfigSvg from '@/assets/images/package/memory_config.svg?react' import EndUserSvg from '@/assets/images/package/end_user.svg?react' import OntologySvg from '@/assets/images/package/ontology.svg?react' import ModelSvg from '@/assets/images/package/model.svg?react' import TechnicalSupportSvg from '@/assets/images/package/technical_support.svg?react' import ApiOpsSvg from '@/assets/images/package/api_ops.svg?react' import arrowSvg from '@/assets/images/package/arrow.svg?react' import slaSvg from '@/assets/images/package/sla.svg?react'; const iconMap: Record>> = { space: SpaceSvg, skill: SkillSvg, app: AppSvg, knowledge: KnowledgeSvg, memory_config: MemoryConfigSvg, end_user: EndUserSvg, ontology: OntologySvg, model: ModelSvg, technical_support: TechnicalSupportSvg, api_ops: ApiOpsSvg, sla: slaSvg, } const btnClassNames = { permanent_free: 'rb:h-10! rb:rounded-[8px]!', default: 'rb:h-10! rb:rounded-[8px]! rb:bg-[#212332]! rb:text-white! rb:border-0! rb:hover:border-0! rb:hover:opacity-[0.8]', } export const UnitWrapper = ({ titleKey, value, icon, unit, theme_color = '#171719' }: { titleKey: string; value?: number | string | null; icon: string; unit?: string; theme_color?: string; }) => { const { t } = useTranslation(); const renderFeatureIcon = (iconKey: string, color: string) => { const SvgComponent = iconMap[iconKey] if (!SvgComponent) return null return } return ( {renderFeatureIcon(icon, theme_color)}
{t(`package.${titleKey}`)}
{value ?
{value} {unit ? t(`package.${unit}`) : ''}
:
{t('package.noLimit')}
}
) } const Package: FC = () => { const { t } = useTranslation(); const { language } = useI18n() const [data, setData] = useState([]) const scrollRef = useRef(null) const CARD_WIDTH = 360 const GAP = 12 const [visibleCount, setVisibleCount] = useState(3) useEffect(() => { const calcVisible = () => { if (!scrollRef.current) return const w = scrollRef.current.offsetWidth setVisibleCount(Math.floor((w + GAP) / (CARD_WIDTH + GAP))) } calcVisible() window.addEventListener('resize', calcVisible) return () => window.removeEventListener('resize', calcVisible) }, []) const [activeTab, setActiveTab] = useState('saas_personal'); const categories = useMemo(() => { const cats = [...new Set(data.map(p => p.category))] return cats }, [data]) const formatTabItems = useMemo(() => { return (['saas_personal', 'commercial_deployment'] as const) .filter(v => categories.includes(v)) .map(value => ({ value, label: t(`package.${value}`) })) }, [t, categories]) const showTabs = categories.length > 1 const handleChangeTab = (value: SegmentedProps['value']) => { setActiveTab(value as string); } const getList = () => { getPackageList({ status: true }).then(res => { setData(res as Package[] || []) }) } useEffect(() => { getList() }, []) useEffect(() => { if (categories.length > 0 && !categories.includes(activeTab as Package['category'])) { setActiveTab(categories[0]) } }, [categories]) const getKeyWithLanguage = (key: string) => { return (language === 'en' ? `${key}_en` : key) as keyof Package } const filteredData = useMemo(() => data.filter(p => p.category === activeTab), [data, activeTab]) const [currentPage, setCurrentPage] = useState(0) const totalPages = visibleCount > 0 ? Math.ceil(filteredData.length / visibleCount) : 1 const showArrows = totalPages > 1 const pageData = filteredData.slice(currentPage * visibleCount, (currentPage + 1) * visibleCount) useEffect(() => { setCurrentPage(0) }, [activeTab, visibleCount, filteredData]) const handleChoosePlan = () => { window.open(`https://docs.redbearai.com/s/${language || 'en'}-memorybear`, '_blank') }; return ( <> {showTabs && ( )}
{showArrows && ( 0, 'rb:cursor-not-allowed': currentPage === 0 })} onClick={() => { if (currentPage === 0) return setCurrentPage(p => p - 1) }} > )} {pageData.map((pkg) => (
{/* Header */}

{String(pkg[getKeyWithLanguage('name')] ?? '')}

{/* Subtitle */}

{String(pkg[getKeyWithLanguage('core_value')] ?? '')}

{/* Price */}
{pkg.billing_cycle !== 'permanent_free' && <> ¥ {pkg.price} } {pkg.billing_cycle && ( {pkg.billing_cycle !== 'permanent_free' && ' /'} {t(`package.${pkg.billing_cycle}`)} )}
{/* Features */} {billingUnits.map(({ key, unit, icon }) => { const value = pkg?.quotas?.[key as keyof Package['quotas']]; return ( ) })} {pkg.tech_support && pkg[getKeyWithLanguage('tech_support')] && ( )} {pkg.sla_compliance && pkg[getKeyWithLanguage('sla_compliance')] && ( )}
))}
{showArrows && ( = totalPages - 1 })} onClick={() => { if (currentPage >= totalPages - 1) return setCurrentPage(p => p + 1) }} > = totalPages - 1 ? '#E1E2E7' : '#171719', fontSize: 24 }} /> )}
); }; export default Package;