feat: Add base project structure with API and web components
This commit is contained in:
23
web/src/views/Home/components/Card.tsx
Normal file
23
web/src/views/Home/components/Card.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { type FC, type ReactNode } from 'react'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
|
||||
interface CardProps {
|
||||
children: ReactNode;
|
||||
title: string;
|
||||
headerOperate?: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const Card: FC<CardProps> = ({ children, title, headerOperate, className }) => {
|
||||
return (
|
||||
<RbCard
|
||||
headerType="borderless"
|
||||
title={title}
|
||||
extra={headerOperate}
|
||||
className={`rb:h-full! ${className}`}
|
||||
>
|
||||
{children}
|
||||
</RbCard>
|
||||
)
|
||||
}
|
||||
export default Card;
|
||||
176
web/src/views/Home/components/LineCard.tsx
Normal file
176
web/src/views/Home/components/LineCard.tsx
Normal file
@@ -0,0 +1,176 @@
|
||||
import { type FC, useRef } 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'
|
||||
|
||||
interface LineCardProps {
|
||||
chartData: Array<Record<string, string | number>>;
|
||||
limit: number;
|
||||
onChange: (value: string, type: string) => void;
|
||||
type: string;
|
||||
className?: string;
|
||||
seriesList: string[];
|
||||
}
|
||||
|
||||
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]
|
||||
}
|
||||
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 },
|
||||
{ label: t('dashboard.lastDays', { days: 90 }), value: 90 },
|
||||
{ label: t('dashboard.lastHalfYear'), value: 180 },
|
||||
{ label: t('dashboard.lastYear'), value: 365 },
|
||||
]
|
||||
|
||||
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'
|
||||
}
|
||||
])
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
return list
|
||||
}
|
||||
const formatSeriesList = () => {
|
||||
return seriesList.map(key => ({
|
||||
...SeriesConfig,
|
||||
name: t(`dashboard.${key}`),
|
||||
}))
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={t(`dashboard.${type}`)}
|
||||
headerOperate={
|
||||
<Select
|
||||
value={limit}
|
||||
options={options}
|
||||
onChange={(value) => onChange(value, type)}
|
||||
style={{ width: '150px' }}
|
||||
/>
|
||||
}
|
||||
className={`rb:pb-[24px] ${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}
|
||||
onEvents={{
|
||||
// 图表渲染完成后再次调整大小,确保宽度正确
|
||||
// 使用 setTimeout 避免在主渲染过程中调用 resize
|
||||
rendered: () => {
|
||||
if (chartRef.current) {
|
||||
setTimeout(() => {
|
||||
chartRef.current?.getEchartsInstance().resize();
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : <Empty size={120} className="rb:mt-[48px] rb:mb-[81px]" />}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default LineCard
|
||||
112
web/src/views/Home/components/PieCard.tsx
Normal file
112
web/src/views/Home/components/PieCard.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import { type FC, useRef } 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'
|
||||
|
||||
interface PieCardProps {
|
||||
chartData: Array<Record<string, string | number>>;
|
||||
loading: boolean;
|
||||
}
|
||||
const Colors = ['#155EEF', '#31E8FF', '#AD88FF', '#FFB048', '#4DA8FF', '#03BDFF']
|
||||
|
||||
const PieCard: FC<PieCardProps> = ({ chartData, loading }) => {
|
||||
const { t } = useTranslation()
|
||||
const chartRef = useRef<ReactEcharts>(null);
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={t('dashboard.knowledgeBaseTypeDistribution')}
|
||||
>
|
||||
{loading
|
||||
? <Loading size={249} />
|
||||
: !chartData || chartData.length === 0
|
||||
? <Empty size={120} className="rb:mt-[48px] rb:mb-[81px]" />
|
||||
: <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}
|
||||
onEvents={{
|
||||
// 图表渲染完成后再次调整大小,确保宽度正确
|
||||
// 使用 setTimeout 避免在主渲染过程中调用 resize
|
||||
rendered: () => {
|
||||
if (chartRef.current) {
|
||||
setTimeout(() => {
|
||||
chartRef.current?.getEchartsInstance().resize();
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default PieCard
|
||||
49
web/src/views/Home/components/QuickOperation.tsx
Normal file
49
web/src/views/Home/components/QuickOperation.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import { type FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
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 arrowTopRight from '@/assets/images/home/arrow_top_right.svg';
|
||||
|
||||
const quickOperations = [
|
||||
{ key: 'createNewApplication', url: '/application' },
|
||||
{ key: 'createNewKnowledge', url: '/knowledge-base' },
|
||||
{ key: 'memoryConversation', url: '/memory-conversation' },
|
||||
]
|
||||
|
||||
const quickOperationIcons: {[key: string]: string | undefined} = {
|
||||
createNewApplication: applicationIcon,
|
||||
createNewKnowledge: knowledgeIcon,
|
||||
memoryConversation: memoryConversationIcon,
|
||||
}
|
||||
const QuickOperation:FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleJump = (url: string | null) => {
|
||||
if (url) {
|
||||
navigate(url)
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Card
|
||||
title={t('dashboard.quickOperation')}
|
||||
>
|
||||
<div className="rb:grid rb:grid-cols-3 rb:gap-[16px]">
|
||||
{quickOperations.map(item => (
|
||||
<div key={item.key} className="rb:rounded-[8px] rb:p-[20px_16px] rb:border-1 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-[32px] rb:h-[32px]" src={quickOperationIcons[item.key]} />
|
||||
<img className="rb:w-[16px] rb:h-[16px]" src={arrowTopRight} />
|
||||
</div>
|
||||
<div className="rb:mt-[24px] rb:text-[#212332] rb:text-[16px] rb:leading-[20px] rb:font-medium">{t(`dashboard.${item.key}`)}</div>
|
||||
<div className="rb:mt-[8px] rb:text-[#5B6167] rb:text-[12px] rb:font-regular">{t(`dashboard.${item.key}Desc`)}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
export default QuickOperation
|
||||
85
web/src/views/Home/components/RecentActivity.tsx
Normal file
85
web/src/views/Home/components/RecentActivity.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import { type FC, useEffect, useState } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Skeleton } from 'antd';
|
||||
import chunkCountIcon from '@/assets/images/home/chunk_count.svg';
|
||||
import statementsCountIcon from '@/assets/images/home/statements_count.svg';
|
||||
import tripletCountIcon from '@/assets/images/home/triplet_count.svg';
|
||||
import temporalCountIcon from '@/assets/images/home/temporal_count.svg';
|
||||
import activityEmpty from '@/assets/images/home/ActivityEmpty.svg'
|
||||
import Empty from '@/components/Empty';
|
||||
import Card from './Card';
|
||||
import { getRecentActivityStats } from '@/api/memory';
|
||||
|
||||
interface Data {
|
||||
latest_relative: string;
|
||||
stats: RecentActivities;
|
||||
}
|
||||
interface RecentActivities {
|
||||
"chunk_count": number; // 数据分块
|
||||
"statements_count": number; // 语句提取
|
||||
"triplet_entities_count": number; // 实体关系萃取-实体节点
|
||||
"triplet_relations_count": number; // 实体关系萃取 - 关系连接
|
||||
"temporal_count": number; // 时间萃取
|
||||
}
|
||||
|
||||
const activityList = [
|
||||
{ key: 'chunk_count', icon: chunkCountIcon },
|
||||
{ key: 'statements_count', icon: statementsCountIcon },
|
||||
{ key: 'triplet_count', icon: tripletCountIcon },
|
||||
{ key: 'temporal_count', icon: temporalCountIcon },
|
||||
]
|
||||
|
||||
const RecentActivity:FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [data, setData] = useState<Data | null>(null);
|
||||
const [recentActivities, setRecentActivities] = useState<RecentActivities | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
getRecentActivityList()
|
||||
}, [])
|
||||
|
||||
// 最近活动统计
|
||||
const getRecentActivityList = () => {
|
||||
setLoading(true)
|
||||
getRecentActivityStats().then(res => {
|
||||
const response = res as Data || {}
|
||||
setData(response)
|
||||
setRecentActivities(response.stats as RecentActivities || {})
|
||||
})
|
||||
.finally(() => setLoading(false))
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={t('dashboard.recentMemoryActivities')}
|
||||
>
|
||||
{loading
|
||||
? <Skeleton />
|
||||
: !recentActivities || Object.keys(recentActivities).length === 0
|
||||
? <Empty url={activityEmpty} subTitle={t('dashboard.activityEmpty')} size={120} className="rb:mt-[45px] rb:mb-[81px]" />
|
||||
: activityList.map((item, index) => (
|
||||
<div key={item.key} className={clsx("rb:flex rb:justify-between rb:items-center rb:not-italic", {
|
||||
'rb:mt-[24px]': index !== 0
|
||||
})}>
|
||||
<div className="rb:flex rb:items-center rb:text-[#060419] rb:text-[16px] rb:font-medium">
|
||||
<img className="rb:w-[40px] rb:h-[40px] rb:mr-[16px]" 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] })
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rb:text-[#5F6266] rb:text-right rb:whitespace-nowrap">{data?.latest_relative || ''}</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
export default RecentActivity
|
||||
50
web/src/views/Home/components/TagList.tsx
Normal file
50
web/src/views/Home/components/TagList.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import { type FC, useEffect, useState } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Skeleton } 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 TagList:FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [tagList, setTagList] = useState<Array<{ name: string; frequency: number }>>([]);
|
||||
|
||||
useEffect(() => {
|
||||
getTagList()
|
||||
}, [])
|
||||
|
||||
// 热门记忆标签
|
||||
const getTagList = () => {
|
||||
setLoading(true)
|
||||
getHotMemoryTags().then(res => {
|
||||
setTagList(Array.isArray(res) ? res : [])
|
||||
}).finally(() => setLoading(false))
|
||||
}
|
||||
return (
|
||||
<Card
|
||||
title={t('dashboard.popularMemoryTags')}
|
||||
>
|
||||
{loading
|
||||
? <Skeleton />
|
||||
: !tagList || tagList.length === 0
|
||||
? <Empty url={tagEmpty} title={t('dashboard.activityEmpty')} size={120} className="rb:mt-[36px] rb:mb-[81px]" />
|
||||
: <div className="rb:gap-[12px] rb:flex rb:flex-wrap">
|
||||
{tagList.map((item, index) => (
|
||||
<div
|
||||
key={item.name}
|
||||
className={clsx("rb:pt-[6px] rb:pb-[6px] rb:pr-[23px] rb:pl-[20px] rb:border-1 rb:leading-[20px] 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>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
export default TagList
|
||||
97
web/src/views/Home/components/TopCardList/index.module.css
Normal file
97
web/src/views/Home/components/TopCardList/index.module.css
Normal file
@@ -0,0 +1,97 @@
|
||||
.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;
|
||||
}
|
||||
81
web/src/views/Home/components/TopCardList/index.tsx
Normal file
81
web/src/views/Home/components/TopCardList/index.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
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 clsx from 'clsx';
|
||||
import type { DashboardData } from '../../index'
|
||||
|
||||
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%)',
|
||||
},
|
||||
]
|
||||
const TopCardList: FC<{data?: DashboardData}> = ({ data }) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div className="rb:grid rb:grid-cols-4 rb:gap-[16px]">
|
||||
{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] || item.value || 0}
|
||||
<div className={styles.contentRight}>
|
||||
{item.trendValue && <div className={clsx(styles.trend, styles[item.trend])}>{item.trendValue}</div>}
|
||||
{item.trendDesc && <div>{t(`dashboard.${item.trendDesc}`)}</div>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TopCardList
|
||||
Reference in New Issue
Block a user