From e15e32cc7b3471fed2fb7a865ce11ff5a4503f53 Mon Sep 17 00:00:00 2001 From: zhaoying Date: Fri, 17 Apr 2026 12:20:15 +0800 Subject: [PATCH 1/2] feat(web): package menu --- web/package.json | 3 +- web/src/api/package.ts | 10 +- web/src/api/user.ts | 7 +- .../assets/images/index/arrow_right_dark.svg | 16 + web/src/assets/images/menuNew/package_bg.png | Bin 0 -> 17500 bytes web/src/assets/images/package/api_ops.svg | 17 ++ web/src/assets/images/package/app.svg | 17 ++ web/src/assets/images/package/arrow.svg | 13 + web/src/assets/images/package/disable.svg | 18 ++ web/src/assets/images/package/enable.svg | 18 ++ web/src/assets/images/package/end_user.svg | 19 ++ web/src/assets/images/package/knowledge.svg | 17 ++ .../assets/images/package/memory_config.svg | 20 ++ web/src/assets/images/package/model.svg | 17 ++ web/src/assets/images/package/ontology.svg | 17 ++ web/src/assets/images/package/skill.svg | 17 ++ web/src/assets/images/package/sla.svg | 19 ++ web/src/assets/images/package/space.svg | 17 ++ .../images/package/technical_support.svg | 17 ++ .../SiderMenu/SubscriptionDetailModal.tsx | 119 ++++++++ web/src/components/SiderMenu/index.tsx | 99 +++++- web/src/i18n/en.ts | 10 +- web/src/i18n/zh.ts | 6 + web/src/svg.d.ts | 5 + web/src/utils/request.ts | 5 - web/src/views/Index/index.tsx | 2 +- web/src/views/Package/constant.ts | 17 +- web/src/views/Package/index.tsx | 286 +++++++++++++----- web/src/views/Package/types.ts | 108 +++---- web/vite.config.ts | 2 + 30 files changed, 790 insertions(+), 148 deletions(-) create mode 100644 web/src/assets/images/index/arrow_right_dark.svg create mode 100644 web/src/assets/images/menuNew/package_bg.png create mode 100644 web/src/assets/images/package/api_ops.svg create mode 100644 web/src/assets/images/package/app.svg create mode 100644 web/src/assets/images/package/arrow.svg create mode 100644 web/src/assets/images/package/disable.svg create mode 100644 web/src/assets/images/package/enable.svg create mode 100644 web/src/assets/images/package/end_user.svg create mode 100644 web/src/assets/images/package/knowledge.svg create mode 100644 web/src/assets/images/package/memory_config.svg create mode 100644 web/src/assets/images/package/model.svg create mode 100644 web/src/assets/images/package/ontology.svg create mode 100644 web/src/assets/images/package/skill.svg create mode 100644 web/src/assets/images/package/sla.svg create mode 100644 web/src/assets/images/package/space.svg create mode 100644 web/src/assets/images/package/technical_support.svg create mode 100644 web/src/components/SiderMenu/SubscriptionDetailModal.tsx create mode 100644 web/src/svg.d.ts 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 0000000000000000000000000000000000000000..cbed6f7aa2bf906eda5105dc34b9dff46da5b222 GIT binary patch literal 17500 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 literal 0 HcmV?d00001 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: { From b659e2a6e151b925e1f545d5d2a9c8df77d18572 Mon Sep 17 00:00:00 2001 From: zhaoying Date: Fri, 17 Apr 2026 13:36:19 +0800 Subject: [PATCH 2/2] feat(web): package tabs --- web/src/views/Package/index.tsx | 67 ++++++++++++++++++++++----------- 1 file changed, 45 insertions(+), 22 deletions(-) diff --git a/web/src/views/Package/index.tsx b/web/src/views/Package/index.tsx index b8e30593..b3aed9d5 100644 --- a/web/src/views/Package/index.tsx +++ b/web/src/views/Package/index.tsx @@ -105,39 +105,54 @@ const Package: FC = () => { }, []) 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'].map(value => ({ - value, - label: `${t(`package.${value}`)}`, - })) - }, [t, activeTab]) + 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({ category: activeTab as Package['category'] }) - .then(res => { - setData(res as Package[] || []) - }) + getPackageList({ status: true }).then(res => { + setData(res as Package[] || []) + }) } useEffect(() => { getList() - }, [activeTab]) + }, []) + + 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(data.length / visibleCount) : 1 + const totalPages = visibleCount > 0 ? Math.ceil(filteredData.length / visibleCount) : 1 const showArrows = totalPages > 1 - const pageData = data.slice(currentPage * visibleCount, (currentPage + 1) * visibleCount) + const pageData = filteredData.slice(currentPage * visibleCount, (currentPage + 1) * visibleCount) useEffect(() => { setCurrentPage(0) - }, [activeTab, visibleCount]) + }, [activeTab, visibleCount, filteredData]) const handleChoosePlan = () => { window.open(`https://docs.redbearai.com/s/${language || 'en'}-memorybear`, '_blank') @@ -145,14 +160,16 @@ const Package: FC = () => { return ( <> - - - - + {showTabs && ( + + + + )} +
{showArrows && ( { {/* Features */} - + {billingUnits.map(({ key, unit, icon }) => { const value = pkg?.quotas?.[key as keyof Package['quotas']]; if (value === undefined || value === null) return null; return (