Merge pull request #926 from SuanmoSuanyangTechnology/feature/package_zy
feat(web): package menu
This commit is contained in:
@@ -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';
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
]
|
||||
{
|
||||
key: 'api_ops_rate_limit',
|
||||
unit: 'ops', placeholder: 'numberPlaceholder',
|
||||
icon: 'api_ops',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -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,115 +28,282 @@ 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<string, ComponentType<SVGProps<SVGSVGElement>>> = {
|
||||
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 <Icon component={SvgComponent} style={{ color, fontSize: 16 }} />
|
||||
}
|
||||
return (
|
||||
<Flex key={titleKey} align="start" gap={16}>
|
||||
<Flex
|
||||
align="center"
|
||||
justify="center"
|
||||
className="rb:mt-1! rb:shrink-0 rb:rounded-lg rb:size-7"
|
||||
style={{ backgroundColor: `${theme_color}14` }}
|
||||
>{renderFeatureIcon(icon, theme_color)}</Flex>
|
||||
<div className="rb:text-[13px] rb:leading-4.5">
|
||||
<div className="rb:text-[#5F6266]">{t(`package.${titleKey}`)}</div>
|
||||
<div>{value} {unit ? t(`package.${unit}`) : ''}</div>
|
||||
</div>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
const Package: FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { language } = useI18n()
|
||||
const navigate = useNavigate();
|
||||
const [data, setData] = useState<Package[]>([])
|
||||
const scrollRef = useRef<HTMLDivElement>(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'].map(value => ({
|
||||
value,
|
||||
label: t(`package.${value}`),
|
||||
}))
|
||||
}, [t])
|
||||
/** Handle tab change */
|
||||
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'], status: true })
|
||||
.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
|
||||
}
|
||||
/** Navigate to order history */
|
||||
const goToHistory = () => {
|
||||
navigate('/orders');
|
||||
}
|
||||
|
||||
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 (
|
||||
<>
|
||||
<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>
|
||||
{showTabs && (
|
||||
<Flex justify="space-between" className="rb:mb-4!">
|
||||
<PageTabs
|
||||
value={activeTab}
|
||||
options={formatTabItems}
|
||||
onChange={handleChangeTab}
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
<BodyWrapper empty={filteredData.length < 1}>
|
||||
<div ref={scrollRef} className="rb:relative rb:mx-9">
|
||||
{showArrows && (
|
||||
<Flex
|
||||
align="center"
|
||||
justify="center"
|
||||
className={clsx("rb:absolute rb:-left-6 rb:top-1/2 rb:-translate-y-1/2 rb:-translate-x-3 rb:z-10 rb:h-25 rb:rounded-lg rb:w-6 rb:bg-[rgba(255,255,255,0.6)] rb:border rb:border-[rgba(255,255,255,0.6)]", {
|
||||
'rb:hover:border-[#171719] rb:cursor-pointer': currentPage > 0,
|
||||
'rb:cursor-not-allowed': currentPage === 0
|
||||
})}
|
||||
onClick={() => {
|
||||
if (currentPage === 0) return
|
||||
setCurrentPage(p => p - 1)
|
||||
}}
|
||||
>
|
||||
<Icon component={arrowSvg} style={{ color: currentPage === 0 ? '#E1E2E7' : '#171719', fontSize: 24 }} />
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
<Flex gap={GAP} justify="center">
|
||||
{pageData.map((pkg) => (
|
||||
<div key={pkg.id} style={{ width: CARD_WIDTH, flexShrink: 0 }}>
|
||||
<RbCard
|
||||
className="rb:h-full! rb:hover:shadow-[0px_4px_10px_0px_rgba(0,0,0,0.12)]!"
|
||||
bodyClassName="rb:p-0! rb:pb-4! rb:h-full!"
|
||||
headerClassName="rb:min-h-0!"
|
||||
>
|
||||
<div className="rb:px-5 rb:pt-4">
|
||||
<div className="rb:h-25!">
|
||||
{/* Header */}
|
||||
<Flex justify="space-between" align="start" className="rb:mb-1!">
|
||||
<Tooltip title={String(pkg[getKeyWithLanguage('name')] ?? '')}>
|
||||
<h3 className="rb:text-[18px] rb:font-bold rb:text-[MiSans-Bold] rb:w-54.5 rb:line-clamp-2" style={{ color: pkg.theme_color }}>
|
||||
{String(pkg[getKeyWithLanguage('name')] ?? '')}
|
||||
</h3>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
|
||||
{/* Subtitle */}
|
||||
<Tooltip title={String(pkg[getKeyWithLanguage('core_value')] ?? '')}>
|
||||
<p className="rb:text-[#5B6167] rb:mb-4 rb:line-clamp-1">
|
||||
{String(pkg[getKeyWithLanguage('core_value')] ?? '')}
|
||||
</p>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
{/* Price */}
|
||||
<div className="rb:h-10 rb:mb-4">
|
||||
{pkg.billing_cycle !== 'permanent_free' && <>
|
||||
<span className="rb:text-[#5B6167] rb:inline-block rb:leading-5 rb:pt-3.25 rb:pb-1.75 rb:mr-1">¥</span>
|
||||
<span className="rb:text-[28px] rb:text-[MiSans-Bold] rb:font-bold rb:leading-10">{pkg.price}</span>
|
||||
</>}
|
||||
{pkg.billing_cycle && (
|
||||
<span className={clsx({
|
||||
'rb:text-[28px] rb:text-[MiSans-Bold] rb:font-bold rb:leading-10': pkg.billing_cycle === 'permanent_free',
|
||||
'rb:text-[#5B6167] rb:inline-block rb:leading-5 rb:pt-3.25 rb:pb-1.75 rb:ml-1': pkg.billing_cycle !== 'permanent_free'
|
||||
})}>
|
||||
{pkg.billing_cycle !== 'permanent_free' && ' /'}
|
||||
{t(`package.${pkg.billing_cycle}`)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type={pkg.billing_cycle !== 'permanent_free' ? 'primary' : 'default'}
|
||||
block
|
||||
className={btnClassNames[pkg.billing_cycle === 'permanent_free' ? 'permanent_free' : 'default']}
|
||||
onClick={handleChoosePlan}
|
||||
>
|
||||
{t('pricing.contactBtn')}
|
||||
</Button>
|
||||
|
||||
<Divider className="rb:my-4" />
|
||||
|
||||
{/* 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']]}{t(`package.${unit}`)}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<Flex gap={12} vertical
|
||||
className={clsx("rb:space-y-3 rb:mb-4 rb:overflow-y-auto", {
|
||||
'rb:h-[calc(100vh-401px)]!': showTabs,
|
||||
'rb:h-[calc(100vh-346px)]!': !showTabs
|
||||
})}
|
||||
{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}{t('package.ops')}</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>
|
||||
}
|
||||
</div>
|
||||
>
|
||||
{billingUnits.map(({ key, unit, icon }) => {
|
||||
const value = pkg?.quotas?.[key as keyof Package['quotas']];
|
||||
if (value === undefined || value === null) return null;
|
||||
return (
|
||||
<UnitWrapper
|
||||
key={key}
|
||||
titleKey={key}
|
||||
value={value}
|
||||
unit={unit}
|
||||
icon={icon}
|
||||
theme_color={pkg.theme_color}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
{pkg.tech_support && (
|
||||
<UnitWrapper
|
||||
titleKey="tech_support"
|
||||
value={String(pkg[getKeyWithLanguage('tech_support')] ?? '')}
|
||||
icon="technical_support"
|
||||
theme_color={pkg.theme_color}
|
||||
/>
|
||||
)}
|
||||
{pkg.sla_compliance && (
|
||||
<UnitWrapper
|
||||
titleKey="sla"
|
||||
value={String(pkg[getKeyWithLanguage('sla_compliance')] ?? '')}
|
||||
icon="sla"
|
||||
theme_color={pkg.theme_color}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
</div>
|
||||
</Flex>
|
||||
</RbCard>
|
||||
</div>
|
||||
))}
|
||||
</Flex>
|
||||
|
||||
</RbCard>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
{showArrows && (
|
||||
<Flex
|
||||
align="center"
|
||||
justify="center"
|
||||
className={clsx("rb:absolute rb:-right-12 rb:top-1/2 rb:-translate-y-1/2 rb:-translate-x-3 rb:z-10 rb:h-25 rb:rounded-lg rb:w-6 rb:bg-[rgba(255,255,255,0.6)] rb:border rb:border-[rgba(255,255,255,0.6)]", {
|
||||
'rb:hover:border-[#171719] rb:cursor-pointer': currentPage < totalPages - 1,
|
||||
'rb:cursor-not-allowed': currentPage >= totalPages - 1
|
||||
})}
|
||||
onClick={() => {
|
||||
if (currentPage >= totalPages - 1) return
|
||||
setCurrentPage(p => p + 1)
|
||||
}}
|
||||
>
|
||||
<Icon component={arrowSvg} className="rb:rotate-180" style={{ color: currentPage >= totalPages - 1 ? '#E1E2E7' : '#171719', fontSize: 24 }} />
|
||||
</Flex>
|
||||
)}
|
||||
</div>
|
||||
</BodyWrapper>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user