/* * @Author: ZhaoYing * @Date: 2026-04-14 11:34:42 * @Last Modified by: ZhaoYing * @Last Modified time: 2026-04-16 17:23:49 */ /** * 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; 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} {unit ? t(`package.${unit}`) : ''}
) } 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 formatTabItems = useMemo(() => { return ['saas_personal', 'commercial_deployment'].map(value => ({ value, label: `${t(`package.${value}`)}`, })) }, [t, activeTab]) const handleChangeTab = (value: SegmentedProps['value']) => { setActiveTab(value as string); } const getList = () => { getPackageList({ category: activeTab as Package['category'] }) .then(res => { setData(res as Package[] || []) }) } useEffect(() => { getList() }, [activeTab]) const getKeyWithLanguage = (key: string) => { return (language === 'en' ? `${key}_en` : key) as keyof Package } const [currentPage, setCurrentPage] = useState(0) const totalPages = visibleCount > 0 ? Math.ceil(data.length / visibleCount) : 1 const showArrows = totalPages > 1 const pageData = data.slice(currentPage * visibleCount, (currentPage + 1) * visibleCount) useEffect(() => { setCurrentPage(0) }, [activeTab, visibleCount]) const handleChoosePlan = () => { window.open(`https://docs.redbearai.com/s/${language || 'en'}-memorybear`, '_blank') }; return ( <>
{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']]; if (value === undefined || value === null) return null; return ( ) })} {pkg.tech_support && ( )} {pkg.sla_compliance && ( )}
))}
{showArrows && ( = totalPages - 1 })} onClick={() => { if (currentPage >= totalPages - 1) return setCurrentPage(p => p + 1) }} > = totalPages - 1 ? '#E1E2E7' : '#171719', fontSize: 24 }} /> )}
); }; export default Package;