feat: refine collected data overview and admin navigation
This commit is contained in:
4
frontend/package-lock.json
generated
4
frontend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "planet-frontend",
|
||||
"version": "1.0.0",
|
||||
"version": "0.21.5-dev",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "planet-frontend",
|
||||
"version": "1.0.0",
|
||||
"version": "0.21.5-dev",
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.2.6",
|
||||
"antd": "^5.12.5",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "planet-frontend",
|
||||
"version": "0.21.4-dev",
|
||||
"version": "0.21.5-dev",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.2.6",
|
||||
|
||||
@@ -173,7 +173,6 @@ body {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
@@ -320,6 +319,10 @@ body {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.users-table-region .ant-table-body {
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
.data-source-table-region .ant-table-wrapper,
|
||||
.data-source-table-region .ant-spin-nested-loading,
|
||||
.data-source-table-region .ant-spin-container {
|
||||
@@ -416,8 +419,8 @@ body {
|
||||
|
||||
.table-scroll-region .ant-table-body::-webkit-scrollbar,
|
||||
.table-scroll-region .ant-table-content::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.table-scroll-region .ant-table-body::-webkit-scrollbar-thumb,
|
||||
@@ -439,6 +442,32 @@ body {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.data-list-controls-shell {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(148, 163, 184, 0.82) transparent;
|
||||
}
|
||||
|
||||
.data-list-controls-shell::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.data-list-controls-shell::-webkit-scrollbar-thumb {
|
||||
background: rgba(148, 163, 184, 0.82);
|
||||
border-radius: 999px;
|
||||
border: 2px solid transparent;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
.data-list-controls-shell::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(100, 116, 139, 0.9);
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
.data-list-controls-shell::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.settings-shell,
|
||||
.settings-tabs-shell,
|
||||
.settings-tabs,
|
||||
@@ -591,6 +620,8 @@ body {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
scrollbar-gutter: stable;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(148, 163, 184, 0.82) transparent;
|
||||
}
|
||||
|
||||
.data-list-summary-card .ant-card-head,
|
||||
@@ -604,6 +635,39 @@ body {
|
||||
|
||||
.data-list-summary-card-inner {
|
||||
min-height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.data-list-summary-kpis {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.data-list-summary-kpi {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
padding: 12px;
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(180deg, #f8fafc 0%, #eef2ff 100%);
|
||||
border: 1px solid rgba(148, 163, 184, 0.22);
|
||||
}
|
||||
|
||||
.data-list-summary-kpi__head,
|
||||
.data-list-summary-section-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.data-list-summary-section-head {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.data-list-right-column {
|
||||
@@ -719,12 +783,24 @@ body {
|
||||
color: rgba(15, 23, 42, 0.72) !important;
|
||||
}
|
||||
|
||||
.data-list-summary-empty {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 140px;
|
||||
border-radius: 14px;
|
||||
background: rgba(248, 250, 252, 0.8);
|
||||
border: 1px dashed rgba(148, 163, 184, 0.35);
|
||||
}
|
||||
|
||||
.data-list-summary-card--panel .ant-card-body::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.data-list-summary-card--panel .ant-card-body::-webkit-scrollbar-thumb {
|
||||
background: rgba(148, 163, 184, 0.8);
|
||||
background: rgba(148, 163, 184, 0.82);
|
||||
border-radius: 999px;
|
||||
border: 2px solid transparent;
|
||||
background-clip: padding-box;
|
||||
@@ -927,6 +1003,13 @@ body {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.data-list-controls-shell {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
min-height: 0;
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
.data-list-topbar {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
@@ -945,11 +1028,22 @@ body {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.data-list-summary-card--panel,
|
||||
.data-list-summary-card--panel .ant-card-body,
|
||||
.data-list-table-shell,
|
||||
.data-list-table-shell .ant-card-body {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.data-list-summary-treemap {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
grid-auto-rows: minmax(88px, 1fr);
|
||||
}
|
||||
|
||||
.data-list-summary-kpis {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.data-list-filter-grid {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
@@ -974,6 +1068,11 @@ body {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.data-list-summary-section-head {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.data-list-detail-modal {
|
||||
|
||||
@@ -4,10 +4,12 @@ import {
|
||||
DatabaseOutlined,
|
||||
BarChartOutlined,
|
||||
AlertOutlined,
|
||||
GlobalOutlined,
|
||||
WifiOutlined,
|
||||
DisconnectOutlined,
|
||||
ReloadOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { useAuthStore } from '../../stores/auth'
|
||||
import AppLayout from '../../components/AppLayout/AppLayout'
|
||||
import { formatDateTimeZhCN } from '../../utils/datetime'
|
||||
@@ -169,6 +171,21 @@ function Dashboard() {
|
||||
</Row>
|
||||
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col xs={24}>
|
||||
<Card>
|
||||
<Space direction="vertical" size={12} style={{ width: '100%' }}>
|
||||
<div>
|
||||
<Title level={5} style={{ margin: 0 }}>快捷入口</Title>
|
||||
<Text type="secondary">快速访问地球可视化页面</Text>
|
||||
</div>
|
||||
<Link to="/earth">
|
||||
<Button type="primary" icon={<GlobalOutlined />}>
|
||||
访问 Earth
|
||||
</Button>
|
||||
</Link>
|
||||
</Space>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} md={8}>
|
||||
<Card>
|
||||
<Statistic title="严重告警" value={stats?.alerts?.critical || 0} valueStyle={{ color: '#ff4d4f' }} prefix={<AlertOutlined />} />
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { useEffect, useLayoutEffect, useMemo, useRef, useState, type CSSProperties } from 'react'
|
||||
import {
|
||||
Table, Tag, Space, Card, Select, Input, Button,
|
||||
Table, Tag, Space, Card, Select, Input, Button, Segmented,
|
||||
Modal, Spin, Empty, Tooltip, Typography, Grid
|
||||
} from 'antd'
|
||||
import type { ColumnsType } from 'antd/es/table'
|
||||
import type { CustomTagProps } from 'rc-select/lib/BaseSelect'
|
||||
import {
|
||||
DatabaseOutlined, GlobalOutlined, CloudServerOutlined,
|
||||
AppstoreOutlined, EyeOutlined, SearchOutlined, FilterOutlined, ReloadOutlined
|
||||
AppstoreOutlined, EyeOutlined, SearchOutlined, FilterOutlined, ReloadOutlined,
|
||||
ApartmentOutlined, EnvironmentOutlined
|
||||
} from '@ant-design/icons'
|
||||
import axios from 'axios'
|
||||
import AppLayout from '../../components/AppLayout/AppLayout'
|
||||
@@ -43,8 +44,10 @@ interface CollectedData {
|
||||
|
||||
interface Summary {
|
||||
total_records: number
|
||||
overall_total_records: number
|
||||
by_source: Record<string, Record<string, number>>
|
||||
source_totals: Array<{ source: string; source_name: string; count: number }>
|
||||
type_totals: Array<{ data_type: string; count: number }>
|
||||
}
|
||||
|
||||
interface SourceOption {
|
||||
@@ -256,6 +259,7 @@ function DataList() {
|
||||
const [tableHeaderHeight, setTableHeaderHeight] = useState(0)
|
||||
const [leftPanelWidth, setLeftPanelWidth] = useState(360)
|
||||
const [summaryBodyHeight, setSummaryBodyHeight] = useState(0)
|
||||
const [summaryBodyWidth, setSummaryBodyWidth] = useState(0)
|
||||
|
||||
const [data, setData] = useState<CollectedData[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
@@ -271,6 +275,7 @@ function DataList() {
|
||||
const [detailVisible, setDetailVisible] = useState(false)
|
||||
const [detailData, setDetailData] = useState<CollectedData | null>(null)
|
||||
const [detailLoading, setDetailLoading] = useState(false)
|
||||
const [treemapDimension, setTreemapDimension] = useState<'source' | 'type'>('source')
|
||||
|
||||
useEffect(() => {
|
||||
const updateLayout = () => {
|
||||
@@ -279,6 +284,7 @@ function DataList() {
|
||||
setRightColumnHeight(rightColumnRef.current?.offsetHeight || 0)
|
||||
setTableHeaderHeight(tableHeaderRef.current?.offsetHeight || 0)
|
||||
setSummaryBodyHeight(summaryBodyRef.current?.offsetHeight || 0)
|
||||
setSummaryBodyWidth(summaryBodyRef.current?.offsetWidth || 0)
|
||||
}
|
||||
|
||||
updateLayout()
|
||||
@@ -306,7 +312,7 @@ function DataList() {
|
||||
const minLeft = 260
|
||||
const minRight = 360
|
||||
const maxLeft = Math.max(minLeft, mainAreaWidth - minRight - 12)
|
||||
const preferredLeft = Math.max(minLeft, Math.min(Math.round((mainAreaWidth - 12) / 4), maxLeft))
|
||||
const preferredLeft = Math.max(minLeft, Math.min(Math.round((mainAreaWidth - 12) / 3), maxLeft))
|
||||
|
||||
setLeftPanelWidth((current) => {
|
||||
if (!hasCustomLeftWidthRef.current) {
|
||||
@@ -364,7 +370,13 @@ function DataList() {
|
||||
|
||||
const fetchSummary = async () => {
|
||||
try {
|
||||
const res = await axios.get('/api/v1/collected/summary')
|
||||
const params = new URLSearchParams()
|
||||
if (sourceFilter.length > 0) params.append('source', sourceFilter.join(','))
|
||||
if (typeFilter.length > 0) params.append('data_type', typeFilter.join(','))
|
||||
if (searchText) params.append('search', searchText)
|
||||
|
||||
const query = params.toString()
|
||||
const res = await axios.get(query ? `/api/v1/collected/summary?${query}` : '/api/v1/collected/summary')
|
||||
setSummary(res.data)
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch summary:', error)
|
||||
@@ -385,17 +397,18 @@ function DataList() {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchSummary()
|
||||
fetchFilters()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
fetchData()
|
||||
fetchSummary()
|
||||
}, [page, pageSize, sourceFilter, typeFilter])
|
||||
|
||||
const handleSearch = () => {
|
||||
setPage(1)
|
||||
fetchData()
|
||||
fetchSummary()
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
@@ -404,6 +417,7 @@ function DataList() {
|
||||
setSearchText('')
|
||||
setPage(1)
|
||||
setTimeout(fetchData, 0)
|
||||
setTimeout(fetchSummary, 0)
|
||||
}
|
||||
|
||||
const handleViewDetail = async (id: number) => {
|
||||
@@ -431,6 +445,25 @@ function DataList() {
|
||||
return iconMap[source] || <DatabaseOutlined />
|
||||
}
|
||||
|
||||
const getDataTypeIcon = (dataType: string) => {
|
||||
const iconMap: Record<string, React.ReactNode> = {
|
||||
supercomputer: <CloudServerOutlined />,
|
||||
gpu_cluster: <CloudServerOutlined />,
|
||||
model: <AppstoreOutlined />,
|
||||
dataset: <DatabaseOutlined />,
|
||||
space: <AppstoreOutlined />,
|
||||
submarine_cable: <GlobalOutlined />,
|
||||
cable_landing_point: <EnvironmentOutlined />,
|
||||
cable_landing_relation: <GlobalOutlined />,
|
||||
ixp: <ApartmentOutlined />,
|
||||
network: <GlobalOutlined />,
|
||||
facility: <ApartmentOutlined />,
|
||||
generic: <DatabaseOutlined />,
|
||||
}
|
||||
|
||||
return iconMap[dataType] || <DatabaseOutlined />
|
||||
}
|
||||
|
||||
const getSourceTagColor = (source: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
top500: 'geekblue',
|
||||
@@ -504,72 +537,69 @@ function DataList() {
|
||||
)
|
||||
}
|
||||
|
||||
const getTypeColor = (type: string) => {
|
||||
const colors: Record<string, string> = {
|
||||
supercomputer: 'red',
|
||||
model: 'blue',
|
||||
dataset: 'green',
|
||||
space: 'purple',
|
||||
submarine_cable: 'cyan',
|
||||
gpu_cluster: 'orange',
|
||||
ixp: 'magenta',
|
||||
network: 'gold',
|
||||
facility: 'lime',
|
||||
}
|
||||
return colors[type] || 'default'
|
||||
}
|
||||
|
||||
const activeFilterCount = useMemo(
|
||||
() => [sourceFilter.length > 0, typeFilter.length > 0, searchText.trim()].filter(Boolean).length,
|
||||
[sourceFilter, typeFilter, searchText]
|
||||
)
|
||||
|
||||
const summaryItems = useMemo(() => {
|
||||
const items = [
|
||||
{ key: 'total', label: '总记录', value: summary?.total_records || 0, icon: <DatabaseOutlined /> },
|
||||
const summaryKpis = useMemo(
|
||||
() => [
|
||||
{ key: 'total', label: '总记录', value: summary?.overall_total_records || 0, icon: <DatabaseOutlined /> },
|
||||
{ key: 'result', label: '筛选结果', value: total, icon: <SearchOutlined /> },
|
||||
{ key: 'filters', label: '启用筛选', value: activeFilterCount, icon: <FilterOutlined /> },
|
||||
{ key: 'sources', label: '数据源数', value: sources.length, icon: <DatabaseOutlined /> },
|
||||
]
|
||||
{
|
||||
key: 'coverage',
|
||||
label: treemapDimension === 'source' ? '覆盖数据源' : '覆盖类型',
|
||||
value: treemapDimension === 'source'
|
||||
? summary?.source_totals?.length || 0
|
||||
: summary?.type_totals?.length || 0,
|
||||
icon: treemapDimension === 'source' ? <DatabaseOutlined /> : <AppstoreOutlined />,
|
||||
},
|
||||
],
|
||||
[summary, total, activeFilterCount, treemapDimension]
|
||||
)
|
||||
|
||||
for (const item of (summary?.source_totals || []).slice(0, isCompact ? 3 : 5)) {
|
||||
items.push({
|
||||
key: item.source,
|
||||
label: item.source_name,
|
||||
const distributionItems = useMemo(() => {
|
||||
if (!summary) return []
|
||||
|
||||
if (treemapDimension === 'type') {
|
||||
return summary.type_totals.map((item) => ({
|
||||
key: item.data_type,
|
||||
label: item.data_type,
|
||||
value: item.count,
|
||||
icon: getSourceIcon(item.source),
|
||||
})
|
||||
icon: getDataTypeIcon(item.data_type),
|
||||
}))
|
||||
}
|
||||
|
||||
return items
|
||||
}, [summary, total, activeFilterCount, isCompact, sources.length])
|
||||
return summary.source_totals.map((item) => ({
|
||||
key: item.source,
|
||||
label: item.source_name,
|
||||
value: item.count,
|
||||
icon: getSourceIcon(item.source),
|
||||
}))
|
||||
}, [summary, treemapDimension])
|
||||
|
||||
const treemapColumns = useMemo(() => {
|
||||
if (isCompact) return 1
|
||||
if (isCompact) return summaryBodyWidth >= 320 ? 2 : 1
|
||||
if (leftPanelWidth < 360) return 2
|
||||
if (leftPanelWidth < 520) return 3
|
||||
return 4
|
||||
}, [isCompact, leftPanelWidth])
|
||||
}, [isCompact, leftPanelWidth, summaryBodyWidth])
|
||||
|
||||
const treemapItems = useMemo(() => {
|
||||
const palette = ['ocean', 'sky', 'mint', 'amber', 'rose', 'violet', 'slate']
|
||||
const maxValue = Math.max(...summaryItems.map((item) => item.value), 1)
|
||||
const allowFeaturedTile = !isCompact && treemapColumns > 1 && summaryItems.length > 2
|
||||
const allowSecondaryTallTiles = !isCompact && leftPanelWidth >= 520
|
||||
const limitedItems = distributionItems.slice(0, isCompact ? 6 : 10)
|
||||
const maxValue = Math.max(...limitedItems.map((item) => item.value), 1)
|
||||
const allowFeaturedTile = !isCompact && treemapColumns > 1 && limitedItems.length > 2
|
||||
|
||||
return summaryItems.map((item, index) => {
|
||||
return limitedItems.map((item, index) => {
|
||||
const ratio = item.value / maxValue
|
||||
let colSpan = 1
|
||||
let rowSpan = 1
|
||||
|
||||
if (allowFeaturedTile && index === 0) {
|
||||
if (allowFeaturedTile && index === 0 && ratio >= 0.35) {
|
||||
colSpan = Math.min(2, treemapColumns)
|
||||
rowSpan = 2
|
||||
} else if (allowSecondaryTallTiles && ratio >= 0.7) {
|
||||
colSpan = Math.min(2, treemapColumns)
|
||||
rowSpan = 2
|
||||
} else if (allowSecondaryTallTiles && ratio >= 0.35) {
|
||||
rowSpan = 2
|
||||
rowSpan = colSpan
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -579,7 +609,7 @@ function DataList() {
|
||||
tone: palette[index % palette.length],
|
||||
}
|
||||
})
|
||||
}, [summaryItems, isCompact, leftPanelWidth, treemapColumns])
|
||||
}, [distributionItems, isCompact, leftPanelWidth, treemapColumns])
|
||||
|
||||
const treemapRows = useMemo(
|
||||
() => estimateTreemapRows(treemapItems, treemapColumns),
|
||||
@@ -587,16 +617,15 @@ function DataList() {
|
||||
)
|
||||
|
||||
const treemapGap = isCompact ? 8 : 10
|
||||
const treemapMinRowHeight = isCompact ? 88 : 68
|
||||
const treemapTargetRowHeight = isCompact ? 88 : leftPanelWidth < 360 ? 44 : leftPanelWidth < 520 ? 48 : 56
|
||||
const treemapAvailableHeight = Math.max(summaryBodyHeight, 0)
|
||||
const treemapAutoRowHeight = treemapRows > 0
|
||||
? Math.floor((treemapAvailableHeight - Math.max(0, treemapRows - 1) * treemapGap) / treemapRows)
|
||||
: treemapTargetRowHeight
|
||||
const treemapRowHeight = Math.max(
|
||||
treemapMinRowHeight,
|
||||
Math.min(treemapTargetRowHeight, treemapAutoRowHeight || treemapTargetRowHeight)
|
||||
const treemapBaseSize = Math.max(
|
||||
isCompact ? 88 : 68,
|
||||
Math.min(
|
||||
isCompact ? 220 : 180,
|
||||
Math.floor((Math.max(summaryBodyWidth, 0) - Math.max(0, treemapColumns - 1) * treemapGap) / treemapColumns)
|
||||
) || (isCompact ? 88 : 68)
|
||||
)
|
||||
const treemapAvailableHeight = Math.max(summaryBodyHeight, 0)
|
||||
const treemapRowHeight = treemapBaseSize
|
||||
const treemapContentHeight = treemapRows * treemapRowHeight + Math.max(0, treemapRows - 1) * treemapGap
|
||||
const treemapTilePadding = treemapRowHeight <= 72 ? 8 : treemapRowHeight <= 84 ? 10 : 12
|
||||
const treemapLabelSize = treemapRowHeight <= 72 ? 10 : treemapRowHeight <= 84 ? 11 : 12
|
||||
@@ -731,6 +760,31 @@ function DataList() {
|
||||
styles={{ body: { padding: isCompact ? 12 : 16 } }}
|
||||
>
|
||||
<div ref={summaryBodyRef} className="data-list-summary-card-inner">
|
||||
<div className="data-list-summary-kpis">
|
||||
{summaryKpis.map((item) => (
|
||||
<div key={item.key} className="data-list-summary-kpi">
|
||||
<div className="data-list-summary-kpi__head">
|
||||
<span className="data-list-summary-tile-icon">{item.icon}</span>
|
||||
<Text className="data-list-treemap-label">{item.label}</Text>
|
||||
</div>
|
||||
<Text strong className="data-list-summary-tile-value">
|
||||
{item.value.toLocaleString()}
|
||||
</Text>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="data-list-summary-section-head">
|
||||
<Text strong>分布概览</Text>
|
||||
<Segmented
|
||||
size="small"
|
||||
value={treemapDimension}
|
||||
onChange={(value) => setTreemapDimension(value as 'source' | 'type')}
|
||||
options={[
|
||||
{ label: '按数据源', value: 'source' },
|
||||
{ label: '按类型', value: 'type' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="data-list-summary-treemap"
|
||||
style={{
|
||||
@@ -744,7 +798,7 @@ function DataList() {
|
||||
['--data-list-treemap-value-size' as '--data-list-treemap-value-size']: `${treemapValueSize}px`,
|
||||
} as CSSProperties}
|
||||
>
|
||||
{treemapItems.map((item) => (
|
||||
{treemapItems.length > 0 ? treemapItems.map((item) => (
|
||||
<div
|
||||
key={item.key}
|
||||
className={`data-list-treemap-tile data-list-treemap-tile--${item.tone}`}
|
||||
@@ -763,7 +817,11 @@ function DataList() {
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
)) : (
|
||||
<div className="data-list-summary-empty">
|
||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无分布数据" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -144,7 +144,7 @@ function Users() {
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>添加用户</Button>
|
||||
</div>
|
||||
<div className="page-shell__body">
|
||||
<div ref={tableRegionRef} className="table-scroll-region data-source-table-region" style={{ height: '100%' }}>
|
||||
<div ref={tableRegionRef} className="table-scroll-region data-source-table-region users-table-region" style={{ height: '100%' }}>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={users}
|
||||
|
||||
Reference in New Issue
Block a user