feat(web): Home page ui upgrade
This commit is contained in:
77
web/src/views/Home/components/ApiLineCard.tsx
Normal file
77
web/src/views/Home/components/ApiLineCard.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 17:17:05
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-10 11:35:52
|
||||
*/
|
||||
/**
|
||||
* Line Chart Card Component
|
||||
* Displays time-series data with ECharts line chart
|
||||
* Supports multiple series and date range selection
|
||||
*/
|
||||
|
||||
import { type FC, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Select } from 'antd'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
import Card from './Card'
|
||||
import { getWorkspaceApiStatistics } from '@/api/application'
|
||||
import LineChart, { type ChartData } from '@/components/Charts/LineChart'
|
||||
|
||||
const seriesList = ['total_calls', 'app_calls', 'service_calls']
|
||||
const ApiLineCard: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const options = [
|
||||
{ label: t('dashboard.lastDays', { days: 7 }), value: 7 },
|
||||
{ label: t('dashboard.lastDays', { days: 30 }), value: 30 },
|
||||
{ label: t('dashboard.lastDays', { days: 90 }), value: 90 },
|
||||
{ label: t('dashboard.lastHalfYear'), value: 180 },
|
||||
{ label: t('dashboard.lastYear'), value: 365 },
|
||||
]
|
||||
const [chartData, setChartData] = useState<ChartData[]>([])
|
||||
const [query, setQuery] = useState(7)
|
||||
|
||||
useEffect(() => {
|
||||
getWorkspaceApiStatistics({
|
||||
start_date: dayjs().subtract(query - 1, 'd').startOf('d').valueOf(),
|
||||
end_date: dayjs().endOf('d').valueOf(),
|
||||
})
|
||||
.then(res => {
|
||||
setChartData(res as ChartData[])
|
||||
})
|
||||
}, [query])
|
||||
|
||||
/** Format series list for legend */
|
||||
const formatSeriesList = () => {
|
||||
const list: Record<string, string> = {}
|
||||
seriesList.forEach(key => {
|
||||
list[key] = t(`dashboard.${key}`)
|
||||
})
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={t(`dashboard.apiCallTrends`)}
|
||||
headerOperate={
|
||||
<Select
|
||||
value={query}
|
||||
options={options}
|
||||
onChange={(value) => setQuery(value)}
|
||||
className="rb:w-35!"
|
||||
/>
|
||||
}
|
||||
className={`rb:pb-6`}
|
||||
>
|
||||
<LineChart
|
||||
chartData={chartData}
|
||||
seriesList={formatSeriesList()}
|
||||
height={239}
|
||||
/>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default ApiLineCard
|
||||
@@ -1,8 +1,8 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 17:27:28
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 17:27:28
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-26 11:16:09
|
||||
*/
|
||||
/**
|
||||
* Card Component
|
||||
@@ -21,15 +21,19 @@ interface CardProps {
|
||||
title: string;
|
||||
headerOperate?: ReactNode;
|
||||
className?: string;
|
||||
bodyClassName?: string;
|
||||
}
|
||||
|
||||
const Card: FC<CardProps> = ({ children, title, headerOperate, className }) => {
|
||||
const Card: FC<CardProps> = ({ children, title, headerOperate, className, bodyClassName }) => {
|
||||
return (
|
||||
<RbCard
|
||||
headerType="borderless"
|
||||
title={title}
|
||||
extra={headerOperate}
|
||||
className={`rb:h-full! ${className}`}
|
||||
variant="borderless"
|
||||
className={`rb:h-full! rb:bg-[#FFFFFF]! ${className}`}
|
||||
bodyClassName={bodyClassName}
|
||||
headerClassName="rb:min-h-[58px]!"
|
||||
>
|
||||
{children}
|
||||
</RbCard>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 17:17:05
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 17:18:32
|
||||
* @Last Modified time: 2026-02-10 11:59:10
|
||||
*/
|
||||
/**
|
||||
* Line Chart Card Component
|
||||
@@ -10,21 +10,18 @@
|
||||
* Supports multiple series and date range selection
|
||||
*/
|
||||
|
||||
import { type FC, useRef } from 'react'
|
||||
import { type FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Select } from 'antd'
|
||||
import ReactEcharts from 'echarts-for-react';
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
import { formatDateTime } from '@/utils/format';
|
||||
import Empty from '@/components/Empty'
|
||||
import Card from './Card'
|
||||
import AreaLineChart, { type ChartData } from '@/components/Charts/AreaLineChart';
|
||||
|
||||
/**
|
||||
* Component props
|
||||
*/
|
||||
interface LineCardProps {
|
||||
chartData: Array<Record<string, string | number>>;
|
||||
chartData: ChartData[];
|
||||
limit: number;
|
||||
onChange: (value: string, type: string) => void;
|
||||
type: string;
|
||||
@@ -32,30 +29,8 @@ interface LineCardProps {
|
||||
seriesList: string[];
|
||||
}
|
||||
|
||||
/** ECharts series configuration */
|
||||
const SeriesConfig = {
|
||||
type: 'line',
|
||||
stack: 'Total',
|
||||
smooth: true,
|
||||
lineStyle: {
|
||||
width: 3
|
||||
},
|
||||
showSymbol: false,
|
||||
label: {
|
||||
show: true,
|
||||
position: 'top'
|
||||
},
|
||||
emphasis: {
|
||||
focus: 'series'
|
||||
},
|
||||
data: [220, 302, 181, 234, 210, 290, 150]
|
||||
}
|
||||
/** Chart color palette */
|
||||
const Colors = ['#FFB048', '#4DA8FF', '#155EEF']
|
||||
|
||||
const LineCard: FC<LineCardProps> = ({ chartData, limit, onChange, type, className, seriesList }) => {
|
||||
const { t } = useTranslation()
|
||||
const chartRef = useRef<ReactEcharts>(null);
|
||||
const options = [
|
||||
{ label: t('dashboard.lastDays', { days: 7 }), value: 7 },
|
||||
{ label: t('dashboard.lastDays', { days: 30 }), value: 30 },
|
||||
@@ -63,39 +38,15 @@ const LineCard: FC<LineCardProps> = ({ chartData, limit, onChange, type, classNa
|
||||
{ label: t('dashboard.lastHalfYear'), value: 180 },
|
||||
{ label: t('dashboard.lastYear'), value: 365 },
|
||||
]
|
||||
|
||||
/** Generate series data with gradient colors */
|
||||
const getSeries = () => {
|
||||
const list = seriesList.map((key, index) => {
|
||||
return {
|
||||
...SeriesConfig,
|
||||
name: t(`dashboard.${key}`),
|
||||
data: chartData.map(vo => vo[key]),
|
||||
areaStyle: {
|
||||
opacity: 0.8,
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{
|
||||
offset: 0,
|
||||
color: Colors[index]
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: '#FFFFFF'
|
||||
}
|
||||
])
|
||||
},
|
||||
}
|
||||
/** Format series list for legend */
|
||||
const formatSeriesList = () => {
|
||||
const list: Record<string, string> = {}
|
||||
seriesList.forEach(key => {
|
||||
list[key] = t(`dashboard.${key}`)
|
||||
})
|
||||
|
||||
return list
|
||||
}
|
||||
/** Format series list for legend */
|
||||
const formatSeriesList = () => {
|
||||
return seriesList.map(key => ({
|
||||
...SeriesConfig,
|
||||
name: t(`dashboard.${key}`),
|
||||
}))
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
@@ -104,79 +55,18 @@ const LineCard: FC<LineCardProps> = ({ chartData, limit, onChange, type, classNa
|
||||
<Select
|
||||
value={limit}
|
||||
options={options}
|
||||
onChange={(value) => onChange(String(value), type)}
|
||||
style={{ width: '150px' }}
|
||||
onChange={(value) => onChange(String(value), type)}
|
||||
className="rb:w-35!"
|
||||
/>
|
||||
}
|
||||
className={`rb:pb-6 ${className}`}
|
||||
>
|
||||
{chartData && chartData.length > 0 ? (
|
||||
<ReactEcharts
|
||||
ref={chartRef}
|
||||
option={{
|
||||
color: Colors,
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
extraCssText: 'box-shadow: 0px 2px 6px 0px rgba(33,35,50,0.16); border-radius: 8px;',
|
||||
axisPointer: {
|
||||
type: 'line',
|
||||
crossStyle: {
|
||||
color: '#5F6266',
|
||||
},
|
||||
lineStyle: {
|
||||
color: '#5F6266',
|
||||
},
|
||||
label: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
data: formatSeriesList(),
|
||||
textStyle: {
|
||||
color: '#5B6167',
|
||||
fontFamily: 'PingFangSC, PingFang SC',
|
||||
lineHeight: 16,
|
||||
},
|
||||
itemGap: 32,
|
||||
padding: 0,
|
||||
itemWidth: 26,
|
||||
itemHeight: 10,
|
||||
left: 'center'
|
||||
},
|
||||
grid: {
|
||||
left: 4,
|
||||
right: '3%',
|
||||
bottom: 0,
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: chartData.map(item => formatDateTime(item.created_at, 'DD/MM')),
|
||||
boundaryGap: false,
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
color: '#A8A9AA',
|
||||
fontFamily: 'PingFangSC, PingFang SC',
|
||||
align: 'right',
|
||||
lineHeight: 17,
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#EBEBEB',
|
||||
}
|
||||
},
|
||||
},
|
||||
series: getSeries()
|
||||
}}
|
||||
style={{ height: '265px', width: '100%', minWidth: '100%', boxSizing: 'border-box' }}
|
||||
opts={{ renderer: 'canvas' }}
|
||||
notMerge={true}
|
||||
lazyUpdate={true}
|
||||
/>
|
||||
) : <Empty size={120} className="rb:mt-12 rb:mb-20.25" />}
|
||||
<AreaLineChart
|
||||
xAxisKey="date"
|
||||
chartData={chartData}
|
||||
seriesList={formatSeriesList()}
|
||||
height={239}
|
||||
/>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,137 +1,39 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 17:16:45
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 17:16:45
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-10 11:57:35
|
||||
*/
|
||||
/**
|
||||
* Pie Chart Card Component
|
||||
* Displays knowledge base type distribution with ECharts donut chart
|
||||
*/
|
||||
|
||||
import { type FC, useRef, useEffect } from 'react'
|
||||
import { type FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ReactEcharts from 'echarts-for-react';
|
||||
|
||||
import Card from './Card'
|
||||
import Loading from '@/components/Empty/Loading'
|
||||
import Empty from '@/components/Empty'
|
||||
import PieChart, { type ChartData } from '@/components/Charts/PieChart'
|
||||
|
||||
/**
|
||||
* Component props
|
||||
*/
|
||||
interface PieCardProps {
|
||||
chartData: Array<Record<string, string | number>>;
|
||||
chartData: ChartData[];
|
||||
loading: boolean;
|
||||
}
|
||||
/** Chart color palette */
|
||||
const Colors = ['#155EEF', '#31E8FF', '#AD88FF', '#FFB048', '#4DA8FF', '#03BDFF']
|
||||
|
||||
const PieCard: FC<PieCardProps> = ({ chartData, loading }) => {
|
||||
const { t } = useTranslation()
|
||||
const chartRef = useRef<ReactEcharts>(null);
|
||||
const resizeScheduledRef = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
if (chartRef.current && !resizeScheduledRef.current) {
|
||||
resizeScheduledRef.current = true
|
||||
requestAnimationFrame(() => {
|
||||
chartRef.current?.getEchartsInstance().resize();
|
||||
resizeScheduledRef.current = false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const resizeObserver = new ResizeObserver(handleResize)
|
||||
const chartElement = chartRef.current?.getEchartsInstance().getDom().parentElement
|
||||
if (chartElement) {
|
||||
resizeObserver.observe(chartElement)
|
||||
}
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect()
|
||||
}
|
||||
}, [chartData])
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={t('dashboard.knowledgeBaseTypeDistribution')}
|
||||
>
|
||||
{loading
|
||||
? <Loading size={249} />
|
||||
: !chartData || chartData.length === 0
|
||||
? <Empty size={120} className="rb:mt-12 rb:mb-20.25" />
|
||||
: <ReactEcharts
|
||||
option={{
|
||||
color: Colors,
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
textStyle: {
|
||||
color: '#5B6167',
|
||||
fontSize: 12,
|
||||
width: 27,
|
||||
height: 16,
|
||||
},
|
||||
formatter: '{d}%',
|
||||
padding: [8, 5],
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderColor: '#DFE4ED',
|
||||
extraCssText: 'width: 36px; height: 36px; box-shadow: 0px 2px 4px 0px rgba(33,35,50,0.12);border-radius: 36px;'
|
||||
},
|
||||
legend: {
|
||||
right: 20 ,
|
||||
top: 'middle',
|
||||
padding: 0,
|
||||
itemWidth: 12,
|
||||
itemHeight: 12,
|
||||
borderRadius: 2,
|
||||
orient: 'vertical',
|
||||
textStyle: {
|
||||
color: '#5B6167',
|
||||
fontFamily: 'PingFangSC, PingFang SC',
|
||||
lineHeight: 16,
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: 'Access From',
|
||||
type: 'pie',
|
||||
radius: ['60%', '100%'],
|
||||
avoidLabelOverlap: false,
|
||||
percentPrecision: 0,
|
||||
padAngle: 4,
|
||||
width: 200,
|
||||
height: 200,
|
||||
left: '10%',
|
||||
top: 'middle',
|
||||
itemStyle: {
|
||||
borderRadius: 0
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center'
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
color: '#212332',
|
||||
formatter: '{d}%\n{b}',
|
||||
}
|
||||
},
|
||||
labelLine: {
|
||||
show: false
|
||||
},
|
||||
data: chartData
|
||||
}
|
||||
]
|
||||
}}
|
||||
style={{ height: '265px', width: '100%', minWidth: '400px' }}
|
||||
notMerge={true}
|
||||
lazyUpdate={true}
|
||||
/>
|
||||
? <Loading size={249} />
|
||||
: <PieChart chartData={chartData} />
|
||||
}
|
||||
</Card>
|
||||
)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 17:16:38
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 17:16:38
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-11 14:57:35
|
||||
*/
|
||||
/**
|
||||
* Quick Operation Component
|
||||
@@ -11,15 +11,16 @@
|
||||
*/
|
||||
|
||||
import { type FC } from 'react'
|
||||
import clsx from 'clsx';
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Flex } from 'antd';
|
||||
|
||||
import Card from './Card';
|
||||
import applicationIcon from '@/assets/images/menu/application_active.svg';
|
||||
import knowledgeIcon from '@/assets/images/menu/knowledge_active.svg';
|
||||
import memoryConversationIcon from '@/assets/images/menu/memoryConversation_active.svg';
|
||||
import applicationIcon from '@/assets/images/home/application.svg';
|
||||
import knowledgeIcon from '@/assets/images/home/knowledge.svg';
|
||||
import memoryConversationIcon from '@/assets/images/home/memoryConversation.svg';
|
||||
import helpCenterIcon from '@/assets/images/menu/helpCenter_active.svg'
|
||||
import arrowTopRight from '@/assets/images/home/arrow_top_right.svg';
|
||||
|
||||
/** Quick operation items configuration */
|
||||
const quickOperations = [
|
||||
@@ -29,6 +30,13 @@ const quickOperations = [
|
||||
{ key: 'helpCenter', url: '' },
|
||||
]
|
||||
|
||||
const bgStyleList = [
|
||||
'rb:bg-[rgba(21,94,239,0.1)]',
|
||||
'rb:bg-[rgba(156,111,255,0.1)]',
|
||||
'rb:bg-[rgba(255,176,72,0.1)]',
|
||||
'rb:bg-[rgba(77,168,255,0.1)]'
|
||||
]
|
||||
|
||||
/** Icon mapping for quick operations */
|
||||
const quickOperationIcons: {[key: string]: string | undefined} = {
|
||||
createNewApplication: applicationIcon,
|
||||
@@ -62,17 +70,19 @@ const QuickOperation:FC = () => {
|
||||
return (
|
||||
<Card
|
||||
title={t('dashboard.quickOperation')}
|
||||
bodyClassName="rb:pt-0! rb:pb-[14px]! rb:px-4!"
|
||||
>
|
||||
<div className="rb:grid rb:grid-cols-4 rb:gap-4">
|
||||
{quickOperations.map(item => (
|
||||
<div key={item.key} className="rb:rounded-lg rb:p-[20px_16px] rb:border rb:border-[#DFE4ED] rb:cursor-pointer rb:hover:border-[#155EEF]" onClick={() => handleJump(item.url)}>
|
||||
<div className="rb:flex rb:justify-between">
|
||||
<img className="rb:w-8 rb:h-8" src={quickOperationIcons[item.key]} />
|
||||
<img className="rb:w-4 rb:h-4" src={arrowTopRight} />
|
||||
<div className="rb:grid rb:grid-cols-1 rb:gap-3">
|
||||
{quickOperations.map((item, index) => (
|
||||
<Flex key={item.key} align="center" gap={20} className={clsx("rb:relative rb:rounded-xl rb:py-2! rb:px-3! rb:cursor-pointer", bgStyleList[index])} onClick={() => handleJump(item.url)}>
|
||||
<div className="rb:size-8 rb:rounded-lg rb:p-1 rb:bg-[#FFFFFF]">
|
||||
<img className="rb:size-6" src={quickOperationIcons[item.key]} />
|
||||
</div>
|
||||
<div className="rb:mt-6 rb:text-[#212332] rb:text-[16px] rb:leading-5 rb:font-medium">{t(`dashboard.${item.key}`)}</div>
|
||||
<div className="rb:mt-2 rb:text-[#5B6167] rb:text-[12px] rb:font-regular">{t(`dashboard.${item.key}Desc`)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="rb:text-[14px] rb:leading-5 rb:font-medium">{t(`dashboard.${item.key}`)}</div>
|
||||
<div className="rb:mt-0.5 rb:text-[#5B6167] rb:text-[12px] rb:font-regular">{t(`dashboard.${item.key}Desc`)}</div>
|
||||
</div>
|
||||
</Flex>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 17:15:33
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 17:15:33
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-11 14:48:31
|
||||
*/
|
||||
/**
|
||||
* Recent Activity Component
|
||||
@@ -13,7 +13,7 @@
|
||||
import { type FC, useEffect, useState } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Skeleton } from 'antd';
|
||||
import { Skeleton, Flex } from 'antd';
|
||||
|
||||
import chunkCountIcon from '@/assets/images/home/chunk_count.svg';
|
||||
import statementsCountIcon from '@/assets/images/home/statements_count.svg';
|
||||
@@ -79,30 +79,31 @@ const RecentActivity:FC = () => {
|
||||
return (
|
||||
<Card
|
||||
title={t('dashboard.recentMemoryActivities')}
|
||||
bodyClassName="rb:pt-0! rb:pb-[14px]! rb:px-4!"
|
||||
>
|
||||
{loading
|
||||
? <Skeleton />
|
||||
: !recentActivities || Object.keys(recentActivities).length === 0
|
||||
? <Empty url={activityEmpty} subTitle={t('dashboard.activityEmpty')} size={120} className="rb:mt-11.25 rb:mb-20.25" />
|
||||
: activityList.map((item, index) => (
|
||||
<div key={item.key} className={clsx("rb:flex rb:justify-between rb:items-center rb:not-italic", {
|
||||
'rb:mt-6': index !== 0
|
||||
})}>
|
||||
<div className="rb:flex rb:items-center rb:text-[#060419] rb:text-[16px] rb:font-medium">
|
||||
<img className="rb:w-10 rb:h-10 rb:mr-4" src={item.icon} />
|
||||
<div>
|
||||
{t(`dashboard.${item.key}`)}
|
||||
<div className="rb:text-[#5B6167] rb:text-[14px] rb:font-normal">
|
||||
{item.key === 'triplet_count'
|
||||
? t(`dashboard.${item.key}_desc`, { entities_count: recentActivities.triplet_entities_count, relations_count: recentActivities.triplet_relations_count })
|
||||
: t(`dashboard.${item.key}_desc`, { count: recentActivities[item.key as keyof RecentActivities] })
|
||||
}
|
||||
: <Flex vertical gap={24} justify="space-around">
|
||||
{activityList.map((item) => (
|
||||
<Flex key={item.key} align="center" justify="space-between" className={clsx("rb:not-italic")}>
|
||||
<Flex align="center" gap={20}>
|
||||
<img className="rb:size-10" src={item.icon} />
|
||||
<div>
|
||||
<div className="rb:text-[16px] rb:leading-5.5 rb:font-medium">{t(`dashboard.${item.key}`)}</div>
|
||||
<div className="rb:text-[#7B8085] rb:text-[14px] rb:font-regular rb:mt-1 rb:leading-4.5">
|
||||
{item.key === 'triplet_count'
|
||||
? t(`dashboard.${item.key}_desc`, { entities_count: recentActivities.triplet_entities_count, relations_count: recentActivities.triplet_relations_count })
|
||||
: t(`dashboard.${item.key}_desc`, { count: recentActivities[item.key as keyof RecentActivities] })
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rb:text-[#5F6266] rb:text-right rb:whitespace-nowrap">{data?.latest_relative || ''}</div>
|
||||
</div>
|
||||
))
|
||||
</Flex>
|
||||
<div className="rb:text-[#7B8085] rb:text-right rb:whitespace-nowrap">{data?.latest_relative || ''}</div>
|
||||
</Flex>
|
||||
))}
|
||||
</Flex>
|
||||
}
|
||||
</Card>
|
||||
)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 17:15:04
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 17:15:04
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-26 11:15:15
|
||||
*/
|
||||
/**
|
||||
* Tag List Component
|
||||
@@ -12,13 +12,24 @@
|
||||
import { type FC, useEffect, useState } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Skeleton } from 'antd';
|
||||
import { Skeleton, Flex } from 'antd';
|
||||
|
||||
import tagEmpty from '@/assets/images/home/tagEmpty.svg'
|
||||
import Empty from '@/components/Empty';
|
||||
import Card from './Card';
|
||||
import { getHotMemoryTags } from '@/api/memory';
|
||||
|
||||
const btnStyleList = [
|
||||
'rb:bg-[rgba(21,94,239,0.06)]',
|
||||
'rb:bg-[rgba(33,35,50,0.06)]',
|
||||
'rb:bg-[rgba(156,111,255,0.06)]'
|
||||
]
|
||||
const numStyleList = [
|
||||
'rb:bg-[#155EEF]',
|
||||
'rb:bg-[#212332]',
|
||||
'rb:bg-[#9C6FFF]'
|
||||
]
|
||||
|
||||
const TagList:FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
@@ -38,23 +49,24 @@ const TagList:FC = () => {
|
||||
return (
|
||||
<Card
|
||||
title={t('dashboard.popularMemoryTags')}
|
||||
bodyClassName='rb:overflow-hidden! rb:pt-0! rb:pb-4! rb:pl-4! rb:pr-3.25!'
|
||||
className="rb:min-h-[calc(100vh-744px)]"
|
||||
>
|
||||
{loading
|
||||
? <Skeleton />
|
||||
: !tagList || tagList.length === 0
|
||||
? <Empty url={tagEmpty} title={t('dashboard.activityEmpty')} size={120} className="rb:mt-9 rb:mb-20.25" />
|
||||
: <div className="rb:gap-3 rb:flex rb:flex-wrap">
|
||||
: <Flex wrap className="rb:gap-x-3! rb:gap-y-2.5!">
|
||||
{tagList.map((item, index) => (
|
||||
<div
|
||||
key={item.name}
|
||||
className={clsx("rb:pt-1.5 rb:pb-1.5 rb:pr-5.75 rb:pl-5 rb:border rb:leading-5 rb:bg-white rb:rounded-[17px]", {
|
||||
'rb:border-[rgba(21,94,239,0.4)] rb:text-[#155EEF]': index % 3 === 0,
|
||||
'rb:border-[rgba(255,138,76,0.4)] rb:text-[#FF5D34]': index % 3 === 1,
|
||||
'rb:border-[rgba(54,159,33,0.4)] rb:text-[#369F21]': index % 3 === 2,
|
||||
})}
|
||||
>{item.name} {item.frequency}</div>
|
||||
className={clsx("rb:rounded-[17px] rb:py-1.5 rb:pl-3 rb:pr-2", btnStyleList[index % 3])}
|
||||
>
|
||||
{item.name}
|
||||
<span className={clsx('rb:px-2 rb:py-0.5 rb:rounded-[10px] rb:text-[#FFFFFF] rb:text-[12px] rb:font-bold rb:font-[MiSans-Demibold] rb:ml-2', numStyleList[index % 3])}>{item.frequency}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Flex>
|
||||
}
|
||||
</Card>
|
||||
)
|
||||
|
||||
112
web/src/views/Home/components/TopCardList.tsx
Normal file
112
web/src/views/Home/components/TopCardList.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 17:28:07
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-11 14:57:55
|
||||
*/
|
||||
/**
|
||||
* Top Card List Component
|
||||
* Displays dashboard summary cards for key metrics
|
||||
* Shows total memory capacity, applications, knowledge bases, and API calls
|
||||
*/
|
||||
|
||||
import { type FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import clsx from 'clsx';
|
||||
import { Flex } from 'antd';
|
||||
|
||||
import totalMemoryCapacity from '@/assets/images/home/totalMemoryCapacity.svg';
|
||||
import userMemory from '@/assets/images/home/userMemory.svg';
|
||||
import knowledgeBaseCount from '@/assets/images/home/knowledgeBaseCount.svg';
|
||||
import apiCallCount from '@/assets/images/home/apiCallCount.svg';
|
||||
import type { DashboardData } from '../index'
|
||||
|
||||
/** Card configuration with styling */
|
||||
const list = [
|
||||
{
|
||||
key: 'totalMemoryCapacity',
|
||||
icon: totalMemoryCapacity,
|
||||
// value: '45,678',
|
||||
// trendValue: '12.5%',
|
||||
// trend: 'up',
|
||||
// trendDesc: 'comparedToYesterday',
|
||||
background: 'rb:bg-[url("@/assets/images/home/totalMemoryCapacity.png")] rb:bg-cover rb:bg-no-repeat',
|
||||
},
|
||||
{
|
||||
key: 'application',
|
||||
icon: userMemory,
|
||||
// value: '32,145',
|
||||
// trendValue: '12.5%',
|
||||
// trend: 'down',
|
||||
// trendDesc: 'comparedToYesterday',
|
||||
// background: 'linear-gradient( 180deg, #F1FBF5 0%, #F9FDFF 100%)',
|
||||
},
|
||||
{
|
||||
key: 'knowledgeBaseCount',
|
||||
icon: knowledgeBaseCount,
|
||||
// value: '13,533',
|
||||
// trendValue: '15.7%',
|
||||
// trend: 'up',
|
||||
// trendDesc: 'thisWeek',
|
||||
// background: 'linear-gradient( 180deg, #E6F5FE 0%, #FBFDFF 100%)',
|
||||
},
|
||||
{
|
||||
key: 'apiCallCount',
|
||||
icon: apiCallCount,
|
||||
// value: '856.2k',
|
||||
// trendValue: '23.1%',
|
||||
// trend: 'up',
|
||||
// trendDesc: 'comparedToYesterday',
|
||||
// background: 'linear-gradient( 180deg, #F8F6F5 0%, #FAFDFF 100%)',
|
||||
},
|
||||
]
|
||||
/**
|
||||
* Component props
|
||||
* @param data - Dashboard statistics data
|
||||
*/
|
||||
const TopCardList: FC<{data?: DashboardData}> = ({ data }) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div className="rb:grid rb:grid-cols-2 rb:gap-3">
|
||||
{list.map((item) => {
|
||||
return (
|
||||
<div
|
||||
key={item.key}
|
||||
className={`rb:rounded-2xl rb:bg-[#FFFFFF] rb:py-4 rb:px-3 ${item.background || ''}`}
|
||||
>
|
||||
<div className={clsx("rb:text-[12px] rb:leading-4", {
|
||||
'rb:text-[#FFFFFF]': item.key === 'totalMemoryCapacity',
|
||||
'rb:text-[#5B6167]': item.key !== 'totalMemoryCapacity',
|
||||
})}>{t(`dashboard.${item.key}`)}</div>
|
||||
|
||||
<div className={clsx("rb:text-[20px] rb:font-bold rb:leading-7 rb:mt-1 rb:font-[MiSans-Bold]", {
|
||||
'rb:text-[#FFFFFF]': item.key === 'totalMemoryCapacity',
|
||||
// 'rb:text-[#171719]': item.key !== 'totalMemoryCapacity',
|
||||
})}>
|
||||
{data?.[item.key as keyof DashboardData] || 0}
|
||||
</div>
|
||||
|
||||
<Flex align="center" className={clsx('rb:font-medium rb:mt-7.5!', {
|
||||
'rb:text-[#FFFFFF]': item.key === 'totalMemoryCapacity',
|
||||
'rb:text-[#369F21]': item.key !== 'totalMemoryCapacity',
|
||||
})}>
|
||||
0%
|
||||
<div className={clsx("rb:size-3.5 rb:cursor-pointer rb:bg-cover", {
|
||||
"rb:bg-[url('@/assets/images/home/arrow_up.svg')]": item.key === 'totalMemoryCapacity',
|
||||
"rb:bg-[url('@/assets/images/home/arrow_up_success.svg')]": item.key !== 'totalMemoryCapacity',
|
||||
})}></div>
|
||||
</Flex>
|
||||
<div className={clsx("rb:text-[12px] rb:leading-4 rb:mt-0.5", {
|
||||
'rb:text-[#FFFFFF]': item.key === 'totalMemoryCapacity',
|
||||
'rb:text-[#5B6167]': item.key !== 'totalMemoryCapacity',
|
||||
})}>
|
||||
{t('dashboard.comparedToYesterday')}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TopCardList
|
||||
@@ -1,97 +0,0 @@
|
||||
.card {
|
||||
border-radius: 12px;
|
||||
border: 1px solid #DFE4ED;
|
||||
padding: 0;
|
||||
}
|
||||
.header {
|
||||
padding: 20px;
|
||||
line-height: 44px;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 16px;
|
||||
color: #5B6167;
|
||||
font-style: normal;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #DFE4ED;
|
||||
}
|
||||
.avatar {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
background: #FFFFFF;
|
||||
box-shadow: 0px 2px 6px 0px rgba(33, 35, 50, 0.1);
|
||||
border-radius: 22px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 12px;
|
||||
}
|
||||
.avatar img {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
.content {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-family: Gilroy, Gilroy;
|
||||
font-weight: 800;
|
||||
font-size: 28px;
|
||||
color: #212332;
|
||||
text-align: left;
|
||||
font-style: normal;
|
||||
}
|
||||
.content-right {
|
||||
text-align: right;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
color: #5F6266;
|
||||
line-height: 16px;
|
||||
font-style: normal;
|
||||
row-gap: 4px;
|
||||
}
|
||||
.trend {
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
line-height: 16px;
|
||||
font-style: normal;
|
||||
padding-left: 15px;
|
||||
position: relative;
|
||||
margin-bottom: 4px;
|
||||
display: inline-block;
|
||||
}
|
||||
.trend::before {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 1px;
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
}
|
||||
.trend.up {
|
||||
color: #369F21;
|
||||
}
|
||||
.trend.up::before {
|
||||
background-image: url('@/assets/images/home/arrow_up_success.svg');
|
||||
}
|
||||
.trend.down {
|
||||
color: #FF5D34;
|
||||
}
|
||||
.trend.down::before {
|
||||
background-image: url('@/assets/images/home/arrow_down.png');
|
||||
}
|
||||
|
||||
.trend-desc {
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
color: #155EEF;
|
||||
line-height: 16px;
|
||||
text-align: left;
|
||||
font-style: normal;
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 17:28:07
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 17:28:07
|
||||
*/
|
||||
/**
|
||||
* Top Card List Component
|
||||
* Displays dashboard summary cards for key metrics
|
||||
* Shows total memory capacity, applications, knowledge bases, and API calls
|
||||
*/
|
||||
|
||||
import { type FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import totalMemoryCapacity from '@/assets/images/home/totalMemoryCapacity.svg';
|
||||
import userMemory from '@/assets/images/home/userMemory.svg';
|
||||
import knowledgeBaseCount from '@/assets/images/home/knowledgeBaseCount.svg';
|
||||
import apiCallCount from '@/assets/images/home/apiCallCount.svg';
|
||||
import styles from './index.module.css'
|
||||
import type { DashboardData } from '../../index'
|
||||
|
||||
/** Card configuration with styling */
|
||||
const list = [
|
||||
{
|
||||
key: 'totalMemoryCapacity',
|
||||
icon: totalMemoryCapacity,
|
||||
// value: '45,678',
|
||||
// trendValue: '12.5%',
|
||||
// trend: 'up',
|
||||
// trendDesc: 'comparedToYesterday',
|
||||
background: 'linear-gradient(180deg, #E6EFFE 0%, #F9FDFF 100%)',
|
||||
},
|
||||
{
|
||||
key: 'application',
|
||||
icon: userMemory,
|
||||
// value: '32,145',
|
||||
// trendValue: '12.5%',
|
||||
// trend: 'down',
|
||||
// trendDesc: 'comparedToYesterday',
|
||||
background: 'linear-gradient( 180deg, #F1FBF5 0%, #F9FDFF 100%)',
|
||||
},
|
||||
{
|
||||
key: 'knowledgeBaseCount',
|
||||
icon: knowledgeBaseCount,
|
||||
// value: '13,533',
|
||||
// trendValue: '15.7%',
|
||||
// trend: 'up',
|
||||
// trendDesc: 'thisWeek',
|
||||
background: 'linear-gradient( 180deg, #E6F5FE 0%, #FBFDFF 100%)',
|
||||
},
|
||||
{
|
||||
key: 'apiCallCount',
|
||||
icon: apiCallCount,
|
||||
// value: '856.2k',
|
||||
// trendValue: '23.1%',
|
||||
// trend: 'up',
|
||||
// trendDesc: 'comparedToYesterday',
|
||||
background: 'linear-gradient( 180deg, #F8F6F5 0%, #FAFDFF 100%)',
|
||||
},
|
||||
]
|
||||
/**
|
||||
* Component props
|
||||
* @param data - Dashboard statistics data
|
||||
*/
|
||||
const TopCardList: FC<{data?: DashboardData}> = ({ data }) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div className="rb:grid rb:grid-cols-4 rb:gap-4">
|
||||
{list.map((item) => {
|
||||
return (
|
||||
<div
|
||||
key={item.key}
|
||||
className={styles.card}
|
||||
style={{
|
||||
background: item.background,
|
||||
}}
|
||||
>
|
||||
<div className={styles.header}>
|
||||
<div className={styles.avatar}><img src={item.icon} /></div>
|
||||
<div className={styles.headerTitle}>{t(`dashboard.${item.key}`)}</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.content}>
|
||||
{data?.[item.key as keyof DashboardData] || 0}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TopCardList
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 17:12:43
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 17:26:04
|
||||
* @Last Modified time: 2026-02-10 11:57:58
|
||||
*/
|
||||
/**
|
||||
* Home Dashboard Page
|
||||
@@ -19,6 +19,7 @@ import { getDashboardData, getMemoryIncrement, getKbTypes } from '@/api/memory';
|
||||
import RecentActivity from './components/RecentActivity'
|
||||
import TagList from './components/TagList'
|
||||
import QuickOperation from './components/QuickOperation'
|
||||
import ApiLineCard from './components/ApiLineCard'
|
||||
|
||||
/**
|
||||
* Dashboard statistics data
|
||||
@@ -120,42 +121,39 @@ const Home = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rb:pb-6">
|
||||
<TopCardList data={dashboardData} />
|
||||
|
||||
<Row className="rb:mt-4" gutter={16}>
|
||||
<Col span={12}>
|
||||
<LineCard
|
||||
chartData={memoryIncrement}
|
||||
limit={limit}
|
||||
onChange={handleRangeChange}
|
||||
type="memoryGrowthTrend"
|
||||
seriesList={['total_num']}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<PieCard
|
||||
loading={loading.knowledgeTypeDistribution}
|
||||
chartData={knowledgeTypeDistribution}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row className="rb:mt-4" gutter={16}>
|
||||
<Col span={12}>
|
||||
<RecentActivity />
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<TagList />
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row className="rb:mt-4" gutter={16}>
|
||||
<Col span={24}>
|
||||
<QuickOperation />
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
<Row gutter={[12, 12]}>
|
||||
<Col span={8}>
|
||||
<TopCardList data={dashboardData} />
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<LineCard
|
||||
chartData={memoryIncrement}
|
||||
limit={limit}
|
||||
onChange={handleRangeChange}
|
||||
type="memoryGrowthTrend"
|
||||
seriesList={['total_num']}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<ApiLineCard
|
||||
/>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<PieCard
|
||||
loading={loading.knowledgeTypeDistribution}
|
||||
chartData={knowledgeTypeDistribution}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<RecentActivity />
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<QuickOperation />
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<TagList />
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user