feat(web): package

This commit is contained in:
zhaoying
2026-04-14 14:51:02 +08:00
parent 72b39c6fa3
commit 7f8765b815
10 changed files with 392 additions and 21 deletions

14
web/src/api/package.ts Normal file
View File

@@ -0,0 +1,14 @@
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; }) => {
return request.get(getPackageListUrl, query)
}
// 获取套餐详情
export const getPackageDetail = (package_plan_id: string) => {
return request.get(`${SYS_API_PREFIX}/package-plans/${package_plan_id}`)
}

View File

@@ -3016,5 +3016,69 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
apply: 'Apply',
tools: 'Tools',
},
package: {
package: 'Package Management',
saas_personal: 'SaaS Personal',
commercial_deployment: 'Commercial Deployment',
noCommercialPackages: 'No commercial deployment packages available',
addPackage: 'Add Plan',
packageName: 'Plan Name',
packageNameZh: 'Plan Name (中文)',
packageNameEn: 'Plan Name (English)',
packageNamePlaceholder: '中文, 例如:记忆体验版',
packageNamePlaceholderEn: 'English, e.g. Memory Trial Plan',
packageCategory: 'Package Category',
price: 'Price',
pricePlaceholder: 'e.g. 0, 19, 299 or Contact Us',
billingPeriod: 'Billing Period',
monthly: 'Monthly',
yearly: 'Yearly',
permanent_free: 'Permanent Free',
local_deployment: 'Local Deployment',
coreValue: 'Core Value',
coreValueZh: 'Core Value (中文)',
coreValueEn: 'Core Value (English)',
coreValuePlaceholder: '中文, 一句话描述核心价值',
coreValuePlaceholderEn: 'EngLish, describe the core value in one sentence',
tech_support: 'Technical Support',
tech_support_zh: 'Technical Support (中文)',
tech_support_en: 'Technical Support (English)',
technicalSupportPlaceholder: '中文, 例如:社群交流、工单支持',
technicalSupportPlaceholderEn: 'English, e.g. Community support, ticket support',
sla: 'SLA & Compliance',
slaZh: 'SLA & Compliance (中文)',
slaEn: 'SLA & Compliance (English)',
slaPlaceholder: '中文, 例如:无、验证力加强+审计日志',
slaPlaceholderEn: 'English, e.g. None, dedicated compute pool + audit logs',
customPage: 'Chat Page Customization',
customPageZh: 'Chat Page Customization (中文)',
customPageEn: 'Chat Page Customization (English)',
customPagePlaceholder: '中文, 例如LOGO定制',
customPagePlaceholderEn: 'English, e.g. Logo customization',
primaryColor: 'Primary Color',
status: 'Status',
active: 'Active',
inactive: 'Inactive',
api_ops_rate_limit: 'API OPS Rate Limit',
ops: 'req/s',
pcs: 'pcs',
GB: 'GB',
tier_level: 'Tier Level',
numberPlaceholder: 'e.g. 10',
packageDetail: 'Package Detail',
basicInfo: 'Basic Info',
featureConfig: 'Billing Unit Quota',
workspace_quota: 'Workspace Quota',
skill_quota: 'Skill Library Quota',
app_quota: 'App Quota',
knowledge_capacity_quota: 'Knowledge Base Capacity',
memory_engine_quota: 'Memory Engine Quota',
end_user_quota: 'Memorable End Users',
ontology_project_quota: 'Ontology Project',
model_quota: 'Model Quota',
editPackage: 'Edit Package',
},
},
};

View File

@@ -2980,5 +2980,69 @@ export const zh = {
apply: '应用',
tools: '工具',
},
package: {
package: '套餐管理',
saas_personal: 'SaaS 个人版',
commercial_deployment: '商业化部署',
noCommercialPackages: '暂无商业化部署套餐',
addPackage: '添加套餐',
packageName: '套餐名称',
packageNameZh: '套餐名称 (中文)',
packageNameEn: '套餐名称 (English)',
packageNamePlaceholder: '中文, 例如:记忆体验版',
packageNamePlaceholderEn: 'English, e.g. Memory Trial Plan',
packageCategory: '套餐分类',
price: '价格',
pricePlaceholder: '例如: 0, 19, 299 或联系我们',
billingPeriod: '计费周期',
monthly: '月',
yearly: '年',
permanent_free: '永久免费',
local_deployment: '本地化部署',
coreValue: '核心价值',
coreValueZh: '核心价值 (中文)',
coreValueEn: '核心价值 (English)',
coreValuePlaceholder: '中文, 一句话描述核心价值',
coreValuePlaceholderEn: 'EngLish, describe the core value in one sentence',
tech_support: '技术支持',
tech_support_zh: '技术支持 (中文)',
tech_support_en: '技术支持 (English)',
technicalSupportPlaceholder: '中文, 例如:社群交流、工单支持',
technicalSupportPlaceholderEn: 'English, e.g. Community support, ticket support',
sla: 'SLA与合规',
slaZh: 'SLA与合规 (中文)',
slaEn: 'SLA与合规 (English)',
slaPlaceholder: '中文, 例如:无、验证力加强+审计日志',
slaPlaceholderEn: 'English, e.g. None, dedicated compute pool + audit logs',
customPage: '对应页面个性化配置',
customPageZh: '对应页面个性化配置 (中文)',
customPageEn: '对应页面个性化配置 (English)',
customPagePlaceholder: '中文, 例如LOGO定制',
customPagePlaceholderEn: 'English, e.g. Logo customization',
primaryColor: '主题色',
status: '状态',
active: '启用',
inactive: '停用',
api_ops_rate_limit: 'API OPS 频次',
ops: '次/秒',
pcs: '个',
GB: 'GB',
tier_level: '层级',
numberPlaceholder: '如: 10',
packageDetail: '套餐详情',
basicInfo: '基础信息',
featureConfig: '计费单元配额',
workspace_quota: '空间数量',
skill_quota: '技能库数量',
app_quota: '应用数量',
knowledge_capacity_quota: '知识库容量',
memory_engine_quota: '记忆引擎数量',
end_user_quota: '可记忆终端用户数',
ontology_project_quota: '本体工程',
model_quota: '可负载模型数量',
editPackage: '编辑套餐',
},
},
}

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-02 16:33:11
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-04 18:11:34
* @Last Modified time: 2026-04-13 16:53:15
*/
/**
* Route Configuration
@@ -76,13 +76,12 @@ const componentMap: Record<string, LazyExoticComponent<ComponentType<object>>> =
SpaceManagement: lazy(() => import('@/views/SpaceManagement')),
ApiKeyManagement: lazy(() => import('@/views/ApiKeyManagement')),
EmotionEngine: lazy(() => import('@/views/EmotionEngine')),
StatementDetail: lazy(() => import('@/views/UserMemoryDetail/pages/StatementDetail')),
ForgetDetail: lazy(() => import('@/views/UserMemoryDetail/pages/ForgetDetail')),
MemoryNodeDetail: lazy(() => import('@/views/UserMemoryDetail/pages/index')),
SelfReflectionEngine: lazy(() => import('@/views/SelfReflectionEngine')),
OrderPayment: lazy(() => import('@/views/OrderPayment')),
OrderHistory: lazy(() => import('@/views/OrderHistory')),
Pricing: lazy(() => import('@/views/Pricing')),
Package: lazy(() => import('@/views/Package')),
ToolManagement: lazy(() => import('@/views/ToolManagement')),
SpaceConfig: lazy(() => import('@/views/SpaceConfig')),
Ontology: lazy(() => import('@/views/Ontology')),

View File

@@ -7,7 +7,7 @@
{ "path": "/model", "element": "ModelManagement" },
{ "path": "/space", "element": "SpaceManagement" },
{ "path": "/tool", "element": "ToolManagement" },
{ "path": "/pricing", "element": "Pricing" },
{ "path": "/pricing", "element": "Package" },
{ "path": "/order-pay", "element": "OrderPayment" },
{ "path": "/orders", "element": "OrderHistory" },
{ "path": "/skills", "element": "Skills" },
@@ -48,7 +48,6 @@
{ "path": "/application/config/:id", "element": "ApplicationConfig" },
{ "path": "/application/config/:id/:source", "element": "ApplicationConfig" },
{ "path": "/user-memory/neo4j/:id", "element": "Neo4jUserMemoryDetail" },
{ "path": "/statement/:id", "element": "StatementDetail" },
{ "path": "/user-memory/detail/:id/:type", "element": "MemoryNodeDetail" },
{ "path": "/ontology/:id", "element": "OntologyDetail" }
]

View File

@@ -421,21 +421,7 @@
"display": false,
"level": 3,
"sort": 0,
"subs": [
{
"id": 2211,
"parent": 221,
"code": "statementDetail",
"label": "记忆详情",
"i18nKey": "menu.statementDetail",
"path": "/statement/:id",
"enable": true,
"display": false,
"level": 4,
"sort": 0,
"subs": null
}
]
"subs": []
},
{
"id": 222,

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-02 16:35:15
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-06 10:39:00
* @Last Modified time: 2026-04-14 14:43:54
*/
/**
* HTTP Request Utility Module
@@ -23,6 +23,7 @@ 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
@@ -74,6 +75,10 @@ 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) {

View File

@@ -0,0 +1,34 @@
export const billingUnits = [
{
key: 'workspace_quota',
unit: '个', placeholder: '如: 10',
},
{
key: 'skill_quota',
unit: '个', placeholder: '如: 10',
},
{
key: 'app_quota',
unit: '个', placeholder: '如: 10',
},
{
key: 'knowledge_capacity_quota',
unit: 'GB', placeholder: '如: 10',
},
{
key: 'memory_engine_quota',
unit: '个', placeholder: '如: 10',
},
{
key: 'end_user_quota',
unit: '个', placeholder: '如: 10',
},
{
key: 'ontology_project_quota',
unit: '个', placeholder: '如: 10',
},
{
key: 'model_quota',
unit: '次/秒', placeholder: '如: 10',
},
]

View File

@@ -0,0 +1,145 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-25
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-04-14 14:48:13
*/
/**
* 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 { useMemo, useState, useEffect, type FC } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { Flex, Row, Col, type SegmentedProps } from 'antd';
import clsx from 'clsx';
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 RbButton from '@/components/RbButton'
const Package: FC = () => {
const { t } = useTranslation();
const { language } = useI18n()
const navigate = useNavigate();
const [data, setData] = useState<Package[]>([])
const [activeTab, setActiveTab] = useState('saas_personal');
const formatTabItems = useMemo(() => {
return ['saas_personal', 'commercial_deployment'].map(value => ({
value,
label: t(`package.${value}`),
}))
}, [t])
/** Handle tab change */
const handleChangeTab = (value: SegmentedProps['value']) => {
setActiveTab(value as string);
}
const getList = () => {
getPackageList({ category: activeTab as Package['category'], status: true })
.then(res => {
setData(res as Package[] || [])
})
}
useEffect(() => {
getList()
}, [activeTab])
const getKeyWithLanguage = (key: string) => {
return (language === 'en' ? `${key}_en` : key) as keyof Package
}
/** Navigate to order history */
const goToHistory = () => {
navigate('/orders');
}
return (
<>
<Flex justify="space-between" className="rb:mb-4!">
<PageTabs
value={activeTab}
options={formatTabItems}
onChange={handleChangeTab}
/>
<RbButton className="rb:text-[#212332] rb:font-medium!" onClick={goToHistory}>
<div
className="rb:size-4 rb:bg-cover rb:bg-[url('@/assets/images/order/order.svg')]"
></div>
{t('pricing.orderHistory')}
</RbButton>
</Flex>
<BodyWrapper empty={data.length < 1}>
<Row gutter={[12, 12]} className="rb:max-h-[calc(100%-48px)]! rb:overflow-y-auto">
{data.map((pkg) => (
<Col key={pkg.id} span={8}>
<RbCard
className="rb:h-full! rb:shadow-md hover:rb:shadow-lg rb:transition-shadow"
bodyClassName="rb:p-6! rb:h-full!"
headerClassName="rb:min-h-0!"
>
<Flex vertical justify="space-between" className="rb:h-full!">
<div>
{/* Header */}
<div className="rb:text-center rb:mb-6">
<h3 className="rb:text-xl rb:font-bold rb:mb-2 rb:min-h-7" style={{ color: pkg.theme_color }}>
{String(pkg[getKeyWithLanguage('name')] ?? '')}
</h3>
<p className="rb:text-sm rb:text-gray-500 rb:mb-4 rb:min-h-5">{String(pkg[getKeyWithLanguage('core_value')] ?? '')}</p>
<div className="rb:text-4xl rb:font-bold rb:mb-2">
{pkg.billing_cycle !== 'permanent_free' && <>¥{pkg.price}</>}
{pkg.billing_cycle && <span className={clsx("", {
'rb:text-base rb:font-normal rb:text-gray-500': pkg.billing_cycle !== 'permanent_free'
})}>{pkg.billing_cycle !== 'permanent_free' && '/'}{t(`package.${pkg.billing_cycle}`)}</span>}
</div>
</div>
{/* Features */}
<div className="rb:space-y-3">
{billingUnits.map(({ key, unit }) => {
if (typeof pkg.quotas[key as keyof Package['quotas']] === 'number') {
return (
<div key={key} className="rb:flex rb:items-center rb:justify-between rb:text-sm">
<span className="rb:text-gray-500">{t(`package.${key}`)}</span>
<span>{pkg.quotas[key as keyof Package['quotas']]}{unit}</span>
</div>
)
}
})}
{pkg.tech_support &&
<div className="rb:flex rb:items-center rb:justify-between rb:text-sm">
<span className="rb:text-gray-500">{t(`package.tech_support`)}</span>
<span>{String(pkg[getKeyWithLanguage('tech_support')] ?? '')}</span>
</div>
}
{pkg.api_ops_rate_limit &&
<div className="rb:flex rb:items-center rb:justify-between rb:text-sm">
<span className="rb:text-gray-500">{t(`package.api_ops_rate_limit`)}</span>
<span>{pkg.api_ops_rate_limit}/)</span>
</div>
}
</div>
</div>
</Flex>
</RbCard>
</Col>
))}
</Row>
</BodyWrapper>
</>
);
};
export default Package;

View File

@@ -0,0 +1,61 @@
/*
* @Author: ZhaoYing
* @Date: 2026-04-14 11:35:01
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-04-14 14:28:46
*/
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;
}