Merge #52 into develop_web from feature/20251219_zy

feat(web): 1. user memory; 2. update workspace's model config

* feature/20251219_zy: (2 commits)
  feat(web): order
  feat(web): 1. user memory; 2. update workspace's model config

Signed-off-by: zhaoying <zhaoying@redbearai.com>
Merged-by: zhaoying <zhaoying@redbearai.com>

CR-link: https://codeup.aliyun.com/redbearai/python/redbear-mem-open/change/52
This commit is contained in:
赵莹
2025-12-25 11:55:06 +08:00
33 changed files with 1592 additions and 93 deletions

View File

@@ -131,6 +131,9 @@ export const getEmotionHealth = (group_id: string) => {
export const getEmotionSuggestions = (group_id: string) => {
return request.post(`/memory/emotion/suggestions`, { group_id, limit: 20 })
}
export const analyticsRefresh = (end_user_id: string) => {
return request.post('/memory-storage/analytics/generate_cache', { end_user_id })
}
/*************** end 用户记忆 相关接口 ******************************/
@@ -189,7 +192,7 @@ export const updateMemoryReflectionConfig = (values: SelfReflectionEngineConfig)
return request.post('/memory/reflection/save', values)
}
// 反思引擎-试运行
export const pilotRunMemoryReflectionConfig = (values: { config_id: number | string; dialogue_text: string; }) => {
export const pilotRunMemoryReflectionConfig = (values: { config_id: number | string; language_type: string; }) => {
return request.get('/memory/reflection/run', values)
}

17
web/src/api/order.ts Normal file
View File

@@ -0,0 +1,17 @@
import { request } from '@/utils/request'
import type { VoucherForm } from '@/views/OrderPayment/types'
export const getOrderListUrl = '/v1/orders/customer'
// 提交支付凭证API
export const submitPaymentVoucherAPI = (voucherData: VoucherForm) => {
return request.post('/v1/orders/', voucherData)
}
// 订单详情
export const getOrderDetail = (order_no: string) => {
return request.get(`/v1/orders/customer/${order_no}`)
}
export const orderStatusUrl = '/v1/order-status/'
export const getOrderStatus = () => {
return request.get(orderStatusUrl)
}

View File

@@ -1,5 +1,6 @@
import { request } from '@/utils/request'
import type { SpaceModalData } from '@/views/SpaceManagement/types'
import type { ConfigModalData } from '@/views/UserMemory/types'
// 空间列表
export const getWorkspaces = () => {
@@ -22,6 +23,6 @@ export const getWorkspaceModels = () => {
return request.get(`/workspaces/workspace_models`)
}
// 更新空间模型配置
export const updateWorkspaceModels = () => {
return request.post(`/workspaces/workspace_models`)
export const updateWorkspaceModels = (data: ConfigModalData) => {
return request.put(`/workspaces/workspace_models`, data)
}

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>菜单-收费管理</title>
<g id="V1.1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
<g id="首页-弹窗" transform="translate(-904, -514)" stroke="#5F6266">
<g id="编组-7" transform="translate(904, 314)">
<g id="菜单-收费管理" transform="translate(0, 200)">
<g id="编组-8" transform="translate(2, 2)">
<polyline id="路径" points="7.5 5.80029489 6 7.30029489 4.5 5.80029489"></polyline>
<path d="M9.5,2 L10.5,2 C11.3284271,2 12,2.67157288 12,3.5 L12,10.5 C12,11.3284271 11.3284271,12 10.5,12 L1.5,12 C0.671572875,12 0,11.3284271 0,10.5 L0,3.5 C0,2.67157288 0.671572875,2 1.5,2 L2.5,2 L2.5,2" id="路径"></path>
<path d="M2.5,2.5 L2.5,1 C2.5,0.44771525 2.94771525,1.11022302e-16 3.5,0 L8.5,0 C9.05228475,0 9.5,0.44771525 9.5,1 L9.5,2.5 L9.5,2.5" id="路径"></path>
<line x1="0" y1="4.01402447" x2="12" y2="4.01402447" id="路径-4"></line>
<line x1="4.01045351" y1="1.8" x2="8.01045351" y2="1.8" id="路径-5"></line>
<line x1="6" y1="7.22565966" x2="6" y2="10.0211794" id="路径-6"></line>
<line x1="4.5" y1="7.70544887" x2="7.5" y2="7.70544887" id="路径-7"></line>
<line x1="4.5" y1="9.20544887" x2="7.5" y2="9.20544887" id="路径-7"></line>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>菜单-收费管理</title>
<g id="V1.1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
<g id="首页-弹窗" transform="translate(-975, -514)" stroke="#212332">
<g id="编组-7" transform="translate(975, 314)">
<g id="菜单-收费管理" transform="translate(0, 200)">
<g id="编组-8" transform="translate(2, 2)">
<polyline id="路径" points="7.5 5.80029489 6 7.30029489 4.5 5.80029489"></polyline>
<path d="M9.5,2 L10.5,2 C11.3284271,2 12,2.67157288 12,3.5 L12,10.5 C12,11.3284271 11.3284271,12 10.5,12 L1.5,12 C0.671572875,12 0,11.3284271 0,10.5 L0,3.5 C0,2.67157288 0.671572875,2 1.5,2 L2.5,2 L2.5,2" id="路径"></path>
<path d="M2.5,2.5 L2.5,1 C2.5,0.44771525 2.94771525,1.11022302e-16 3.5,0 L8.5,0 C9.05228475,0 9.5,0.44771525 9.5,1 L9.5,2.5 L9.5,2.5" id="路径"></path>
<line x1="0" y1="4.01402447" x2="12" y2="4.01402447" id="路径-4"></line>
<line x1="4.01045351" y1="1.8" x2="8.01045351" y2="1.8" id="路径-5"></line>
<line x1="6" y1="7.22565966" x2="6" y2="10.0211794" id="路径-6"></line>
<line x1="4.5" y1="7.70544887" x2="7.5" y2="7.70544887" id="路径-7"></line>
<line x1="4.5" y1="9.20544887" x2="7.5" y2="9.20544887" id="路径-7"></line>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>注意</title>
<g id="V1.1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="收费管理" transform="translate(-280, -839)" fill="#FF5D34" fill-rule="nonzero">
<g id="编组-10" transform="translate(264, 823)">
<g id="注意" transform="translate(16, 16)">
<path d="M8.7755948,1.95407174 C9.31341765,1.01530942 10.6865824,1.01530942 11.2358482,1.95407174 L19.8066846,16.6349572 C20.3559505,17.5737195 19.6693682,18.7386896 18.5708364,18.75 L1.42916361,18.75 C0.330631841,18.75 -0.355950514,17.5737195 0.19331537,16.6349572 Z M10,15 C9.48223305,15 9.0625,15.419733 9.0625,15.9375 C9.0625,16.455267 9.48223305,16.875 10,16.875 C10.517767,16.875 10.9375,16.455267 10.9375,15.9375 C10.9375,15.419733 10.517767,15 10,15 Z M10,6.25 C9.48223305,6.25 9.0625,6.66973305 9.0625,7.1875 L9.0625,12.8125 C9.0625,13.330267 9.48223305,13.75 10,13.75 C10.517767,13.75 10.9375,13.330267 10.9375,12.8125 L10.9375,7.1875 C10.9375,6.66973305 10.517767,6.25 10,6.25 Z" id="形状结合"></path>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="12px" height="12px" viewBox="0 0 12 12" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>对号备份</title>
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="定价-支付" transform="translate(-941, -357)" fill="#5B6167" fill-rule="nonzero">
<g id="编组-8" transform="translate(360, 219)">
<g id="编组-2" transform="translate(581, 134)">
<g id="对号备份" transform="translate(0, 4)">
<path d="M6,0 C10,0 12,2 12,6 C12,10 10,12 6,12 C2,12 0,10 0,6 C0,2 2,0 6,0 Z M8.96637129,3.91456632 C8.74659858,3.69499924 8.39044332,3.69516594 8.17087624,3.91493864 L5.06113614,7.0275 L3.85141273,5.81979618 C3.63162085,5.60024831 3.2754656,5.60044612 3.05591773,5.82023801 C2.83636985,6.04002989 2.83656766,6.39618514 3.05635955,6.61573301 L4.6637238,8.22131277 C4.88350211,8.44084708 5.23963056,8.44066484 5.45918407,8.22090572 L8.9667436,4.71006136 C9.18631068,4.49028866 9.18614399,4.1341334 8.96637129,3.91456632 Z" id="形状结合"></path>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="32px" height="32px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>企业_画板 39@2x</title>
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="定价-支付" transform="translate(-404, -623)" fill="#5B6167" fill-rule="nonzero">
<g id="编组-9" transform="translate(360, 572)">
<g id="编组-13" transform="translate(24, 28)">
<g id="企业_画板-39" transform="translate(20, 23)">
<path d="M22.6288053,2.37305419 C23.0204473,2.23747924 23.3145868,2.25900923 23.6919502,2.44276018 C24.0617664,2.62288728 25.6534241,3.72773786 25.9496034,4.01380953 C26.0658721,4.12614915 26.3712305,4.58595288 26.3712305,4.72515169 L26.3712305,28.0018359 C26.4530266,28.0862505 27.1973505,27.7436893 27.3764451,28.1371977 C27.4557934,28.3119956 27.4672163,29.6287353 27.2467137,29.6287353 L21.8000336,29.6287353 C21.7818794,29.6287353 21.6233867,29.5236434 21.6056404,29.4931604 L21.5446504,5.02657147 C21.4958991,4.86989287 21.331899,4.74412365 21.166879,4.77098284 C21.0187893,4.79507085 20.3731919,5.12974486 20.1677838,5.2218335 C18.029459,6.17981122 15.6408506,7.2130373 13.5747348,8.29294721 C13.376262,8.3967601 13.2344958,8.39505476 13.2049187,8.65192239 C13.1657544,8.99022026 13.1547395,10.3811425 13.2102221,10.6870388 C13.2300082,10.7957546 13.3515804,10.9596809 13.449083,10.9831294 C13.6673418,11.0353556 15.7193829,10.1487892 16.1403981,10.0260044 C16.2782888,9.98571558 16.4594232,9.91110672 16.5977218,9.92389681 C16.9036922,9.95203501 18.7858177,11.1775388 19.1684847,11.4395224 C19.2908728,11.5232975 19.6443705,11.7298575 19.680475,11.8532818 L19.7406492,27.6469109 C19.9835897,28.1139624 20.7572867,27.6294311 20.9818689,28.04383 C21.1122123,28.2842837 21.1334262,29.458414 20.8600927,29.5944153 L15.2826613,29.5950548 L15.1829149,29.4283573 L15.1563975,12.6505308 C15.1329398,12.3595562 15.1168253,12.2231286 14.7978003,12.2116175 L7.01432266,14.8538369 L6.69162593,15.0586915 L6.6579692,29.3893475 C6.65144183,29.4910287 6.60044677,29.537073 6.52640195,29.5907914 C6.29610827,29.7579152 4.81786354,29.7711317 4.64346045,29.5325965 C4.54534596,29.3987269 4.56003253,28.3989682 4.61306739,28.1992296 C4.68996794,27.9097473 5.17544089,28.12995 5.23622699,27.6326287 L5.23459515,14.2139061 L5.32414247,14.0365502 L11.5253454,11.7079012 C11.6140768,11.6079253 11.6389624,11.4994227 11.653445,11.3674716 C11.7713456,10.2926777 11.5033156,8.89003123 11.6560968,7.84657308 C11.6811863,7.67582538 11.7862362,7.47629998 11.9263706,7.38357183 Z M13.1298539,26.7341249 C13.41767,26.7230401 13.7566852,26.6450206 13.8427648,27.033413 C13.8603581,27.1127115 13.87035,27.429576 13.8728157,27.8021561 L13.8727754,28.1865728 C13.8693843,28.7059304 13.8526579,29.2165747 13.8227748,29.2874531 C13.7956454,29.3518299 13.7146652,29.411517 13.6606105,29.4569218 C12.4008286,29.6148794 11.1332954,29.5374994 9.86617025,29.557324 C9.31134403,29.5660639 8.69348791,29.6911936 8.11398008,29.632146 C7.95915908,29.6163716 7.80291023,29.5944153 7.73049725,29.423028 C7.65910416,29.2537725 7.6701191,27.3030707 7.7443679,27.1365863 C7.77170125,27.0754071 7.80291023,27.0187044 7.8692038,26.9961085 C9.62119,26.9070042 11.3768478,26.8021255 13.1298539,26.734338 Z M13.6149189,22.525759 C13.8127797,22.624669 13.840929,22.7357297 13.8605111,22.949111 C13.902531,23.408062 13.902939,24.395457 13.8564315,24.85185 C13.8466405,24.9490547 13.8360335,25.1031753 13.748322,25.1609438 L7.69643255,26.0366386 C7.79169131,25.3491713 7.56833296,24.2673429 7.69643255,23.6310359 C7.70561166,23.5860575 7.77272115,23.5152856 7.79413908,23.4619936 Z M13.5565806,18.2619694 C13.7026304,18.3067348 13.8248146,18.3877387 13.8552076,18.5499596 C13.9153818,18.8720567 13.8951878,20.217361 13.8603071,20.5855024 C13.851128,20.6816413 13.8684663,20.7790591 13.7964613,20.8577182 L13.5476054,21.0041647 L7.69663653,22.3096065 L7.69663653,20.0395787 C7.69663653,20.0163434 7.79638286,19.8671257 7.8290197,19.8392007 Z M13.5115009,13.9915717 C13.6646901,13.9862425 13.8266504,14.1254413 13.8554116,14.2808409 C13.9096703,14.5739471 13.9006952,16.0603687 13.8598992,16.3835316 C13.8480683,16.4779651 13.8456206,16.5600348 13.7634165,16.6216404 L7.99179592,18.5844929 C7.73090521,18.5465489 7.71581067,18.222107 7.70030817,18.0093652 C7.671139,17.60733 7.60851707,16.2677813 7.89164163,16.041823 Z" id="形状结合"></path>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>订单</title>
<g id="V1.1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="收费管理" transform="translate(-1266, -130)" fill-rule="nonzero">
<g id="编组-5" transform="translate(264, 88)">
<g id="编组-9" transform="translate(986, 32)">
<g id="订单" transform="translate(16, 10)">
<rect id="矩形" fill="#000000" opacity="0" x="0" y="0" width="16" height="16"></rect>
<path d="M12.8333281,4 C12.8333281,3.72385763 13.0571858,3.5 13.3333281,3.5 C13.6094705,3.5 13.8333281,3.72385763 13.8333281,4 L13.8333281,12.8333281 C13.8333281,13.8458281 13.0125,14.6666719 12,14.6666719 L4,14.6666719 C2.9875,14.6666719 2.16667188,13.8458281 2.16667188,12.8333281 L2.16667188,3.16667188 C2.16667188,2.15417187 2.9875,1.33332812 4,1.33332812 L13.3333281,1.33332812 C13.6094705,1.33332812 13.8333281,1.55718575 13.8333281,1.83332812 C13.8333281,2.1094705 13.6094705,2.33332812 13.3333281,2.33332812 L4,2.33332812 C3.77898493,2.33332812 3.5670223,2.42112709 3.41074251,2.57740981 C3.25446272,2.73369252 3.16667188,2.94565681 3.16667188,3.16667188 L3.16667188,12.8333281 C3.16667188,13.0543432 3.25446272,13.2663075 3.41074251,13.4225902 C3.5670223,13.5788729 3.77898493,13.6666719 4,13.6666719 L12,13.6666719 C12.2210151,13.6666719 12.4329777,13.5788729 12.5892575,13.4225902 C12.7455373,13.2663075 12.8333323,13.0543432 12.8333281,12.8333281 L12.8333281,4 Z M5.33332812,6.83332812 C5.05718575,6.83332812 4.83332812,6.6094705 4.83332812,6.33332812 C4.83332812,6.05718575 5.05718575,5.83332812 5.33332812,5.83332812 L10.6666719,5.83332812 C10.9428142,5.83332812 11.1666719,6.05718575 11.1666719,6.33332812 C11.1666719,6.6094705 10.9428142,6.83332812 10.6666719,6.83332812 L5.33332812,6.83332812 Z M5.33332812,9.5 C5.05718575,9.5 4.83332812,9.27614237 4.83332812,9 C4.83332812,8.72385763 5.05718575,8.5 5.33332812,8.5 L8.66667188,8.5 C8.94281425,8.5 9.16667188,8.72385763 9.16667188,9 C9.16667188,9.27614237 8.94281425,9.5 8.66667188,9.5 L5.33332812,9.5 Z" id="形状" stroke="#212332" stroke-width="0.4" fill="#212332"></path>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>订单</title>
<g id="V1.1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="收费管理" transform="translate(-1266, -130)" fill-rule="nonzero">
<g id="编组-5" transform="translate(264, 88)">
<g id="编组-9" transform="translate(986, 32)">
<g id="订单" transform="translate(16, 10)">
<rect id="矩形" fill="#155EEF" opacity="0" x="0" y="0" width="16" height="16"></rect>
<path d="M12.8333281,4 C12.8333281,3.72385763 13.0571858,3.5 13.3333281,3.5 C13.6094705,3.5 13.8333281,3.72385763 13.8333281,4 L13.8333281,12.8333281 C13.8333281,13.8458281 13.0125,14.6666719 12,14.6666719 L4,14.6666719 C2.9875,14.6666719 2.16667188,13.8458281 2.16667188,12.8333281 L2.16667188,3.16667188 C2.16667188,2.15417187 2.9875,1.33332812 4,1.33332812 L13.3333281,1.33332812 C13.6094705,1.33332812 13.8333281,1.55718575 13.8333281,1.83332812 C13.8333281,2.1094705 13.6094705,2.33332812 13.3333281,2.33332812 L4,2.33332812 C3.77898493,2.33332812 3.5670223,2.42112709 3.41074251,2.57740981 C3.25446272,2.73369252 3.16667188,2.94565681 3.16667188,3.16667188 L3.16667188,12.8333281 C3.16667188,13.0543432 3.25446272,13.2663075 3.41074251,13.4225902 C3.5670223,13.5788729 3.77898493,13.6666719 4,13.6666719 L12,13.6666719 C12.2210151,13.6666719 12.4329777,13.5788729 12.5892575,13.4225902 C12.7455373,13.2663075 12.8333323,13.0543432 12.8333281,12.8333281 L12.8333281,4 Z M5.33332812,6.83332812 C5.05718575,6.83332812 4.83332812,6.6094705 4.83332812,6.33332812 C4.83332812,6.05718575 5.05718575,5.83332812 5.33332812,5.83332812 L10.6666719,5.83332812 C10.9428142,5.83332812 11.1666719,6.05718575 11.1666719,6.33332812 C11.1666719,6.6094705 10.9428142,6.83332812 10.6666719,6.83332812 L5.33332812,6.83332812 Z M5.33332812,9.5 C5.05718575,9.5 4.83332812,9.27614237 4.83332812,9 C4.83332812,8.72385763 5.05718575,8.5 5.33332812,8.5 L8.66667188,8.5 C8.94281425,8.5 9.16667188,8.72385763 9.16667188,9 C9.16667188,9.27614237 8.94281425,9.5 8.66667188,9.5 L5.33332812,9.5 Z" id="形状" stroke="#155EEF" stroke-width="0.4" fill="#155EEF"></path>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -38,6 +38,8 @@ import toolIcon from '@/assets/images/menu/tool.png';
import toolActiveIcon from '@/assets/images/menu/tool_active.png';
import apiKeyIcon from '@/assets/images/menu/apiKey.png';
import apiKeyActiveIcon from '@/assets/images/menu/apiKey_active.png';
import pricingIcon from '@/assets/images/menu/pricing.svg'
import pricingActiveIcon from '@/assets/images/menu/pricing_active.svg'
// 图标路径映射表
const iconPathMap: Record<string, string> = {
@@ -65,6 +67,8 @@ const iconPathMap: Record<string, string> = {
'toolActive': toolActiveIcon,
'apiKey': apiKeyIcon,
'apiKeyActive': apiKeyActiveIcon,
'pricing': pricingIcon,
'pricingActive': pricingActiveIcon
};
const { Sider } = Layout;

View File

@@ -19,6 +19,7 @@ interface TableComponentProps extends Omit<TableProps, 'pagination'> {
isScroll?: boolean;
scrollX?: number | string | true; // 支持自定义横向滚动宽度
scrollY?: number | string; // 支持自定义纵向滚动高度
currentPageKey?: string;
}
export interface TableRef {
loadData: () => void;
@@ -48,6 +49,7 @@ const TableComponent = forwardRef<TableRef, TableComponentProps>(({
isScroll = false,
scrollX,
scrollY,
currentPageKey = 'page',
...props
}, ref) => {
const { t } = useTranslation();
@@ -86,7 +88,7 @@ const TableComponent = forwardRef<TableRef, TableComponentProps>(({
...currentPagination,
...pageData,
})
params = {...params, ...pageData}
params = { ...params, ...pageData, [currentPageKey]: pageData.page}
}
setLoading(true)
// 构建查询参数并调用API
@@ -95,7 +97,7 @@ const TableComponent = forwardRef<TableRef, TableComponentProps>(({
// 支持两种响应格式:直接返回 total 或在 page 对象中返回
const totalCount = res.page?.total ?? res.total ?? 0;
setTotal(totalCount)
setData(Array.isArray(res.items) ? res.items : Array.isArray(res.hosts) ? res.hosts : res || [])
setData(Array.isArray(res.items) ? res.items : Array.isArray(res.hosts) ? res.hosts : Array.isArray(res.list) ? res.list : res || [])
setLoading(false)
})
.catch(err => {

View File

@@ -38,6 +38,9 @@ export const en = {
emotionEngine: 'Emotion Engine',
statementDetail: 'Emotion Memory',
selfReflectionEngine: 'Self Reflection Engine',
pricing: 'Pricing Management',
orderPayment: 'Order Payment',
orderHistory: 'Order History',
},
dashboard: {
totalMemoryCapacity: 'Total Memory Capacity',
@@ -326,6 +329,7 @@ export const en = {
publicApiCannotRefreshToken: 'Public API cannot refresh token',
refreshTokenNotExist: 'Refresh token does not exist',
reset: 'Reset',
refresh: 'Refresh'
},
model: {
searchPlaceholder: 'search model…',
@@ -1053,6 +1057,16 @@ export const en = {
MemorySummary: 'Episodic Memory',
Statement: 'Emotional Memory',
ExtractedEntity: 'Short-term Memory',
PERCEPTUAL_MEMORY: 'Perceptual Memory',
WORKING_MEMORY: 'Working Memory',
SHORT_TERM_MEMORY: 'Shot Term Memory',
LONG_TERM_MEMORY: 'Long Term Memory',
EXPLICIT_MEMORY: 'Explicit Memory',
IMPLICIT_MEMORY: 'Implicit Memory',
EMOTIONAL_MEMORY: 'Emotional Memory',
EPISODIC_MEMORY: 'Episodic Memory',
endUserProfile: 'Core Profile',
editEndUserProfile: 'Edit',
name: 'Name',
@@ -1721,6 +1735,123 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
privacy_types: 'Privacy Types',
summary: 'Audit Summary',
}
}
},
pricing: {
title: 'Flexible Pricing for Teams of All Sizes',
desc: 'Transparent pricing that helps you easily find a plan that fits your budget.',
solution: 'Product Positioning',
targetAudience: 'Target Audience',
orderPayment: 'Order Payment',
creationTime: 'Creation Time',
orderPaymentDesc: 'Please confirm the order information and complete the payment.',
orderInformation: 'Order Information',
paymentMethod: 'Payment Method',
paymentVoucher: 'Payment Voucher',
corporateTransfer: 'Corporate transfer',
corporateTransferDesc: 'Pay via corporate bank transfer.',
payeeInformation: 'Payee Account Information',
receivingEntity: 'Receiving Entity',
bankName: 'Bank Name',
bankAccountNumber: 'Bank Account Number',
pay_txn_id: 'Payment Transaction ID',
pay_txn_idPlaceholder: 'Please enter the bank transfer transaction ID.',
pay_txn_idDesc: 'Please fill in the serial number on the bank transfer voucher for verification of payment',
payer: 'Payer(Company/Individual)',
payerPlaceholder: 'Please enter the bank transfer transaction ID.',
redirectCountdown: 'seconds until redirecting to Memory Bear...',
confirmRedirect: 'Payment voucher submitted successfully!',
confirmRedirectContent: 'We will verify your payment information within 1-3 business days. Would you like to go to Memory Bear now to start your experience?',
stayCurrentPage: 'Stay on current page',
goBack: 'Go to backend',
transferDate: 'Transfer Date',
payerAccount: 'Payer Account',
remark: 'Remark',
remarkPlaceholder: 'Please provide any additional remarks here (optional).',
confirm: 'SUBMIT PAYMENT',
submitting: 'Submitting...',
payInfo: 'After submission, we will verify your payment information within 13 business days.',
paySuccess: 'Once verified, your subscription service will be automatically activated by the system.',
comboName: 'Combo Name',
spAndTa: 'Solution positioning and target audience',
versionInformation: 'Version information',
orderCycle: 'Order Cycle',
orderAmount: 'Order Amount',
personal: {
type: 'Personal',
label: 'Current Package',
typeDesc: 'For individuals',
solution: "A person's second brain, capable of storing up to 2000 memories.",
targetAudience: 'individual users, students, and first-time users',
priceDesc: '/Forever free',
supportServices: 'Community Forum + Email Support',
},
team: {
type: 'Team',
label: 'Small Team',
typeDesc: 'Small Team Version',
solution: "Enable every team to build a shared second brain in seconds.",
targetAudience: 'Small teams, early-stage startups, and small projects.',
priceDesc: '/Month',
supportServices: 'Standard customer service support',
},
biz: {
type: 'Biz+',
label: 'Most Popular',
typeDesc: 'Enterprise Growth Edition',
solution: "Scale your organization with a powerful, enterprise-ready second-brain system.",
targetAudience: 'Growing teams, startups, and SMBs requiring advanced memory capabilities.',
priceDesc: '/Month/workspace',
supportServices: 'Priority customer service support',
},
commerce: {
type: 'Commerce',
label: 'Commercial OEM',
typeDesc: 'Commercial OEM version',
solution: "Seamlessly integrate advanced memory capabilities into your SaaS or enterprise product.",
targetAudience: 'Large enterprises, SaaS vendors, and system integrators requiring fully customizable and secure deployment.',
priceDesc: 'On-premises deployment',
supportServices: 'Standard customer service support',
flexibleDeployment: 'Supports localized deployment in data centers',
reliableGuarantee: '99.9% SLA guarantee'
},
mostPopular: 'Most Popular',
startedBtn: 'GET STARTED',
choosePlanBtn: 'CHOOSE PLAN',
contactBtn: 'CONTACT US',
memoryCapacity: 'Memory capacity:',
entries: 'entries',
intelligentSearchFrequency: 'Intelligent search frequency:',
timesMonth: 'times/month',
supportServices: 'Support Services:',
flexibleDeployment: 'Flexible deployment:',
reliableGuarantee: 'Reliable guarantee:',
alertTitle: 'Intellectual Property Authorization Reminder',
alertContent: 'Please note: Using certain AI models (such as GPT-4, Claude, etc.) may involve third-party API call fees, which are not included in the Memory Bear platform subscription fee. You need to pay the relevant fees separately to the model provider. Memory Bear only charges platform management and service fees and does not bear the usage fees of third-party APIs.',
currentAccountType: 'Current Account Type',
validUntil: 'Valid Until',
orderHistory: 'Order History',
order_no: 'Order Number',
product_type: 'Package Name',
payable_amount: 'Order Amount',
status: 'Order Status',
pay_time: 'Payment Time',
viewDetail: 'View Details',
PENDING: 'Pending Review',
APPROVED: 'Approved',
REJECTED: 'Rejected',
allStatus: 'All Status',
allTime: 'All Time',
today: 'Today',
week: 'Last Week',
month: 'Last Month',
threeMonth: 'Last Three Months',
year: 'Last Year',
searchPlaceholder: 'Search order number',
allType: 'All Packages',
orderDetail: 'Order Details',
orderInfo: 'Order Information',
orderPayInfo: 'Payment Information',
create_time: 'Creation Time',
},
},
};

View File

@@ -38,6 +38,9 @@ export const zh = {
emotionEngine: '情感引擎',
statementDetail: '情绪记忆',
selfReflectionEngine: '反思引擎',
pricing: '收费管理',
orderPayment: '订单支付',
orderHistory: '订单记录',
},
knowledgeBase: {
home: '首页',
@@ -808,7 +811,8 @@ export const zh = {
logoutApiCannotRefreshToken: '退出登录接口不能刷新token',
publicApiCannotRefreshToken: '公共接口不能刷新token',
refreshTokenNotExist: '刷新token不存在',
reset: '重置'
reset: '重置',
refresh: '刷新'
},
product: {
applicationManagement: '应用管理',
@@ -1124,10 +1128,21 @@ export const zh = {
nodeStatistics: '记忆分类',
total: '总计',
Chunk: '长期记忆',
MemorySummary: '情景记忆',
Statement: '情绪记忆',
ExtractedEntity: '短期记忆',
PERCEPTUAL_MEMORY: '感知记忆',
WORKING_MEMORY: '工作记忆',
SHORT_TERM_MEMORY: '短期记忆',
LONG_TERM_MEMORY: '长期记忆',
EXPLICIT_MEMORY: '显性记忆',
IMPLICIT_MEMORY: '隐性记忆',
EMOTIONAL_MEMORY: '情绪记忆',
EPISODIC_MEMORY: '情景记忆',
endUserProfile: '核心档案',
editEndUserProfile: '编辑',
name: '姓名',
@@ -1808,6 +1823,123 @@ export const zh = {
privacy_types: '隐私类型',
summary: '审核摘要',
}
}
},
pricing: {
title: '灵活定价,满足各类团队需求',
desc: '透明的定价策略,助您轻松找到符合预算的方案',
solution: '方案定位',
targetAudience: '目标人群',
orderPayment: '订单支付',
orderPaymentDesc: '请确认订单信息并完成支付',
orderInformation: '订单信息',
paymentMethod: '支付方式',
paymentVoucher: '支付凭证',
corporateTransfer: '企业转账',
corporateTransferDesc: '通过企业对公账户转账支付',
receivingEntity: '收款单位',
bankName: '开户银行',
bankAccountNumber: '银行账号',
pay_txn_id: '支付流水号',
payer: '付款单位/个人',
transferDate: '转账日期',
payerAccount: '付款账号',
remark: '备注信息',
remarkPlaceholder: '如有其他说明,请在此填写(选填)',
confirm: '确认支付',
submitting: '提交中...',
payInfo: '提交后我们将在1-3个工作日内核实您的付款信息',
paySuccess: '核实通过后,系统将自动开通您的套餐服务',
pay_txn_idPlaceholder: '请输入支付流水号',
pay_txn_idDesc: '请填写转账时的交易流水号,以便我们快速确认您的付款',
payerPlaceholder: '请输入付款单位或个人姓名',
redirectCountdown: '秒后跳转后台...',
confirmRedirect: '支付凭证已提交成功!',
confirmRedirectContent: '我们将在1-3个工作日内核实您的付款信息。是否立即跳转到记忆熊开始体验',
stayCurrentPage: '留在当前页',
goBack: '跳转后台',
payeeInformation: '收款信息',
creationTime: '创建时间',
comboName: '套餐名称',
spAndTa: '方案定位与目标人群',
versionInformation: '版本信息',
orderCycle: '订购周期',
orderAmount: '订单金额',
personal: {
type: '个人版',
label: '当前安装包',
typeDesc: '个人玩家版本',
solution: '个人的第二大脑最多可存储2000条记忆。',
targetAudience: '个人用户、学生及初次使用者',
priceDesc: '/永久免费',
supportServices: '社区论坛 + 邮件支持'
},
team: {
type: '团队版',
label: '小型团队',
typeDesc: '小型团队版本',
solution: '让每一条业务记录瞬间成为团队的第二大脑。',
targetAudience: '初创团队、小微型企业、小型项目',
priceDesc: '/月',
supportServices: '标准客服支持'
},
biz: {
type: '企业增长版',
label: '最受欢迎',
typeDesc: '企业增长版本',
solution: '让每一条业务记录瞬间成为团队的第二大脑。',
targetAudience: '初创团队、小微型企业、小型项目',
priceDesc: '/月/工作区',
supportServices: '优先客服支持'
},
commerce: {
type: '商业OEM版',
label: '商业OEM',
typeDesc: '商业OEM版本',
solution: '将强大的记忆能力无缝嵌入您的SaaS产品中。',
targetAudience: '需要集成解决方案的大型企业、SaaS厂商及系统集成商。',
priceDesc: '本地化部署',
supportServices: '标准客服支持',
flexibleDeployment: '支持在数据中心进行本地化部署',
reliableGuarantee: '99.9% SLA保障'
},
mostPopular: '最受欢迎',
startedBtn: '立即开始',
choosePlanBtn: '选择方案',
contactBtn: '联系我们',
memoryCapacity: '记忆容量:',
entries: '条',
intelligentSearchFrequency: '智能搜索次数:',
timesMonth: '次/月',
supportServices: '支持服务:',
flexibleDeployment: '灵活部署:',
reliableGuarantee: '可靠保障:',
alertTitle: '知识产权授权提醒',
alertContent: '请注意使用某些AI模型如GPT-4、Claude等可能涉及第三方API调用费用这些费用不包含在Memory Bear平台订阅费中。您需要单独向模型提供商支付相关费用。Memory Bear仅收取平台管理和服务费不承担第三方API的使用费用。',
currentAccountType: '当前账户类型',
validUntil: '有效期至',
orderHistory: '订单记录',
order_no: '订单号',
product_type: '套餐名称',
payable_amount: '订单金额',
status: '订单状态',
pay_time: '支付时间',
viewDetail: '查看详情',
PENDING: '待审核',
APPROVED: '审核通过',
REJECTED: '审核不通过',
allStatus: '全部状态',
allTime: '全部时间',
today: '今天',
week: '最近一周',
month: '最近一月',
threeMonth: '最近三个月',
year: '最近一年',
searchPlaceholder: '搜索订单号',
allType: '全部套餐',
orderDetail: '订单详情',
orderInfo: '订单信息',
orderPayInfo: '支付信息',
create_time: '创建时间',
},
},
}

View File

@@ -58,6 +58,9 @@ const componentMap: Record<string, LazyExoticComponent<ComponentType<object>>> =
EmotionEngine: lazy(() => import('@/views/EmotionEngine')),
StatementDetail: lazy(() => import('@/views/UserMemoryDetail/pages/StatementDetail')),
SelfReflectionEngine: lazy(() => import('@/views/SelfReflectionEngine')),
OrderPayment: lazy(() => import('@/views/OrderPayment')),
OrderHistory: lazy(() => import('@/views/OrderHistory')),
Pricing: lazy(() => import('@/views/Pricing')),
Login: lazy(() => import('@/views/Login')),
InviteRegister: lazy(() => import('@/views/InviteRegister')),
NoPermission: lazy(() => import('@/views/NoPermission')),

View File

@@ -5,6 +5,9 @@
{ "path": "/user-management", "element": "UserManagement" },
{ "path": "/model", "element": "ModelManagement" },
{ "path": "/space", "element": "SpaceManagement" },
{ "path": "/pricing", "element": "Pricing" },
{ "path": "/order-pay", "element": "OrderPayment" },
{ "path": "/orders", "element": "OrderHistory" },
{ "path": "/no-permission", "element": "NoPermission" }
]
},

View File

@@ -0,0 +1,76 @@
import { forwardRef, useImperativeHandle, useState, useCallback } from 'react';
import { Descriptions } from 'antd';
import { useTranslation } from 'react-i18next';
import dayjs from 'dayjs';
import type { Order, OrderDetailRef } from '../types'
import RbModal from '@/components/RbModal'
import { getOrderDetail } from '@/api/order'
import { STATUS } from '../index';
const OrderDetail = forwardRef<OrderDetailRef>((_props, ref) => {
const { t } = useTranslation();
const [visible, setVisible] = useState(false);
const [data, setData] = useState({})
// 封装取消方法,添加关闭弹窗逻辑
const handleClose = () => {
setVisible(false);
};
const handleOpen = (order: Order) => {
setVisible(true);
getOrderDetail(order.order_no)
.then(res => {
setData(res as Order)
})
};
const formatItems = useCallback(() => {
if (!data) return []
return ['order_no', 'product_type', 'payable_amount', 'status', 'pay_time', 'create_time'].map(key => {
const value = (data as any)[key]
return {
key,
label: t(`pricing.${key}`),
children: ['pay_time', 'create_time'].includes(key) && value
? dayjs(value).format('YYYY-MM-DD HH:mm:ss')
: key === 'status' && value
? t(`pricing.${STATUS[value as keyof typeof STATUS].key}`)
: key === 'product_type' && value
? t(`pricing.${value.toLowerCase()}.type`)
: value
}
})
}, [data])
const formatPayItems = useCallback(() => {
if (!data) return []
return ['pay_txn_id', 'payer'].map(key => ({
key,
label: t(`pricing.${key}`),
children: (data as any)[key]
}))
}, [data])
// 暴露给父组件的方法
useImperativeHandle(ref, () => ({
handleOpen,
handleClose
}));
// ['pay_txn_id', 'payer']
// ['pay_txn_id', 'payer']
return (
<RbModal
title={t('pricing.orderDetail')}
open={visible}
footer={null}
onCancel={handleClose}
width={1000}
>
<Descriptions title={t('pricing.orderInfo')} column={2} items={formatItems()} classNames={{ label: 'rb:w-40' }} />
<Descriptions title={t('pricing.orderPayInfo')} column={2} items={formatPayItems()} classNames={{ label: 'rb:w-40' }} className="rb:mt-6!" />
</RbModal>
);
});
export default OrderDetail;

View File

@@ -0,0 +1,227 @@
import React, { useRef, useState, useEffect } from 'react';
import { Button, Space, Select, Flex } from 'antd';
import { useTranslation } from 'react-i18next';
import type { ColumnsType } from 'antd/es/table';
import type { SelectProps } from 'antd/es/select'
import dayjs from 'dayjs';
import Table, { type TableRef } from '@/components/Table'
import StatusTag from '@/components/StatusTag'
import { getOrderListUrl, getOrderStatus } from '@/api/order'
import { formatDateTime } from '@/utils/format';
import type { Order, OrderDetailRef, Query } from './types'
import OrderDetail from './components/OrderDetail'
import SearchInput from '@/components/SearchInput'
import { PRICE_LIST } from '@/views/Pricing'
export const STATUS = {
100: {
status: 'warning',
key: 'PENDING'
},
150: {
key: 'APPROVED',
status: 'success'
},
500: {
key: 'REJECTED',
status: 'error'
}
}
const OrderHistory: React.FC = () => {
const { t } = useTranslation();
const orderDetailRef = useRef<OrderDetailRef>(null)
const tableRef = useRef<TableRef>(null);
const [query, setQuery] = useState<Query>({
status: null,
product_type: null,
start_time: null,
end_time: null
} as Query)
const [statusOptions, setStatusOptions] = useState<SelectProps['options']>([])
const [timeType, setTimeType] = useState<string>('all')
const timeOptions = [
{ label: t('pricing.allTime'), value: 'all' },
{ label: t('pricing.today'), value: 'today' },
{ label: t('pricing.week'), value: '7d' },
{ label: t('pricing.month'), value: '1month' },
{ label: t('pricing.threeMonth'), value: '3month' },
{ label: t('pricing.year'), value: '1year' },
]
const productTypeOptions = [
{ label: t('pricing.allType'), value: null },
...PRICE_LIST.map(vo => ({
label: t(`pricing.${vo.type}.type`),
value: vo.type
}))
]
const handleView = (order: Order) => {
orderDetailRef.current?.handleOpen(order)
}
useEffect(() => {
getStatus()
}, [])
const getStatus = () => {
getOrderStatus()
.then(res => {
const response = res as Record<string, { value: number }>
setStatusOptions([
{
label: t(`pricing.allStatus`),
value: null
},
...Object.keys(response).map(key => ({
label: t(`pricing.${key}`),
value: response[key].value
}))
])
})
}
const handleChangeStatus = (value: string) => {
if (value !== query.status) {
setQuery(prev => ({
...prev,
status: value
}))
}
}
const handleChangeType = (value: string) => {
if (value !== query.product_type) {
setQuery(prev => ({
...prev,
product_type: value
}))
}
}
const handleChangeTime = (value: string) => {
setTimeType(value)
let start_time = null;
let end_time: number | null = dayjs().endOf('day').valueOf()
switch(value) {
case 'all':
start_time = null;
end_time = null
break
case 'today':
start_time = dayjs().startOf('day').valueOf()
break
case '7d':
start_time = dayjs().subtract(7, 'day').startOf('day').valueOf()
break
case '1month':
start_time = dayjs().subtract(1, 'month').startOf('day').valueOf()
break
case '3month':
start_time = dayjs().subtract(3, 'month').startOf('day').valueOf()
break
case '1year':
start_time = dayjs().subtract(1, 'year').startOf('day').valueOf()
break
}
setQuery(prev => ({
...prev,
start_time,
end_time
}))
}
// 表格列配置
const columns: ColumnsType = [
{
title: t('pricing.order_no'),
dataIndex: 'order_no',
key: 'order_no',
fixed: 'left',
},
{
title: t('pricing.product_type'),
dataIndex: 'product_type',
key: 'product_type',
render: (type) => t(`pricing.${type.toLowerCase()}.type`)
},
{
title: t('pricing.payable_amount'),
dataIndex: 'payable_amount',
key: 'payable_amount',
render: (amount: number) => `${amount}`,
},
{
title: t('pricing.status'),
dataIndex: 'status',
key: 'status',
render: (status: number) => <StatusTag status={STATUS[status as keyof typeof STATUS].status as 'warning' | 'success' | 'error'} text={t(`pricing.${STATUS[status as keyof typeof STATUS].key}`)} />
},
{
title: t('pricing.pay_time'),
dataIndex: 'pay_time',
key: 'pay_time',
render: (pay_time: unknown) => formatDateTime(pay_time as string, 'YYYY-MM-DD HH:mm:ss'),
},
{
title: t('common.operation'),
key: 'action',
fixed: 'right',
render: (_, record) => (
<Space size="large">
<Button
type="link"
onClick={() => handleView(record as Order)}
>
{t(`common.viewDetail`)}
</Button>
</Space>
),
},
];
return (
<div className="rb:h-[calc(100vh-80px)] rb:overflow-hidden">
<Flex justify="space-between" className="rb:mb-4!">
<Space size={10}>
<Select
defaultValue={query.status}
placeholder={t('common.select')}
options={statusOptions}
className="rb:w-30"
onChange={handleChangeStatus}
/>
<Select
defaultValue={query.product_type}
placeholder={t('common.select')}
options={productTypeOptions}
className="rb:w-30"
onChange={handleChangeType}
/>
<Select
defaultValue={timeType}
placeholder={t('common.select')}
options={timeOptions}
className="rb:w-30"
onChange={handleChangeTime}
/>
</Space>
<SearchInput
placeholder={t('pricing.searchPlaceholder')}
onSearch={(value) => setQuery(prev => ({ ...prev, search: value }))}
className="rb:w-70"
/>
</Flex>
<Table
ref={tableRef}
apiUrl={getOrderListUrl}
apiParams={query}
columns={columns}
rowKey="id"
currentPageKey="page_index"
isScroll={true}
/>
<OrderDetail ref={orderDetailRef} />
</div>
);
};
export default OrderHistory;

View File

@@ -0,0 +1,43 @@
export interface Query {
product_type?: string | null;
status?: string | null;
search?: string;
page_index?: number;
page_size?: number;
start_time?: number | null;
end_time?: number | null;
[key: string]: string | number | null | undefined;
}
export interface Order {
order_no: string;
user_id: string;
tenant_id: string;
uname: string;
status: number;
product_type: string;
valid: number;
payable_amount: string;
pay_status: number;
pay_txn_id: string;
pay_time: number;
payer: string;
servicer_id: number;
valid_time: number;
remarks: string;
closed: number;
service_status: number;
ship_status: number;
invite_code: string;
from_view: string;
tags: string;
app_id: number;
id: number;
optimistic: number;
create_time: number;
update_time: number;
deleted: number;
}
export interface OrderDetailRef {
handleOpen: (order: Order) => void;
}

View File

@@ -0,0 +1,391 @@
import React, { useState, useEffect, useMemo } from 'react';
import { useSearchParams, useNavigate } from 'react-router-dom';
import { Button, Input, App, Form, DatePicker } from 'antd';
import { useTranslation } from 'react-i18next';
import copy from 'copy-to-clipboard'
import dayjs from 'dayjs';
import { submitPaymentVoucherAPI } from '@/api/order';
import corporateImg from '@/assets/images/order/corporate.svg'
import checkImg from '@/assets/images/order/check.svg'
import type { PriceItem, VoucherForm } from './types'
const { TextArea } = Input;
const paymentInfo = {
payee: '上海算模算样科技有限公司',
bankName: '交通银行上海同济支行',
bankAccount: '3100 6634 4013 0082 44111'
};
const OrderPayment: React.FC = () => {
const [searchParams] = useSearchParams();
const { message, modal } = App.useApp()
const navigate = useNavigate();
const { t } = useTranslation();
const [form] = Form.useForm<VoucherForm>()
const [isSubmitting, setIsSubmitting] = useState(false);
const [selectedType, setSelectedType] = useState('biz');
const PRICE_LIST: PriceItem[] = [
{
type: 'personal',
label: 'pricing.personal.label',
typeDesc: 'pricing.personal.typeDesc',
priceDescObj: {
solution: 'pricing.personal.solution',
targetAudience: 'pricing.personal.targetAudience',
},
priceObj: {
type: 'default',
price: 0,
time: 'pricing.personal.priceDesc',
},
btnType: 'started',
memoryCapacity: '2000',
intelligentSearchFrequency: '100',
},
{
type: 'team',
label: 'pricing.team.label',
typeDesc: 'pricing.team.typeDesc',
priceDescObj: {
solution: 'pricing.team.solution',
targetAudience: 'pricing.team.targetAudience',
},
priceObj: {
type: 'default',
price: 19,
time: 'pricing.team.priceDesc'
},
btnType: 'choosePlan',
memoryCapacity: '20,000',
intelligentSearchFrequency: '10,000',
},
{
type: 'biz',
label: 'pricing.biz.label',
typeDesc: 'pricing.biz.typeDesc',
priceDescObj: {
solution: 'pricing.biz.solution',
targetAudience: 'pricing.biz.targetAudience',
},
mostPopular: true,
priceObj: {
type: 'default',
price: 299,
time: 'pricing.biz.priceDesc'
},
btnType: 'choosePlan',
memoryCapacity: '100,000',
intelligentSearchFrequency: '50,000',
},
{
type: 'commerce',
label: 'pricing.commerce.label',
typeDesc: 'pricing.commerce.typeDesc',
priceDescObj: {
solution: 'pricing.commerce.solution',
targetAudience: 'pricing.commerce.targetAudience',
},
priceObj: {
type: 'commerce',
time: 'pricing.commerce.priceDesc'
},
btnType: 'contact',
memoryCapacity: '20,000',
intelligentSearchFrequency: '10,000',
flexibleDeployment: true,
reliableGuarantee: true
},
];
const selectedPlan = useMemo(() => {
return PRICE_LIST.find(item => item.type === selectedType) || PRICE_LIST[2];
}, [selectedType]);
const translations = useMemo(() => ({
title: t('pricing.title'),
desc: t('pricing.desc'),
mostPopular: t('pricing.mostPopular'),
startedBtn: t('pricing.startedBtn'),
choosePlanBtn: t('pricing.choosePlanBtn'),
contactBtn: t('pricing.contactBtn'),
memoryCapacity: t('pricing.memoryCapacity'),
entries: t('pricing.entries'),
intelligentSearchFrequency: t('pricing.intelligentSearchFrequency'),
timesMonth: t('pricing.timesMonth'),
supportServices: t('pricing.supportServices'),
flexibleDeployment: t('pricing.flexibleDeployment'),
reliableGuarantee: t('pricing.reliableGuarantee'),
getItemType: (type: string) => t(`pricing.${type}.type`),
getItemLabel: (labelKey: string) => t(labelKey),
getItemDesc: (descKey: string) => t(descKey),
getPriceKey: (key: string) => t(`pricing.${key}`),
getItemPriceDesc: (descKey: string) => t(descKey),
getPriceTime: (timeKey: string) => t(timeKey),
getTypeSupportService: (type: string, key: string) => t(`pricing.${type}.${key}`),
}), [t]);
const getProductType = (type: string) => {
const typeMap: Record<string, string> = {
'personal': 'FREE',
'team': 'TEAM',
'biz': 'ENTERPRISE',
'commerce': 'OEM'
};
return typeMap[type] || 'ENTERPRISE';
};
const generateOrderNumber = () => {
const date = new Date();
const dateStr = date.getFullYear().toString() +
(date.getMonth() + 1).toString().padStart(2, '0') +
date.getDate().toString().padStart(2, '0');
const random = Math.floor(Math.random() * 1000000).toString().padStart(6, '0');
return `ORD-${dateStr}${random}`;
};
const orderInfo = useMemo(() => {
const plan = selectedPlan;
return {
orderNumber: generateOrderNumber(),
creationTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
comboName: t(`pricing.${plan.type}.type`),
comboEdition: t(plan.typeDesc),
solutionPositioning: t(plan.priceDescObj.solution),
targetAudience: t(plan.priceDescObj.targetAudience),
memoryCapacity: `${plan.memoryCapacity} ${t('pricing.entries')}`,
searchFrequency: `${plan.intelligentSearchFrequency} ${t('pricing.timesMonth')}`,
supportServices: t(`pricing.${plan.type}.supportServices`),
flexibleDeployment: t(`pricing.${plan.type}.flexibleDeployment`),
reliableGuarantee: t(`pricing.${plan.type}.reliableGuarantee`),
orderingCycle: '1 month',
orderAmount: plan.priceObj.price || 'Contact Us'
};
}, [selectedPlan, t]);
const copyText = (text: string) => {
copy(text)
message.success(t('common.copySuccess'))
};
const submitPayment = (values: VoucherForm) => {
if (isSubmitting) return;
setIsSubmitting(true);
const submitData = {
product_type: getProductType(selectedType),
...values,
payable_amount: orderInfo.orderAmount,
pay_time: values.transferDate.valueOf(),
};
submitPaymentVoucherAPI(submitData)
.then(res => {
form.resetFields()
modal.confirm({
title: t('pricing.confirmRedirect'),
content: t('pricing.confirmRedirectContent'),
okText: t('pricing.goBack'),
cancelText: t('pricing.stayCurrentPage'),
onOk() {
navigate('/pricing')
},
});
})
.finally(() => {
setIsSubmitting(false);
})
};
useEffect(() => {
const type = searchParams.get('type');
if (type && PRICE_LIST.find(item => item.type === type)) {
setSelectedType(type);
}
}, [searchParams]);
return (
<div className="rb:w-full rb:pb-20">
{/* 订单信息 */}
<div className="rb:mb-6">
<h2 className="rb:text-[16px] rb:text-lg rb:font-semibold rb:mb-4">{t('pricing.orderInformation')}</h2>
<div className="rb:flex rb:flex-col rb:items-start rb:gap-8 rb:mb-6 rb:text-[12px] ">
<div className="rb:flex rb:items-center rb:gap-2">
<span className="rb:text-[#5B6167]">{t('pricing.creationTime')}:</span>
<span className="">{orderInfo.creationTime}</span>
</div>
</div>
{/* 订单详情表格 */}
<div className="rb:border rb:border-[#DFE4ED] rb:rounded-2xl rb:overflow-hidden">
{/* 桌面端表头 */}
<div className="rb:flex rb:gap-4 rb:p-4 rb:bg-[rgba(255,255,255,0.03)] rb:border-b rb:border-b-[rgba(255,255,255,0.1)]">
<div className="rb:flex-1">{t('pricing.comboName')}</div>
<div className="rb:flex-2">{t('pricing.spAndTa')}</div>
<div className="rb:flex-2">{t('pricing.versionInformation')}</div>
<div className="rb:w-32">{t('pricing.orderCycle')}</div>
<div className="rb:w-32 rb:text-right">{t('pricing.orderAmount')}</div>
</div>
{/* 表格内容 */}
<div className="rb:flex rb:p-4 rb:flex-row rb:gap-4">
{/* 套餐名称 */}
<div className="rb:flex-1">
<div className="rb:hidden rb:text-[12px] rb:text-[#5B6167] rb:mb-1">{t('pricing.comboName')}</div>
<div className="rb:text-[18px] rb:text-xl rb:font-bold rb:mb-1">{orderInfo.comboName}</div>
<div className="rb:text-[12px] rb:text-[#5B6167]">{orderInfo.comboEdition}</div>
</div>
{/* 解决方案和目标受众 */}
<div className="rb:flex-2 rb:text-[12px] ">
<div className="rb:hidden rb:text-[12px] rb:text-[#5B6167] rb:mb-2">{t('pricing.spAndTa')}</div>
<div className="rb:mb-4">
<div className=" rb:font-medium rb:mb-1">{translations.getPriceKey('solution')}</div>
<div className="rb:text-[#5B6167]">{orderInfo.solutionPositioning}</div>
</div>
<div>
<div className=" rb:font-medium rb:mb-1">{translations.getPriceKey('targetAudience')}</div>
<div className="rb:text-[#5B6167]">{orderInfo.targetAudience}</div>
</div>
</div>
{/* 版本信息 */}
<div className="rb:flex-2 rb:text-[12px] rb:space-y-2">
<div className="rb:hidden rb:text-[12px] rb:text-[#5B6167] rb:mb-2">{t('pricing.versionInformation')}</div>
<div className="rb:flex rb:items-center rb:gap-2">
<img src={checkImg} className="rb:w-3 rb:h-3 rb:size-3" />
<span className="rb:text-[#5B6167]">{translations.memoryCapacity} <span className="">{orderInfo.memoryCapacity}</span></span>
</div>
<div className="rb:flex rb:items-center rb:gap-2">
<img src={checkImg} className="rb:w-3 rb:h-3 rb:size-3" />
<span className="rb:text-[#5B6167]">{translations.intelligentSearchFrequency} <span className="">{orderInfo.searchFrequency}</span></span>
</div>
<div className="rb:flex rb:items-center rb:gap-2">
<img src={checkImg} className="rb:w-3 rb:h-3 rb:size-3" />
<span className="rb:text-[#5B6167]">{translations.supportServices} <span className="">{orderInfo.supportServices}</span></span>
</div>
{selectedType === 'commerce' && (
<>
<div className="rb:flex rb:items-center rb:gap-2">
<img src={checkImg} className="rb:w-3 rb:h-3 rb:size-3" />
<span className="rb:text-[#5B6167]">{translations.flexibleDeployment} <span className="">{translations.getTypeSupportService('commerce', 'flexibleDeployment')}</span></span>
</div>
<div className="rb:flex rb:items-center rb:gap-2">
<img src={checkImg} className="rb:w-3 rb:h-3 rb:size-3" />
<span className="rb:text-[#5B6167]">{translations.reliableGuarantee} <span className="">{translations.getTypeSupportService('commerce', 'reliableGuarantee')}</span></span>
</div>
</>
)}
</div>
{/* 订购周期和金额 */}
<div className="rb:w-32 rb:text-[12px] rb:text-[#5B6167]">
{orderInfo.orderingCycle}
</div>
<div className={`rb:w-32 rb:text-right rb:font-bold rb:text-[20px] rb:text-xl ${selectedType === 'commerce' ? '' : 'rb:text-2xl'}`}>
<span className="rb:text-[#5B6167] rb:font-normal rb:text-[12px] rb:hidden">{t('pricing.orderAmount')}: </span>
{selectedType === 'commerce' ? '' : '$'}{orderInfo.orderAmount}
</div>
</div>
</div>
</div>
{/* 支付方式和支付凭证 */}
<div className="rb:grid rb:grid-cols-2 rb:gap-6">
{/* 支付方式 */}
<div className="rb:border rb:border-[#DFE4ED] rb:rounded-2xl rb:p-4">
<h2 className="rb:text-[16px] rb:text-lg rb:font-semibold rb:mb-4">{t('pricing.paymentMethod')}</h2>
<div className="rb:bg-[rgba(255,255,255,0.12)] rb:rounded-2xl rb:p-3 rb:mb-6">
<div className="rb:flex rb:items-center rb:gap-3">
<img src={corporateImg} className="rb:size-8" />
<div>
<div className="rb:text-[14px] rb:text-base rb:font-medium">{t('pricing.corporateTransfer')}</div>
<div className="rb:text-[12px] rb:text-xs rb:text-[#5B6167]">{t('pricing.corporateTransferDesc')}</div>
</div>
</div>
</div>
<div>
<h3 className="rb:text-[12px] rb:font-medium rb:mb-4">{t('pricing.payeeInformation')}</h3>
<div className="rb:space-y-4 rb:text-[12px] ">
<div>
<div className="rb:text-[#5B6167] rb:mb-1">{t('pricing.receivingEntity')}:</div>
<div className="">{paymentInfo.payee}</div>
</div>
<div>
<div className="rb:text-[#5B6167] rb:mb-1">{t('pricing.bankName')}:</div>
<div className="">{paymentInfo.bankName}</div>
</div>
<div>
<div className="rb:text-[#5B6167] rb:mb-1">{t('pricing.bankAccountNumber')}:</div>
<div className="rb:flex rb:items-center rb:gap-2">
<span className=" rb:break-all">{paymentInfo.bankAccount}</span>
<div
className="rb:w-4 rb:h-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/copy.svg')] rb:hover:bg-[url('@/assets/images/copy_hover.svg')]"
onClick={() => copyText(paymentInfo.bankAccount)}
></div>
</div>
</div>
</div>
</div>
</div>
{/* 支付凭证 */}
<div className="rb:border rb:border-[#DFE4ED] rb:rounded-2xl rb:p-4">
<h2 className="rb:text-[16px] rb:text-lg rb:font-semibold rb:mb-4">{t('pricing.paymentVoucher')}</h2>
<Form
form={form}
layout="vertical"
onFinish={submitPayment}
className="rb:space-y-4"
>
<Form.Item
name="pay_txn_id"
label={t('pricing.pay_txn_id')}
required
extra={t('pricing.pay_txn_idDesc')}
>
<Input placeholder={t('pricing.pay_txn_idPlaceholder')} />
</Form.Item>
<Form.Item
name="payer"
label={t('pricing.payer')}
required
>
<Input placeholder={t('pricing.payerPlaceholder')} />
</Form.Item>
<Form.Item
name="transferDate"
label={t('pricing.transferDate')}
required
>
<DatePicker className="rb:w-full" />
</Form.Item>
<Form.Item
name="remarks"
label={t('pricing.remark')}
>
<TextArea placeholder={t('pricing.remarkPlaceholder')} />
</Form.Item>
<Button type="primary" htmlType="submit" loading={isSubmitting} block>
{t('pricing.confirm')}
</Button>
<p className="rb:text-[12px] rb:text-[#5B6167] rb:text-left">
{t('pricing.payInfo')}<br />
{t('pricing.paySuccess')}
</p>
</Form>
</div>
</div>
</div>
);
};
export default OrderPayment;

View File

@@ -0,0 +1,27 @@
export interface PriceItem {
type: string;
label: string;
typeDesc: string;
priceDescObj: {
solution: string;
targetAudience: string;
};
priceObj: {
type: string;
price?: number;
time: string;
};
btnType: string;
memoryCapacity: string;
intelligentSearchFrequency: string;
mostPopular?: boolean;
flexibleDeployment?: boolean;
reliableGuarantee?: boolean;
}
export interface VoucherForm {
pay_txn_id: string;
payer: string;
transferDate: string;
remarks: string;
}

View File

@@ -0,0 +1,281 @@
import React from 'react';
import { Button } from 'antd';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import personal from '@/assets/images/order/personal.png'
import team from '@/assets/images/order/team.png'
import biz from '@/assets/images/order/biz.png'
import commerce from '@/assets/images/order/commerce.png'
import checkIcon from '@/assets/images/login/checkBg.png'
import alertIcon from '@/assets/images/order/alert.svg';
import { useUser } from '@/store/user'
interface PriceItem {
type: string;
label: string;
typeDesc: string;
priceDescObj: {
solution: string;
targetAudience: string;
};
priceObj: {
type: string;
price?: number;
time: string;
};
btnType: string;
memoryCapacity: string;
intelligentSearchFrequency: string;
mostPopular?: boolean;
flexibleDeployment?: boolean;
reliableGuarantee?: boolean;
}
const btnClassNames = {
personal: 'rb:h-10! rb:rounded-[8px]!',
team: 'rb:h-10! rb:rounded-[8px]! rb:bg-[#FF5D34]! rb:text-white! rb:border-0! rb:hover:border-0! rb:hover:opacity-[0.8]',
biz: 'rb:h-10! rb:rounded-[8px]!',
commerce: '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 PRICE_LIST: PriceItem[] = [
{
type: 'personal',
label: 'pricing.personal.label',
typeDesc: 'pricing.personal.typeDesc',
priceDescObj: {
solution: 'pricing.personal.solution',
targetAudience: 'pricing.personal.targetAudience',
},
priceObj: {
type: 'default',
price: 0,
time: 'pricing.personal.priceDesc',
},
btnType: 'started', // started / choosePlan / contact
memoryCapacity: '2000',
intelligentSearchFrequency: '100',
},
{
type: 'team',
label: 'pricing.team.label',
typeDesc: 'pricing.team.typeDesc',
priceDescObj: {
solution: 'pricing.team.solution',
targetAudience: 'pricing.team.targetAudience',
},
priceObj: {
type: 'default',
price: 19,
time: 'pricing.team.priceDesc'
},
btnType: 'choosePlan', // started / choosePlan / contact
memoryCapacity: '20,000',
intelligentSearchFrequency: '10,000',
},
{
type: 'biz',
label: 'pricing.biz.label',
typeDesc: 'pricing.biz.typeDesc',
priceDescObj: {
solution: 'pricing.biz.solution',
targetAudience: 'pricing.biz.targetAudience',
},
mostPopular: true,
priceObj: {
type: 'default', // default / biz
price: 299,
time: 'pricing.biz.priceDesc'
},
btnType: 'choosePlan', // started / choosePlan / contact
memoryCapacity: '100,000',
intelligentSearchFrequency: '50,000',
},
{
type: 'commerce',
label: 'pricing.commerce.label',
typeDesc: 'pricing.commerce.typeDesc',
priceDescObj: {
solution: 'pricing.commerce.solution',
targetAudience: 'pricing.commerce.targetAudience',
},
priceObj: {
type: 'commerce', // default / commerce
time: 'pricing.commerce.priceDesc'
},
btnType: 'contact', // started / choosePlan / contact
memoryCapacity: '20,000',
intelligentSearchFrequency: '10,000',
flexibleDeployment: true,
reliableGuarantee: true
},
]
const PricingView: React.FC = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const { user } = useUser();
const handleChoosePlan = (type: string) => {
switch(type) {
case 'team':
case 'biz':
navigate(`/order-pay?type=${type}`);
break
case 'personal':
navigate(user.current_workspace_id ? '/' : '/space');
break
case 'commerce':
break
}
};
const goToHistory = () => {
navigate('/orders');
}
const getCardIcon = (type: string) => {
const iconMap: Record<string, string> = {
personal: personal,
team: team,
biz: biz,
commerce: commerce
};
return iconMap[type] || commerce;
};
return (
<div className="rb:h-[calc(100vh-88px)] rb:overflow-y-auto rb:w-full rb:p-3">
{/* <div className="rb:p-[20px_24px] rb:flex rb:items-center rb:justify-end rb:bg-[url(@/assets/images/order/bg.png)] rb:h-25 rb:rounded-xl rb:mb-6 rb:bg-cover rb:bg-no-repeat rb:bg-center rb:mb-[20px rb:w-full">
<div className="rb:flex rb:items-center">
<img src={getCardIcon('personal')} className="rb:size-15 rb:mr-3.5 rb:shrink-0" />
<div className="rb:text-white rb:text-[24px] rb:font-semibold rb:leading-8">
{t(`pricing.${'personal'}.type`)}
<div className="rb:mt-1 rb:leading-5 rb:text-[14px] rb:font-regular">
{t('pricing.currentAccountType')} | {t(`pricing.validUntil`)} <span className="rb:font-medium">December 31, 2024</span>
</div>
</div>
</div>
<Button className="rb:group rb:text-[#212332] rb:font-medium!" onClick={goToHistory}>
<div
className="rb:size-4 rb:bg-cover rb:bg-[url('@/assets/images/order/order.svg')] rb:group-hover:bg-[url('@/assets/images/order/order_hover.svg')]"
></div>
{t('pricing.orderHistory')}
</Button>
</div> */}
<div className="rb:flex rb:items-center rb:justify-end rb:rounded-xl rb:mb-6 rb:w-full">
<Button className="rb:group rb:text-[#212332] rb:font-medium!" onClick={goToHistory}>
<div
className="rb:size-4 rb:bg-cover rb:bg-[url('@/assets/images/order/order.svg')] rb:group-hover:bg-[url('@/assets/images/order/order_hover.svg')]"
></div>
{t('pricing.orderHistory')}
</Button>
</div>
<div className="rb:grid rb:grid-cols-4 rb:gap-6">
{PRICE_LIST.map((item) => (
<div
key={item.type}
className={`rb:relative rb:bg-[#FBFDFF] rb:rounded-xl rb:border rb:border-[#DFE4ED] rb:px-5 rb:py-6 rb:shadow-sm rb:transition-all rb:duration-200 hover:rb:shadow-lg ${
item.mostPopular ? 'rb:-top-3' : ''
}`}
>
{item.mostPopular && (
<div className="rb:absolute rb:right-0 rb:top-0 rb:bg-[#FF5D34] rb:rounded-[0px_12px_0px_12px] rb:text-[12px] rb:text-white rb:font-regular rb:leading-4 rb:p-[4px_8px]">
{t('pricing.mostPopular')}
</div>
)}
{/* Icon */}
<img src={getCardIcon(item.type)} className="rb:size-15 rb:mb-6" />
{/* Title */}
<h3 className="rb:text-[28px] rb:font-extrabold rb:mb-2">
{t(`pricing.${item.type}.type`)}
</h3>
{/* Description */}
<p className="rb:text-[#5B6167] rb:mb-8">
{t(item.typeDesc)}
</p>
{/* Price */}
<div className="rb:mb-5">
{typeof item.priceObj.price !== 'undefined' ? (
<div className="rb:flex rb:items-baseline">
<span className="rb:text-[16px] rb:text-[#5B6167] rb:font-regular rb:mr-1 rb:mb-1">¥</span>
<span className="rb:text-[40px] rb:font-extrabold">
{item.priceObj.price.toLocaleString()}
</span>
<span className="rb:text-[16px] rb:text-[#5B6167] rb:ml-1 rb:mb-1">
{t(item.priceObj.time)}
</span>
</div>
) : (
<div className="rb:text-2xl rb:font-bold rb:text-gray-900">
{t(item.priceObj.time)}
</div>
)}
</div>
{/* CTA Button */}
<Button
type={item.type === 'biz' ? 'primary' : 'default'}
block
className={btnClassNames[item.type as keyof typeof btnClassNames]}
onClick={() => handleChoosePlan(item.type)}
>
{item.btnType === 'started' ? t('pricing.startedBtn') : item.btnType === 'choosePlan' ? t('pricing.choosePlanBtn') : t('pricing.contactBtn')}
</Button>
{/* Features */}
<div className="rb:space-y-3 rb:border-t rb:border-t-[#DFE4ED] rb:mt-6 rb:pt-6">
<div className="rb:flex rb:mb-2">
<img src={checkIcon} className="rb:w-4 rb:h-4 rb:mr-1 rb:mt-0.5" />
<div className="rb:font-regular rb:text-[12px] rb:text-[#5B6167] rb:leading-5">
{t('pricing.memoryCapacity')} { item.memoryCapacity } {t('pricing.entries')}
</div>
</div>
<div className="rb:flex rb:mb-2">
<img src={checkIcon} className="rb:w-4 rb:h-4 rb:mr-1 rb:mt-0.5" />
<div className="rb:font-regular rb:text-[12px] rb:text-[#5B6167] rb:leading-5">
{t('pricing.intelligentSearchFrequency')}<span className="rb:text-[#FFFFFF]">{ item.intelligentSearchFrequency } {t('pricing.timesMonth')}</span>
</div>
</div>
{['supportServices', 'flexibleDeployment', 'reliableGuarantee'].map(type => {
if ((item as any)[type] || type === 'supportServices') {
return (
<div key={type} className="rb:flex rb:mb-2">
<img src={checkIcon} className="rb:w-4 rb:h-4 rb:mr-1 rb:mt-0.5" />
<div className="rb:font-regular rb:text-[12px] rb:text-[#5B6167] rb:leading-5">
{t(`pricing.${type}`)}{t(`pricing.${item.type}.${type}`)}
</div>
</div>
)
}
return null
})}
</div>
</div>
))}
</div>
{/* Warning Notice */}
<div className="rb:mt-6 rb:bg-[rgba(255,93,52,0.06)] rb:border rb:border-[rgba(255,93,52,0.25)] rb:rounded-lg rb:p-4">
<div className="rb:flex rb:items-start rb:gap-2">
<img src={alertIcon} className="rb:w-5 rb:h-5 rb:shrink-0" />
<div>
<h4 className="rb:text-sm rb:font-medium rb:text-[#FF5D34] rb:mb-1">
{t('pricing.alertTitle')}
</h4>
<p className="rb:mt-2 rb:font-regular rb:text-[12px] rb:leading-4.25 rb:text-[#5B6167]">
{t('pricing.alertContent')}
</p>
</div>
</div>
</div>
</div>
);
};
export default PricingView;

View File

@@ -10,6 +10,7 @@ import type { ConfigForm, Result, ReflexionData, MemoryVerify, QualityAssessment
import CustomSelect from '@/components/CustomSelect';
import { getModelListUrl } from '@/api/models'
import Tag from '@/components/Tag'
import { useI18n } from '@/store/locale';
const configList = [
// 启用反思引擎
@@ -78,6 +79,7 @@ const SelfReflectionEngine: React.FC = () => {
const [loading, setLoading] = useState(false)
const [runLoading, setRunLoading] = useState(false)
const [result, setResult] = useState<Result | null>(null)
const { language } = useI18n()
const values = Form.useWatch([], form);
@@ -135,7 +137,7 @@ const SelfReflectionEngine: React.FC = () => {
.then(() => {
pilotRunMemoryReflectionConfig({
config_id: id,
dialogue_text: t('reflectionEngine.exampleText')
language_type: language
})
.then((res) => {
setResult(res as Result)

View File

@@ -41,15 +41,15 @@ const ConfigModal = forwardRef<ConfigModalRef>((_props, ref) => {
.validateFields()
.then(() => {
setLoading(true)
// updateWorkspaceModels(values as ConfigModalData)
// .then(() => {
// setLoading(false)
// handleClose()
// message.success(t('common.createSuccess'))
// })
// .catch(() => {
// setLoading(false)
// });
updateWorkspaceModels(values)
.then(() => {
setLoading(false)
handleClose()
message.success(t('common.updateSuccess'))
})
.catch(() => {
setLoading(false)
});
handleClose()
})

View File

@@ -1,7 +1,7 @@
import { type FC, useEffect, useState } from 'react'
import { type FC, useEffect, useState, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import clsx from 'clsx'
import { Row, Col, Skeleton } from 'antd'
import { Row, Col, Skeleton, Flex, Button } from 'antd'
import { useParams } from 'react-router-dom'
import aboutUs from '@/assets/images/userMemory/aboutUs.svg'
import down from '@/assets/images/userMemory/down.svg'
@@ -10,7 +10,9 @@ import PieCard from './components/PieCard'
import RbCard from '@/components/RbCard/Card'
import {
getUserSummary,
analyticsRefresh
} from '@/api/memory'
import type { MemoryInsightRef } from './types'
import RelationshipNetwork from './components/RelationshipNetwork'
import MemoryInsight from './components/MemoryInsight'
import Empty from '@/components/Empty'
@@ -45,10 +47,12 @@ const Title: FC<TitleProps> = ({ type, title, icon, t, expanded, onClick }) => (
const Neo4j: FC = () => {
const { t } = useTranslation()
const { id } = useParams()
const memoryInsightRef = useRef<MemoryInsightRef>(null)
const [expanded, setExpanded] = useState<string[]>(['aboutUs', 'interestDistribution', 'importantRelationships', 'importantMomentsInLife'])
const [summary, setSummary] = useState<string | null>(null)
const [loading, setLoading] = useState<Record<string, boolean>>({
summary: false,
refresh: false
})
useEffect(() => {
@@ -70,73 +74,96 @@ const Neo4j: FC = () => {
setLoading(prev => ({ ...prev, summary: false }))
})
}
const handleRefresh = () => {
setLoading(prev => ({ ...prev, refresh: true }))
analyticsRefresh(id as string)
.then(res => {
const response = res as { insight_success: boolean; summary_success: boolean; }
if (response.insight_success) {
memoryInsightRef.current?.getInsightReport()
}
if (response.summary_success) {
getSummary()
}
})
.finally(() => {
setLoading(prev => ({ ...prev, refresh: false }))
})
}
return (
<Row gutter={[16, 16]} className="rb:pb-6">
<Col span={8}>
<Row gutter={[16, 16]}>
<Col span={24}>
<EndUserProfile />
</Col>
<Col span={24}>
<RbCard>
{/* 关于我 */}
<>
<Title
type="aboutUs"
title={t('userMemory.aboutMe')}
icon={aboutUs}
t={t}
expanded={expanded.includes('aboutUs')}
onClick={handleTitleClick}
/>
{expanded.includes('aboutUs') && (
<>
{loading.summary
? <Skeleton className="rb:mt-4" />
: summary
? <div className="rb:font-regular rb:leading-5.5 rb:pt-4">
{summary || '-'}
</div>
: <Empty size={88} className="rb:mt-12 rb:mb-20.25" />
}
</>
)}
</>
<div>
<Flex justify="flex-end">
<Button type="primary" loading={loading.refresh} className="rb:mb-3" onClick={handleRefresh}>
{t('common.refresh')}
</Button>
</Flex>
<Row gutter={[16, 16]} className="rb:pb-6">
<Col span={8}>
<Row gutter={[16, 16]}>
<Col span={24}>
<EndUserProfile />
</Col>
<Col span={24}>
<RbCard>
{/* 关于我 */}
<>
<Title
type="aboutUs"
title={t('userMemory.aboutMe')}
icon={aboutUs}
t={t}
expanded={expanded.includes('aboutUs')}
onClick={handleTitleClick}
/>
{expanded.includes('aboutUs') && (
<>
{loading.summary
? <Skeleton className="rb:mt-4" />
: summary
? <div className="rb:font-regular rb:leading-5.5 rb:pt-4">
{summary || '-'}
</div>
: <Empty size={88} className="rb:mt-12 rb:mb-20.25" />
}
</>
)}
</>
{/* 兴趣分布 */}
<>
<Title
type="interestDistribution"
title={t('userMemory.interestDistribution')}
icon={interestDistribution}
t={t}
expanded={expanded.includes('interestDistribution')}
onClick={handleTitleClick}
/>
{/* 兴趣分布 */}
<>
<Title
type="interestDistribution"
title={t('userMemory.interestDistribution')}
icon={interestDistribution}
t={t}
expanded={expanded.includes('interestDistribution')}
onClick={handleTitleClick}
/>
{expanded.includes('interestDistribution') && (
<PieCard />
)}
</>
</RbCard>
</Col>
{expanded.includes('interestDistribution') && (
<PieCard />
)}
</>
</RbCard>
</Col>
</Row>
</Col>
<Col span={16}>
<Row gutter={[16, 16]}>
<Col span={24}>
<NodeStatistics />
</Col>
{/* 记忆洞察 */}
<Col span={24}>
<MemoryInsight ref={memoryInsightRef} />
</Col>
{/* 关系网络 + 记忆详情 */}
<RelationshipNetwork />
</Row>
</Col>
</Row>
</Col>
<Col span={16}>
<Row gutter={[16, 16]}>
<Col span={24}>
<NodeStatistics />
</Col>
{/* 记忆洞察 */}
<Col span={24}>
<MemoryInsight />
</Col>
{/* 关系网络 + 记忆详情 */}
<RelationshipNetwork />
</Row>
</Col>
</Row>
</div>
)
}
export default Neo4j

View File

@@ -1,4 +1,4 @@
import { type FC, useEffect, useState } from 'react'
import { type FC, useEffect, useState, forwardRef, useImperativeHandle } from 'react'
import { useTranslation } from 'react-i18next'
import { useParams } from 'react-router-dom'
import { Skeleton } from 'antd';
@@ -7,8 +7,9 @@ import Empty from '@/components/Empty';
import {
getMemoryInsightReport,
} from '@/api/memory'
import type { MemoryInsightRef } from '../types'
const MemoryInsight:FC = () => {
const MemoryInsight = forwardRef<MemoryInsightRef>((_props, ref) => {
const { t } = useTranslation()
const { id } = useParams()
const [loading, setLoading] = useState<boolean>(false)
@@ -31,6 +32,10 @@ const MemoryInsight:FC = () => {
setLoading(false)
})
}
// 暴露给父组件的方法
useImperativeHandle(ref, () => ({
getInsightReport,
}));
return (
<RbCard
title={t('userMemory.memoryInsight')}
@@ -51,5 +56,5 @@ const MemoryInsight:FC = () => {
}
</RbCard>
)
}
})
export default MemoryInsight

View File

@@ -29,9 +29,11 @@ const NodeStatistics: FC = () => {
if (!id) return
setLoading(true)
getNodeStatistics(id).then((res) => {
const response = res as { nodes: NodeStatisticsItem[], total: number }
setData(response.nodes)
setTotal(response.total)
const response = res as NodeStatisticsItem[]
setData(response)
// 计算count总计
const totalCount = response.reduce((sum, item) => sum + (item.count || 0), 0)
setTotal(totalCount)
setLoading(false)
})
.finally(() => {
@@ -40,7 +42,7 @@ const NodeStatistics: FC = () => {
}
const handleViewDetail = (type: string) => {
switch (type) {
case 'Statement':
case 'EMOTIONAL_MEMORY':
navigate(`/statement/${id}`)
break
}
@@ -56,19 +58,19 @@ const NodeStatistics: FC = () => {
>
{loading
? <Skeleton />
: data.length > 0
? <div className={`rb:w-full rb:grid rb:grid-cols-${data.length} rb:gap-2`}>
: data && data.length > 0
? <div className={`rb:w-full rb:grid rb:grid-cols-3 rb:gap-2`}>
{data.map(vo => (
<div
key={vo.type}
className={clsx("rb:group rb:border rb:border-[#DFE4ED] rb:p-0 rb:rounded-xl rb:hover:shadow-[0px_2px_4px_0px_rgba(0,0,0,0.15)]", {
'rb:cursor-pointer': vo.type === 'Statement'
'rb:cursor-pointer': vo.type === 'EMOTIONAL_MEMORY'
})}
onClick={() => handleViewDetail(vo.type)}
>
<div className="rb:gap-0.5 rb:p-3 rb:leading-4 rb:text-[#5B6167] rb:flex rb:items-center rb:justify-between rb:border-b rb:border-[#DFE4ED]">
<div className="rb:wrap-break-word rb:line-clamp-1">{t(`userMemory.${vo.type}`)}</div>
{vo.type === 'Statement' && <div
{vo.type === 'EMOTIONAL_MEMORY' && <div
className="rb:w-3 rb:h-3 rb:-ml-0.75 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/home/arrow_top_right.svg')] rb:group-hover:bg-[url('@/assets/images/home/arrow_top_right_hover.svg')]"
></div>}
</div>

View File

@@ -130,4 +130,7 @@ export interface EndUser {
}
export interface EndUserProfileModalRef {
handleOpen: (vo: EndUser) => void;
}
export interface MemoryInsightRef {
getInsightReport: () => void
}