diff --git a/web/package.json b/web/package.json index b41ab9b5..1f1fc397 100644 --- a/web/package.json +++ b/web/package.json @@ -93,7 +93,8 @@ "typescript-eslint": "^8.45.0", "unplugin-auto-import": "^20.2.0", "unplugin-vue-components": "^29.1.0", - "vite": "npm:rolldown-vite@7.1.14" + "vite": "npm:rolldown-vite@7.1.14", + "vite-plugin-svgr": "^5.2.0" }, "overrides": { "vite": "npm:rolldown-vite@7.1.14" diff --git a/web/src/api/package.ts b/web/src/api/package.ts index da52d355..f9cd2f74 100644 --- a/web/src/api/package.ts +++ b/web/src/api/package.ts @@ -1,14 +1,8 @@ import { request } from '@/utils/request' import type { Package } from '@/views/Package/types' - -export const SYS_API_PREFIX = '/sys'; // 套餐列表 -export const getPackageListUrl = `${SYS_API_PREFIX}/package-plans` -export const getPackageList = (query: { category: Package['category']; status: boolean; }) => { +export const getPackageListUrl = `/package-plans` +export const getPackageList = (query?: { category?: Package['category']; status?: boolean; }) => { return request.get(getPackageListUrl, query) -} -// 获取套餐详情 -export const getPackageDetail = (package_plan_id: string) => { - return request.get(`${SYS_API_PREFIX}/package-plans/${package_plan_id}`) } \ No newline at end of file diff --git a/web/src/api/user.ts b/web/src/api/user.ts index 72a3ad73..0752f019 100644 --- a/web/src/api/user.ts +++ b/web/src/api/user.ts @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-03 14:00:23 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-25 11:17:44 + * @Last Modified time: 2026-04-14 18:36:01 */ import { request } from '@/utils/request' import type { CreateModalData, ChangeEmailModalForm } from '@/views/UserManagement/types' @@ -56,4 +56,9 @@ export const sendEmailCode = (data: { email: string }) => { // Verify code and change email export const changeEmail = (data: ChangeEmailModalForm) => { return request.put('/users/change-email', data) +} + +// 获取租户套餐信息 +export const getTenantSubscription = () => { + return request.get('/tenant/subscription') } \ No newline at end of file diff --git a/web/src/assets/images/index/arrow_right_dark.svg b/web/src/assets/images/index/arrow_right_dark.svg new file mode 100644 index 00000000..b2742d11 --- /dev/null +++ b/web/src/assets/images/index/arrow_right_dark.svg @@ -0,0 +1,16 @@ + + + 编组 5 + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/assets/images/menuNew/package_bg.png b/web/src/assets/images/menuNew/package_bg.png new file mode 100644 index 00000000..cbed6f7a Binary files /dev/null and b/web/src/assets/images/menuNew/package_bg.png differ diff --git a/web/src/assets/images/package/api_ops.svg b/web/src/assets/images/package/api_ops.svg new file mode 100644 index 00000000..47512f69 --- /dev/null +++ b/web/src/assets/images/package/api_ops.svg @@ -0,0 +1,17 @@ + + + 频次 + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/assets/images/package/app.svg b/web/src/assets/images/package/app.svg new file mode 100644 index 00000000..699e5d87 --- /dev/null +++ b/web/src/assets/images/package/app.svg @@ -0,0 +1,17 @@ + + + 应用 + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/assets/images/package/arrow.svg b/web/src/assets/images/package/arrow.svg new file mode 100644 index 00000000..675d3dee --- /dev/null +++ b/web/src/assets/images/package/arrow.svg @@ -0,0 +1,13 @@ + + + 编组 49 + + + + + + + + + + \ No newline at end of file diff --git a/web/src/assets/images/package/disable.svg b/web/src/assets/images/package/disable.svg new file mode 100644 index 00000000..7e23d26f --- /dev/null +++ b/web/src/assets/images/package/disable.svg @@ -0,0 +1,18 @@ + + + 编组 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/assets/images/package/enable.svg b/web/src/assets/images/package/enable.svg new file mode 100644 index 00000000..3df8f472 --- /dev/null +++ b/web/src/assets/images/package/enable.svg @@ -0,0 +1,18 @@ + + + 编组 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/assets/images/package/end_user.svg b/web/src/assets/images/package/end_user.svg new file mode 100644 index 00000000..e6109b18 --- /dev/null +++ b/web/src/assets/images/package/end_user.svg @@ -0,0 +1,19 @@ + + + 终端 + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/assets/images/package/knowledge.svg b/web/src/assets/images/package/knowledge.svg new file mode 100644 index 00000000..3858efe1 --- /dev/null +++ b/web/src/assets/images/package/knowledge.svg @@ -0,0 +1,17 @@ + + + 知识库容量 + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/assets/images/package/memory_config.svg b/web/src/assets/images/package/memory_config.svg new file mode 100644 index 00000000..a1b38c5e --- /dev/null +++ b/web/src/assets/images/package/memory_config.svg @@ -0,0 +1,20 @@ + + + 记忆引擎 + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/assets/images/package/model.svg b/web/src/assets/images/package/model.svg new file mode 100644 index 00000000..23483fc0 --- /dev/null +++ b/web/src/assets/images/package/model.svg @@ -0,0 +1,17 @@ + + + 模型 + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/assets/images/package/ontology.svg b/web/src/assets/images/package/ontology.svg new file mode 100644 index 00000000..ff94829b --- /dev/null +++ b/web/src/assets/images/package/ontology.svg @@ -0,0 +1,17 @@ + + + 本体工程 + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/assets/images/package/skill.svg b/web/src/assets/images/package/skill.svg new file mode 100644 index 00000000..195248d9 --- /dev/null +++ b/web/src/assets/images/package/skill.svg @@ -0,0 +1,17 @@ + + + 技能 + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/assets/images/package/sla.svg b/web/src/assets/images/package/sla.svg new file mode 100644 index 00000000..10e4ce10 --- /dev/null +++ b/web/src/assets/images/package/sla.svg @@ -0,0 +1,19 @@ + + + SLA + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/assets/images/package/space.svg b/web/src/assets/images/package/space.svg new file mode 100644 index 00000000..6775932d --- /dev/null +++ b/web/src/assets/images/package/space.svg @@ -0,0 +1,17 @@ + + + 空间 + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/assets/images/package/technical_support.svg b/web/src/assets/images/package/technical_support.svg new file mode 100644 index 00000000..d9b4251e --- /dev/null +++ b/web/src/assets/images/package/technical_support.svg @@ -0,0 +1,17 @@ + + + 合规 + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/components/SiderMenu/SubscriptionDetailModal.tsx b/web/src/components/SiderMenu/SubscriptionDetailModal.tsx new file mode 100644 index 00000000..ae084fcd --- /dev/null +++ b/web/src/components/SiderMenu/SubscriptionDetailModal.tsx @@ -0,0 +1,119 @@ +/* + * @Author: ZhaoYing + * @Date: 2026-04-14 12:28:23 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-04-16 17:34:02 + */ + +import { useState, forwardRef, useImperativeHandle } from 'react'; +import { Flex, Tooltip, Divider } from 'antd'; +import { useTranslation } from 'react-i18next'; +import clsx from 'clsx'; + +import RbModal from '@/components/RbModal'; +import type { Subscription } from './index' +import { billingUnits } from '@/views/Package/constant' +import { useI18n } from '@/store/locale' +import { UnitWrapper } from '@/views/Package' + +export interface SubscriptionDetailModalRef { + handleOpen: (subscription: Subscription | null) => void; +} + +const SubscriptionDetailModal = forwardRef((_props, ref) => { + const { t } = useTranslation(); + const [open, setOpen] = useState(false); + const { language } = useI18n() + const [detail, setDetail] = useState(null); + + const handleOpen = (subscription: Subscription | null) => { + setOpen(true) + setDetail(subscription); + }; + + const handleCancel = () => { + setOpen(false); + }; + + useImperativeHandle(ref, () => ({ + handleOpen, + })); + + const getKeyWithLanguage = (key: string) => { + return (language === 'en' ? `${key}_en` : key) as keyof Subscription['package_plan'] + } + + return ( + item).join(' - ')} + open={open} + onCancel={handleCancel} + footer={null} + > + {/* Header */} +

+ {String(detail?.package_plan?.[getKeyWithLanguage('name')] ?? '')} +

+ + {/* Subtitle */} +

+ {String(detail?.package_plan?.[getKeyWithLanguage('core_value')] ?? '')} +

+ + {/* Price */} +
+ {detail?.package_plan?.billing_cycle !== 'permanent_free' && <> + ¥ + {detail?.package_plan?.price} + } + {detail?.package_plan?.billing_cycle && ( + + {detail?.package_plan?.billing_cycle !== 'permanent_free' && ' /'} + {t(`package.${detail?.package_plan?.billing_cycle}`)} + + )} +
+ + + + {/* Features */} + + {billingUnits.map(({ key, unit, icon }) => { + const value = detail?.quota[key as keyof Subscription['quota']]; + if (value === undefined || value === null) return null; + return ( + + ) + })} + {detail?.package_plan?.tech_support && ( + + )} + {detail?.package_plan?.sla_compliance && ( + + )} + +
+ ); +}); + +export default SubscriptionDetailModal; diff --git a/web/src/components/SiderMenu/index.tsx b/web/src/components/SiderMenu/index.tsx index c85f3c9f..a08146c1 100644 --- a/web/src/components/SiderMenu/index.tsx +++ b/web/src/components/SiderMenu/index.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-02 15:25:31 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-03-27 19:11:43 + * @Last Modified time: 2026-04-16 17:35:38 */ /** * SiderMenu Component @@ -18,7 +18,7 @@ * @component */ -import { useState, useEffect, type FC } from 'react'; +import { useState, useEffect, useRef, type FC } from 'react'; import { Menu as AntMenu, Layout, Flex } from 'antd'; import { UserOutlined } from '@ant-design/icons'; import type { MenuProps } from 'antd'; @@ -30,6 +30,9 @@ import { useMenu, type MenuItem } from '@/store/menu'; import styles from './index.module.css' import logo from '@/assets/images/logo.png' import { useUser } from '@/store/user'; +import { getTenantSubscription } from '@/api/user'; +import { useI18n } from '@/store/locale' +import SubscriptionDetailModal, { type SubscriptionDetailModalRef } from './SubscriptionDetailModal' // Import SVG files // space @@ -70,7 +73,51 @@ import pricingActiveIcon from '@/assets/images/menuNew/pricing_active.svg' import skillsIcon from '@/assets/images/menuNew/skills.svg' import skillsActiveIcon from '@/assets/images/menuNew/skills_active.svg' +export interface PackagePlan { + id: string + name: string + name_en?: string + version: string + category: string + tier_level: number + price: number + billing_cycle: string + core_value?: string + core_value_en?: string + tech_support?: string + tech_support_en?: string + sla_compliance?: string + sla_compliance_en?: string + page_customization?: string + page_customization_en?: string + theme_color?: string +} +export interface SubscriptionQuota { + app_quota: number + model_quota: number + skill_quota: number + end_user_quota: number + workspace_quota: number + api_ops_rate_limit: number + memory_engine_quota: number + ontology_project_quota: number + knowledge_capacity_quota: number +} + +export interface Subscription { + subscription_id: string | null + tenant_id: string + package_plan_id: string + package_version: string + package_plan: PackagePlan + started_at: number | null + expired_at: number | null + status: string + quota: SubscriptionQuota + created_at: number + updated_at: number +} /** Icon path mapping table for menu items (normal and active states) */ const iconPathMap: Record = { 'dashboard': dashboardIcon, @@ -121,10 +168,12 @@ const Menu: FC<{ const navigate = useNavigate(); const location = useLocation(); const { t } = useTranslation(); + const { language } = useI18n() const [selectedKeys, setSelectedKeys] = useState([]); const { allMenus, collapsed, loadMenus, toggleSider } = useMenu() const [menus, setMenus] = useState([]) const { user, storageType } = useUser() + const subscriptionDetailRef = useRef(null) /** Filter menus based on user role and source */ useEffect(() => { @@ -279,6 +328,25 @@ const Menu: FC<{ localStorage.removeItem('user') } + const [subscription, setSubscription] = useState(null) + useEffect(() => { + if (source === 'manage') { + getTenantSubscription() + .then(res => { + setSubscription(res as Subscription) + }) + } else { + setSubscription(null) + } + }, [source]) + + const getKeyWithLanguage = (key: string) => { + return (language === 'en' ? `${key}_en` : key) as keyof Subscription['package_plan'] + } + const handleViewDetail = () => { + subscriptionDetailRef.current?.handleOpen(subscription) + } + return ( {/* Return to space button for superusers */} @@ -341,6 +410,30 @@ const Menu: FC<{ {collapsed ? null : t('common.returnToSpace')} } + {source === 'manage' && subscription && !collapsed && +
+
{subscription.package_plan?.[getKeyWithLanguage('name')]}
+ +
+ {['workspace_quota', 'skill_quota', 'app_quota', 'model_quota'].map(key => ( +
+
{subscription.quota?.[key as keyof typeof subscription.quota]}
+
{t(`index.${key}`)}
+
+ ))} +
+ + {t('package.viewDetail')} +
+
+
+ } + +
); }; diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index 6bcc5034..a18468b4 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -15,6 +15,10 @@ export const en = { startedDesc: 'Understand the core functions of the platform and quickly get started through graphic guidance and video tutorials. Includes a full process demonstration from creating a space to publishing an application.', spaceTitle:'Memory Bear Intelligent Space Management Platform', spaceSubTitle: 'Making it easier to implement intelligent models - a one-stop platform for model management, knowledge building, workflow orchestration, and spatial operations', + workspace_quota: 'Spaces', + skill_quota: 'Skills', + app_quota: 'Apps', + model_quota: 'Models', }, version:{ releaseDate: 'Release Date', @@ -2891,8 +2895,8 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re context_details: 'Preference Details', supporting_evidence: 'Preference Source', specific_examples: 'Source', - preferencesTip: 'Reminder: Click on the preferences above to view the corresponding Lenovo network', - wordEmpty: 'There is currently no Lenovo network available', + preferencesTip: 'Reminder: Click on the preferences above to view the corresponding association network', + wordEmpty: 'There is currently no association network available', noData: 'Portrait data does not exist, please click the refresh button to initialize', }, shortTermDetail: { @@ -3079,6 +3083,8 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re ontology_project_quota: 'Ontology Project', model_quota: 'Model Quota', editPackage: 'Edit Package', + + viewDetail: 'View full package details', }, }, }; diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index fff8c1af..c39ef14e 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -15,6 +15,10 @@ export const zh = { startedDesc: '了解该平台的核心功能,并通过图形指引和视频教程快速上手。包含从创建空间到发布应用程序的整个操作流程演示。', spaceTitle:'记忆熊智能空间管理平台', spaceSubTitle: '使智能模型的实施变得更加容易——一个集模型管理、知识构建、工作流程编排以及空间操作于一体的综合性平台', + workspace_quota: '空间', + skill_quota: '技能', + app_quota: '应用', + model_quota: '模型', }, version:{ releaseDate: '发布日', @@ -3043,6 +3047,8 @@ export const zh = { ontology_project_quota: '本体工程', model_quota: '可负载模型数量', editPackage: '编辑套餐', + + viewDetail: '查看完整套餐详情', }, }, } \ No newline at end of file diff --git a/web/src/svg.d.ts b/web/src/svg.d.ts new file mode 100644 index 00000000..2c19c5bb --- /dev/null +++ b/web/src/svg.d.ts @@ -0,0 +1,5 @@ +declare module '*.svg?react' { + import type { FC, SVGProps } from 'react' + const ReactComponent: FC> + export default ReactComponent +} diff --git a/web/src/utils/request.ts b/web/src/utils/request.ts index 318738dd..cca7953e 100644 --- a/web/src/utils/request.ts +++ b/web/src/utils/request.ts @@ -23,7 +23,6 @@ import { clearAuthData } from './auth'; import { message } from 'antd'; import { refreshTokenUrl, refreshToken, loginUrl, logoutUrl } from '@/api/user' import i18n from '@/i18n' -import { SYS_API_PREFIX } from '@/api/package' /** * Standard API response structure @@ -75,10 +74,6 @@ let requests: RequestQueueItem[] = []; // Request interceptor service.interceptors.request.use( (config) => { - console.log('config', config, config.url?.startsWith(SYS_API_PREFIX)) - if (config.url?.startsWith(SYS_API_PREFIX)) { - config.baseURL = ''; - } if (!config.headers.Authorization) { const token = cookieUtils.get('authToken'); if (token) { diff --git a/web/src/views/Index/index.tsx b/web/src/views/Index/index.tsx index b10dc000..dece9770 100644 --- a/web/src/views/Index/index.tsx +++ b/web/src/views/Index/index.tsx @@ -1,7 +1,7 @@ import { useEffect, useState, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; -import { Space, Button, Row, Col, Flex } from 'antd'; +import { Space, Button, Flex } from 'antd'; import TopCardList from './components/TopCardList'; import GuideCard from './components/GuideCard'; diff --git a/web/src/views/Package/constant.ts b/web/src/views/Package/constant.ts index 8d3b0d48..7fc69969 100644 --- a/web/src/views/Package/constant.ts +++ b/web/src/views/Package/constant.ts @@ -2,39 +2,52 @@ * @Author: ZhaoYing * @Date: 2026-04-14 11:43:57 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-04-14 14:55:20 + * @Last Modified time: 2026-04-14 11:44:40 */ export const billingUnits = [ { key: 'workspace_quota', unit: 'pcs', placeholder: 'numberPlaceholder', + icon: 'space', }, { key: 'skill_quota', unit: 'pcs', placeholder: 'numberPlaceholder', + icon: 'skill', }, { key: 'app_quota', unit: 'pcs', placeholder: 'numberPlaceholder', + icon: 'app', }, { key: 'knowledge_capacity_quota', unit: 'GB', placeholder: 'numberPlaceholder', + icon: 'knowledge', }, { key: 'memory_engine_quota', unit: 'pcs', placeholder: 'numberPlaceholder', + icon: 'memory_config', }, { key: 'end_user_quota', unit: 'pcs', placeholder: 'numberPlaceholder', + icon: 'end_user', }, { key: 'ontology_project_quota', unit: 'pcs', placeholder: 'numberPlaceholder', + icon: 'ontology', }, { key: 'model_quota', unit: 'ops', placeholder: 'numberPlaceholder', + icon: 'model', }, -] \ No newline at end of file + { + key: 'api_ops_rate_limit', + unit: 'ops', placeholder: 'numberPlaceholder', + icon: 'api_ops', + }, +] diff --git a/web/src/views/Package/index.tsx b/web/src/views/Package/index.tsx index 64ce0c04..b8e30593 100644 --- a/web/src/views/Package/index.tsx +++ b/web/src/views/Package/index.tsx @@ -1,8 +1,8 @@ /* * @Author: ZhaoYing - * @Date: 2026-02-25 + * @Date: 2026-04-14 11:34:42 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-04-14 14:59:11 + * @Last Modified time: 2026-04-16 17:23:49 */ /** * Package Component @@ -15,11 +15,11 @@ * @component */ -import { useMemo, useState, useEffect, type FC } from 'react'; +import { useRef, useMemo, useState, useEffect, type FC, type ComponentType, type SVGProps } from 'react'; import { useTranslation } from 'react-i18next'; -import { useNavigate } from 'react-router-dom'; -import { Flex, Row, Col, type SegmentedProps } from 'antd'; +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'; @@ -28,27 +28,95 @@ import { billingUnits } from './constant' import RbCard from '@/components/RbCard/Card' import BodyWrapper from '@/components/Empty/BodyWrapper' import { useI18n } from '@/store/locale' -import RbButton from '@/components/RbButton' + +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 navigate = useNavigate(); 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}`), + label: `${t(`package.${value}`)}`, })) - }, [t]) - /** Handle tab change */ + }, [t, activeTab]) + const handleChangeTab = (value: SegmentedProps['value']) => { setActiveTab(value as string); } const getList = () => { - getPackageList({ category: activeTab as Package['category'], status: true }) + getPackageList({ category: activeTab as Package['category'] }) .then(res => { setData(res as Package[] || []) }) @@ -61,10 +129,20 @@ const Package: FC = () => { const getKeyWithLanguage = (key: string) => { return (language === 'en' ? `${key}_en` : key) as keyof Package } - /** Navigate to order history */ - const goToHistory = () => { - navigate('/orders'); - } + + 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 ( <> @@ -73,70 +151,136 @@ const Package: FC = () => { options={formatTabItems} onChange={handleChangeTab} /> - -
- {t('pricing.orderHistory')} -
- - {data.map((pkg) => ( - - - -
- {/* Header */} -
-

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

-

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

-
- {pkg.billing_cycle !== 'permanent_free' && <>¥{pkg.price}} - {pkg.billing_cycle && {pkg.billing_cycle !== 'permanent_free' && '/'}{t(`package.${pkg.billing_cycle}`)}} -
+
+ {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 }) => { - if (typeof pkg.quotas[key as keyof Package['quotas']] === 'number') { - return ( -
- {t(`package.${key}`)} - {pkg.quotas[key as keyof Package['quotas']]}{t(`package.${unit}`)} -
- ) - } + + {billingUnits.map(({ key, unit, icon }) => { + const value = pkg?.quotas?.[key as keyof Package['quotas']]; + if (value === undefined || value === null) return null; + return ( + + ) })} - {pkg.api_ops_rate_limit && -
- {t(`package.api_ops_rate_limit`)} - {pkg.api_ops_rate_limit}{t('package.ops')} -
- } - {pkg.tech_support && -
- {t(`package.tech_support`)} - {String(pkg[getKeyWithLanguage('tech_support')] ?? '')} -
- } -
+ {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 }} /> + + )} +
); diff --git a/web/src/views/Package/types.ts b/web/src/views/Package/types.ts index 6517f63a..6ae9e48c 100644 --- a/web/src/views/Package/types.ts +++ b/web/src/views/Package/types.ts @@ -2,60 +2,60 @@ * @Author: ZhaoYing * @Date: 2026-04-14 11:35:01 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-04-14 14:28:46 + * @Last Modified time: 2026-04-16 16:44:19 */ export interface Package { - id: string; - // 名称 - name: string; - name_en: string; - // 类型 - category: "saas_personal" | "commercial_deployment"; - tier_level: number; - // 版本 - version: string; - // 状态 - status: boolean; - // 价格 - price: string; - // 计费周期 - billing_cycle: "monthly" | "yearly" | "permanent_free" | "local_deployment"; - // 核心价值 - core_value: string; - core_value_en: string; - // 技术支持 - tech_support: string; - tech_support_en: string; - // SLA与合规 - sla_compliance: string; - sla_compliance_en: string; - // 对话页面个性化配置 - page_customization: string; - page_customization_en: string; - // API OPS 频次(次/秒) - api_ops_rate_limit: number; - // 主题色 - theme_color: string; - quotas: { - // 空间数量 - workspace_quota: number; - // 技能库数量 - skill_quota: number; - // 应用数量 - app_quota: number; - // 知识库容量 - knowledge_capacity_quota: string; - // 记忆引擎数量 - memory_engine_quota: number; - // 可记忆终端用户数 - end_user_quota: number; - // 本体工程 - ontology_project_quota: number; - // 可负载模型数量 - model_quota: number; - }, - created_at: number; - updated_at: number; - created_by: string; - updated_by: string | null; + id: string; + // 名称 + name: string | null; + name_en: string | null; + // 类型 + category: "saas_personal" | "commercial_deployment"; + tier_level: number; + // 版本 + version: string; + // 状态 + status: boolean; + // 价格 + price: string | null; + // 计费周期 + billing_cycle: "monthly" | "yearly" | "permanent_free" | "local_deployment"; + // 核心价值 + core_value: string | null; + core_value_en: string | null; + // 技术支持 + tech_support: string | null; + tech_support_en: string | null; + // SLA与合规 + sla_compliance: string | null; + sla_compliance_en: string | null; + // 对话页面个性化配置 + page_customization: string | null; + page_customization_en: string | null; + // 主题色 + theme_color: string; + quotas: { + // API OPS 频次(次/秒) + api_ops_rate_limit: number | null; + // 空间数量 + workspace_quota: number | null; + // 技能库数量 + skill_quota: number | null; + // 应用数量 + app_quota: number | null; + // 知识库容量 + knowledge_capacity_quota: number | null; + // 记忆引擎数量 + memory_engine_quota: number | null; + // 可记忆终端用户数 + end_user_quota: number | null; + // 本体工程 + ontology_project_quota: number | null; + // 可负载模型数量 + model_quota: number | null; + }, + created_at: number; + updated_at: number; + created_by: string | null; + updated_by: string | null; } diff --git a/web/vite.config.ts b/web/vite.config.ts index 8cc1fa3b..4a1a0b34 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -3,6 +3,7 @@ import react from '@vitejs/plugin-react' import { resolve } from 'path' import AutoImport from 'unplugin-auto-import/vite' import tailwindcss from '@tailwindcss/vite' +import svgr from 'vite-plugin-svgr'; // https://vite.dev/config/ export default defineConfig({ @@ -32,6 +33,7 @@ export default defineConfig({ imports: ['react', 'react-router-dom'], dts: 'public/auto-imports.d.ts', }), + svgr({ svgrOptions: { icon: true } }), ], css: { modules: {