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:
76
web/src/views/OrderHistory/components/OrderDetail.tsx
Normal file
76
web/src/views/OrderHistory/components/OrderDetail.tsx
Normal 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;
|
||||
227
web/src/views/OrderHistory/index.tsx
Normal file
227
web/src/views/OrderHistory/index.tsx
Normal 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;
|
||||
43
web/src/views/OrderHistory/types.ts
Normal file
43
web/src/views/OrderHistory/types.ts
Normal 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;
|
||||
}
|
||||
391
web/src/views/OrderPayment/index.tsx
Normal file
391
web/src/views/OrderPayment/index.tsx
Normal 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;
|
||||
27
web/src/views/OrderPayment/types.ts
Normal file
27
web/src/views/OrderPayment/types.ts
Normal 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;
|
||||
}
|
||||
281
web/src/views/Pricing/index.tsx
Normal file
281
web/src/views/Pricing/index.tsx
Normal 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;
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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>
|
||||
|
||||
@@ -130,4 +130,7 @@ export interface EndUser {
|
||||
}
|
||||
export interface EndUserProfileModalRef {
|
||||
handleOpen: (vo: EndUser) => void;
|
||||
}
|
||||
export interface MemoryInsightRef {
|
||||
getInsightReport: () => void
|
||||
}
|
||||
Reference in New Issue
Block a user