feat: refine collected data overview and admin navigation

This commit is contained in:
linkong
2026-03-27 15:08:45 +08:00
parent a761dfc5fb
commit 3dd210a3e5
9 changed files with 310 additions and 75 deletions

View File

@@ -1 +1 @@
0.21.4-dev 0.21.5-dev

View File

@@ -214,21 +214,35 @@ async def list_collected_data(
@router.get("/summary") @router.get("/summary")
async def get_data_summary( async def get_data_summary(
mode: str = Query("current", description="查询模式: current/history"), mode: str = Query("current", description="查询模式: current/history"),
source: Optional[str] = Query(None, description="数据源过滤"),
data_type: Optional[str] = Query(None, description="数据类型过滤"),
country: Optional[str] = Query(None, description="国家过滤"),
search: Optional[str] = Query(None, description="搜索名称"),
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
): ):
"""获取数据汇总统计""" """获取数据汇总统计"""
where_sql = "WHERE COALESCE(is_current, TRUE) = TRUE" if mode != "history" else "" where_sql, params = build_where_clause(source, data_type, country, search)
if mode != "history":
where_sql = f"({where_sql}) AND COALESCE(is_current, TRUE) = TRUE"
overall_where_sql = "COALESCE(is_current, TRUE) = TRUE" if mode != "history" else "1=1"
overall_total_result = await db.execute(
text(f"SELECT COUNT(*) FROM collected_data WHERE {overall_where_sql}")
)
overall_total = overall_total_result.scalar() or 0
# By source and data_type # By source and data_type
result = await db.execute( result = await db.execute(
text(""" text(f"""
SELECT source, data_type, COUNT(*) as count SELECT source, data_type, COUNT(*) as count
FROM collected_data FROM collected_data
""" + where_sql + """ WHERE {where_sql}
GROUP BY source, data_type GROUP BY source, data_type
ORDER BY source, data_type ORDER BY source, data_type
""") """),
params,
) )
rows = result.fetchall() rows = result.fetchall()
source_name_map = await get_source_name_map(db) source_name_map = await get_source_name_map(db)
@@ -248,18 +262,32 @@ async def get_data_summary(
# Total by source # Total by source
source_totals = await db.execute( source_totals = await db.execute(
text(""" text(f"""
SELECT source, COUNT(*) as count SELECT source, COUNT(*) as count
FROM collected_data FROM collected_data
""" + where_sql + """ WHERE {where_sql}
GROUP BY source GROUP BY source
ORDER BY count DESC ORDER BY count DESC
""") """),
params,
) )
source_rows = source_totals.fetchall() source_rows = source_totals.fetchall()
type_totals = await db.execute(
text(f"""
SELECT data_type, COUNT(*) as count
FROM collected_data
WHERE {where_sql}
GROUP BY data_type
ORDER BY count DESC, data_type
"""),
params,
)
type_rows = type_totals.fetchall()
return { return {
"total_records": total, "total_records": total,
"overall_total_records": overall_total,
"by_source": by_source, "by_source": by_source,
"source_totals": [ "source_totals": [
{ {
@@ -269,6 +297,13 @@ async def get_data_summary(
} }
for row in source_rows for row in source_rows
], ],
"type_totals": [
{
"data_type": row[0],
"count": row[1],
}
for row in type_rows
],
} }

View File

@@ -7,6 +7,32 @@ This project follows the repository versioning rule:
- `feature` -> `+0.1.0` - `feature` -> `+0.1.0`
- `bugfix` -> `+0.0.1` - `bugfix` -> `+0.0.1`
## 0.21.5
Released: 2026-03-27
### Highlights
- Reworked the collected-data overview into a clearer split between KPI cards and a switchable treemap distribution.
- Added a direct Earth entry on the dashboard and tightened several admin-side scrolling/layout behaviors.
### Added
- Added a dashboard quick-access card linking directly to `/earth`.
- Added collected-data treemap switching between `按数据源` and `按类型`.
- Added data-type-specific icons for the collected-data overview treemap.
### Improved
- Improved collected-data summary behavior so overview counts follow the active filters and search state.
- Improved the collected-data treemap with square tiles, a wider default overview panel width, and narrower overview scrollbars.
- Improved responsive behavior near the tablet breakpoint so the collected-data page can scroll instead of clipping the overview or crushing the table.
### Fixed
- Fixed the user-management table overflow issue by restoring `ant-table-body` to auto height for that page so the outer container no longer incorrectly takes over vertical scrolling.
- Fixed overly wide scrollbar presentation in collected-data and related admin surfaces by aligning them with the slimmer in-app scrollbar style.
## 0.21.3 ## 0.21.3
Released: 2026-03-27 Released: 2026-03-27

View File

@@ -1,12 +1,12 @@
{ {
"name": "planet-frontend", "name": "planet-frontend",
"version": "1.0.0", "version": "0.21.5-dev",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "planet-frontend", "name": "planet-frontend",
"version": "1.0.0", "version": "0.21.5-dev",
"dependencies": { "dependencies": {
"@ant-design/icons": "^5.2.6", "@ant-design/icons": "^5.2.6",
"antd": "^5.12.5", "antd": "^5.12.5",

View File

@@ -1,6 +1,6 @@
{ {
"name": "planet-frontend", "name": "planet-frontend",
"version": "0.21.4-dev", "version": "0.21.5-dev",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@ant-design/icons": "^5.2.6", "@ant-design/icons": "^5.2.6",

View File

@@ -173,7 +173,6 @@ body {
flex: 1 1 auto; flex: 1 1 auto;
min-width: 0; min-width: 0;
min-height: 0; min-height: 0;
height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;
@@ -320,6 +319,10 @@ body {
min-height: 0; min-height: 0;
} }
.users-table-region .ant-table-body {
height: auto !important;
}
.data-source-table-region .ant-table-wrapper, .data-source-table-region .ant-table-wrapper,
.data-source-table-region .ant-spin-nested-loading, .data-source-table-region .ant-spin-nested-loading,
.data-source-table-region .ant-spin-container { .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-body::-webkit-scrollbar,
.table-scroll-region .ant-table-content::-webkit-scrollbar { .table-scroll-region .ant-table-content::-webkit-scrollbar {
width: 10px; width: 8px;
height: 10px; height: 8px;
} }
.table-scroll-region .ant-table-body::-webkit-scrollbar-thumb, .table-scroll-region .ant-table-body::-webkit-scrollbar-thumb,
@@ -439,6 +442,32 @@ body {
background: transparent; 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-shell,
.settings-tabs-shell, .settings-tabs-shell,
.settings-tabs, .settings-tabs,
@@ -591,6 +620,8 @@ body {
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
scrollbar-gutter: stable; scrollbar-gutter: stable;
scrollbar-width: thin;
scrollbar-color: rgba(148, 163, 184, 0.82) transparent;
} }
.data-list-summary-card .ant-card-head, .data-list-summary-card .ant-card-head,
@@ -604,6 +635,39 @@ body {
.data-list-summary-card-inner { .data-list-summary-card-inner {
min-height: 100%; 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 { .data-list-right-column {
@@ -719,12 +783,24 @@ body {
color: rgba(15, 23, 42, 0.72) !important; 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 { .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 { .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-radius: 999px;
border: 2px solid transparent; border: 2px solid transparent;
background-clip: padding-box; background-clip: padding-box;
@@ -927,6 +1003,13 @@ body {
gap: 10px; gap: 10px;
} }
.data-list-controls-shell {
overflow-y: auto;
overflow-x: hidden;
min-height: 0;
scrollbar-gutter: stable;
}
.data-list-topbar { .data-list-topbar {
align-items: flex-start; align-items: flex-start;
flex-direction: column; flex-direction: column;
@@ -945,11 +1028,22 @@ body {
height: auto; 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 { .data-list-summary-treemap {
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
grid-auto-rows: minmax(88px, 1fr); grid-auto-rows: minmax(88px, 1fr);
} }
.data-list-summary-kpis {
grid-template-columns: 1fr;
}
.data-list-filter-grid { .data-list-filter-grid {
flex-wrap: wrap; flex-wrap: wrap;
} }
@@ -974,6 +1068,11 @@ body {
min-width: 100%; min-width: 100%;
} }
.data-list-summary-section-head {
align-items: flex-start;
flex-direction: column;
}
} }
.data-list-detail-modal { .data-list-detail-modal {

View File

@@ -4,10 +4,12 @@ import {
DatabaseOutlined, DatabaseOutlined,
BarChartOutlined, BarChartOutlined,
AlertOutlined, AlertOutlined,
GlobalOutlined,
WifiOutlined, WifiOutlined,
DisconnectOutlined, DisconnectOutlined,
ReloadOutlined, ReloadOutlined,
} from '@ant-design/icons' } from '@ant-design/icons'
import { Link } from 'react-router-dom'
import { useAuthStore } from '../../stores/auth' import { useAuthStore } from '../../stores/auth'
import AppLayout from '../../components/AppLayout/AppLayout' import AppLayout from '../../components/AppLayout/AppLayout'
import { formatDateTimeZhCN } from '../../utils/datetime' import { formatDateTimeZhCN } from '../../utils/datetime'
@@ -169,6 +171,21 @@ function Dashboard() {
</Row> </Row>
<Row gutter={[16, 16]}> <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}> <Col xs={24} md={8}>
<Card> <Card>
<Statistic title="严重告警" value={stats?.alerts?.critical || 0} valueStyle={{ color: '#ff4d4f' }} prefix={<AlertOutlined />} /> <Statistic title="严重告警" value={stats?.alerts?.critical || 0} valueStyle={{ color: '#ff4d4f' }} prefix={<AlertOutlined />} />

View File

@@ -1,13 +1,14 @@
import { useEffect, useLayoutEffect, useMemo, useRef, useState, type CSSProperties } from 'react' import { useEffect, useLayoutEffect, useMemo, useRef, useState, type CSSProperties } from 'react'
import { import {
Table, Tag, Space, Card, Select, Input, Button, Table, Tag, Space, Card, Select, Input, Button, Segmented,
Modal, Spin, Empty, Tooltip, Typography, Grid Modal, Spin, Empty, Tooltip, Typography, Grid
} from 'antd' } from 'antd'
import type { ColumnsType } from 'antd/es/table' import type { ColumnsType } from 'antd/es/table'
import type { CustomTagProps } from 'rc-select/lib/BaseSelect' import type { CustomTagProps } from 'rc-select/lib/BaseSelect'
import { import {
DatabaseOutlined, GlobalOutlined, CloudServerOutlined, DatabaseOutlined, GlobalOutlined, CloudServerOutlined,
AppstoreOutlined, EyeOutlined, SearchOutlined, FilterOutlined, ReloadOutlined AppstoreOutlined, EyeOutlined, SearchOutlined, FilterOutlined, ReloadOutlined,
ApartmentOutlined, EnvironmentOutlined
} from '@ant-design/icons' } from '@ant-design/icons'
import axios from 'axios' import axios from 'axios'
import AppLayout from '../../components/AppLayout/AppLayout' import AppLayout from '../../components/AppLayout/AppLayout'
@@ -43,8 +44,10 @@ interface CollectedData {
interface Summary { interface Summary {
total_records: number total_records: number
overall_total_records: number
by_source: Record<string, Record<string, number>> by_source: Record<string, Record<string, number>>
source_totals: Array<{ source: string; source_name: string; count: number }> source_totals: Array<{ source: string; source_name: string; count: number }>
type_totals: Array<{ data_type: string; count: number }>
} }
interface SourceOption { interface SourceOption {
@@ -256,6 +259,7 @@ function DataList() {
const [tableHeaderHeight, setTableHeaderHeight] = useState(0) const [tableHeaderHeight, setTableHeaderHeight] = useState(0)
const [leftPanelWidth, setLeftPanelWidth] = useState(360) const [leftPanelWidth, setLeftPanelWidth] = useState(360)
const [summaryBodyHeight, setSummaryBodyHeight] = useState(0) const [summaryBodyHeight, setSummaryBodyHeight] = useState(0)
const [summaryBodyWidth, setSummaryBodyWidth] = useState(0)
const [data, setData] = useState<CollectedData[]>([]) const [data, setData] = useState<CollectedData[]>([])
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
@@ -271,6 +275,7 @@ function DataList() {
const [detailVisible, setDetailVisible] = useState(false) const [detailVisible, setDetailVisible] = useState(false)
const [detailData, setDetailData] = useState<CollectedData | null>(null) const [detailData, setDetailData] = useState<CollectedData | null>(null)
const [detailLoading, setDetailLoading] = useState(false) const [detailLoading, setDetailLoading] = useState(false)
const [treemapDimension, setTreemapDimension] = useState<'source' | 'type'>('source')
useEffect(() => { useEffect(() => {
const updateLayout = () => { const updateLayout = () => {
@@ -279,6 +284,7 @@ function DataList() {
setRightColumnHeight(rightColumnRef.current?.offsetHeight || 0) setRightColumnHeight(rightColumnRef.current?.offsetHeight || 0)
setTableHeaderHeight(tableHeaderRef.current?.offsetHeight || 0) setTableHeaderHeight(tableHeaderRef.current?.offsetHeight || 0)
setSummaryBodyHeight(summaryBodyRef.current?.offsetHeight || 0) setSummaryBodyHeight(summaryBodyRef.current?.offsetHeight || 0)
setSummaryBodyWidth(summaryBodyRef.current?.offsetWidth || 0)
} }
updateLayout() updateLayout()
@@ -306,7 +312,7 @@ function DataList() {
const minLeft = 260 const minLeft = 260
const minRight = 360 const minRight = 360
const maxLeft = Math.max(minLeft, mainAreaWidth - minRight - 12) 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) => { setLeftPanelWidth((current) => {
if (!hasCustomLeftWidthRef.current) { if (!hasCustomLeftWidthRef.current) {
@@ -364,7 +370,13 @@ function DataList() {
const fetchSummary = async () => { const fetchSummary = async () => {
try { 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) setSummary(res.data)
} catch (error) { } catch (error) {
console.error('Failed to fetch summary:', error) console.error('Failed to fetch summary:', error)
@@ -385,17 +397,18 @@ function DataList() {
} }
useEffect(() => { useEffect(() => {
fetchSummary()
fetchFilters() fetchFilters()
}, []) }, [])
useEffect(() => { useEffect(() => {
fetchData() fetchData()
fetchSummary()
}, [page, pageSize, sourceFilter, typeFilter]) }, [page, pageSize, sourceFilter, typeFilter])
const handleSearch = () => { const handleSearch = () => {
setPage(1) setPage(1)
fetchData() fetchData()
fetchSummary()
} }
const handleReset = () => { const handleReset = () => {
@@ -404,6 +417,7 @@ function DataList() {
setSearchText('') setSearchText('')
setPage(1) setPage(1)
setTimeout(fetchData, 0) setTimeout(fetchData, 0)
setTimeout(fetchSummary, 0)
} }
const handleViewDetail = async (id: number) => { const handleViewDetail = async (id: number) => {
@@ -431,6 +445,25 @@ function DataList() {
return iconMap[source] || <DatabaseOutlined /> 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 getSourceTagColor = (source: string) => {
const colorMap: Record<string, string> = { const colorMap: Record<string, string> = {
top500: 'geekblue', 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( const activeFilterCount = useMemo(
() => [sourceFilter.length > 0, typeFilter.length > 0, searchText.trim()].filter(Boolean).length, () => [sourceFilter.length > 0, typeFilter.length > 0, searchText.trim()].filter(Boolean).length,
[sourceFilter, typeFilter, searchText] [sourceFilter, typeFilter, searchText]
) )
const summaryItems = useMemo(() => { const summaryKpis = useMemo(
const items = [ () => [
{ key: 'total', label: '总记录', value: summary?.total_records || 0, icon: <DatabaseOutlined /> }, { key: 'total', label: '总记录', value: summary?.overall_total_records || 0, icon: <DatabaseOutlined /> },
{ key: 'result', label: '筛选结果', value: total, icon: <SearchOutlined /> }, { key: 'result', label: '筛选结果', value: total, icon: <SearchOutlined /> },
{ key: 'filters', label: '启用筛选', value: activeFilterCount, icon: <FilterOutlined /> }, { 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)) { const distributionItems = useMemo(() => {
items.push({ if (!summary) return []
if (treemapDimension === 'type') {
return summary.type_totals.map((item) => ({
key: item.data_type,
label: item.data_type,
value: item.count,
icon: getDataTypeIcon(item.data_type),
}))
}
return summary.source_totals.map((item) => ({
key: item.source, key: item.source,
label: item.source_name, label: item.source_name,
value: item.count, value: item.count,
icon: getSourceIcon(item.source), icon: getSourceIcon(item.source),
}) }))
} }, [summary, treemapDimension])
return items
}, [summary, total, activeFilterCount, isCompact, sources.length])
const treemapColumns = useMemo(() => { const treemapColumns = useMemo(() => {
if (isCompact) return 1 if (isCompact) return summaryBodyWidth >= 320 ? 2 : 1
if (leftPanelWidth < 360) return 2 if (leftPanelWidth < 360) return 2
if (leftPanelWidth < 520) return 3 if (leftPanelWidth < 520) return 3
return 4 return 4
}, [isCompact, leftPanelWidth]) }, [isCompact, leftPanelWidth, summaryBodyWidth])
const treemapItems = useMemo(() => { const treemapItems = useMemo(() => {
const palette = ['ocean', 'sky', 'mint', 'amber', 'rose', 'violet', 'slate'] const palette = ['ocean', 'sky', 'mint', 'amber', 'rose', 'violet', 'slate']
const maxValue = Math.max(...summaryItems.map((item) => item.value), 1) const limitedItems = distributionItems.slice(0, isCompact ? 6 : 10)
const allowFeaturedTile = !isCompact && treemapColumns > 1 && summaryItems.length > 2 const maxValue = Math.max(...limitedItems.map((item) => item.value), 1)
const allowSecondaryTallTiles = !isCompact && leftPanelWidth >= 520 const allowFeaturedTile = !isCompact && treemapColumns > 1 && limitedItems.length > 2
return summaryItems.map((item, index) => { return limitedItems.map((item, index) => {
const ratio = item.value / maxValue const ratio = item.value / maxValue
let colSpan = 1 let colSpan = 1
let rowSpan = 1 let rowSpan = 1
if (allowFeaturedTile && index === 0) { if (allowFeaturedTile && index === 0 && ratio >= 0.35) {
colSpan = Math.min(2, treemapColumns) colSpan = Math.min(2, treemapColumns)
rowSpan = 2 rowSpan = colSpan
} else if (allowSecondaryTallTiles && ratio >= 0.7) {
colSpan = Math.min(2, treemapColumns)
rowSpan = 2
} else if (allowSecondaryTallTiles && ratio >= 0.35) {
rowSpan = 2
} }
return { return {
@@ -579,7 +609,7 @@ function DataList() {
tone: palette[index % palette.length], tone: palette[index % palette.length],
} }
}) })
}, [summaryItems, isCompact, leftPanelWidth, treemapColumns]) }, [distributionItems, isCompact, leftPanelWidth, treemapColumns])
const treemapRows = useMemo( const treemapRows = useMemo(
() => estimateTreemapRows(treemapItems, treemapColumns), () => estimateTreemapRows(treemapItems, treemapColumns),
@@ -587,16 +617,15 @@ function DataList() {
) )
const treemapGap = isCompact ? 8 : 10 const treemapGap = isCompact ? 8 : 10
const treemapMinRowHeight = isCompact ? 88 : 68 const treemapBaseSize = Math.max(
const treemapTargetRowHeight = isCompact ? 88 : leftPanelWidth < 360 ? 44 : leftPanelWidth < 520 ? 48 : 56 isCompact ? 88 : 68,
const treemapAvailableHeight = Math.max(summaryBodyHeight, 0) Math.min(
const treemapAutoRowHeight = treemapRows > 0 isCompact ? 220 : 180,
? Math.floor((treemapAvailableHeight - Math.max(0, treemapRows - 1) * treemapGap) / treemapRows) Math.floor((Math.max(summaryBodyWidth, 0) - Math.max(0, treemapColumns - 1) * treemapGap) / treemapColumns)
: treemapTargetRowHeight ) || (isCompact ? 88 : 68)
const treemapRowHeight = Math.max(
treemapMinRowHeight,
Math.min(treemapTargetRowHeight, treemapAutoRowHeight || treemapTargetRowHeight)
) )
const treemapAvailableHeight = Math.max(summaryBodyHeight, 0)
const treemapRowHeight = treemapBaseSize
const treemapContentHeight = treemapRows * treemapRowHeight + Math.max(0, treemapRows - 1) * treemapGap const treemapContentHeight = treemapRows * treemapRowHeight + Math.max(0, treemapRows - 1) * treemapGap
const treemapTilePadding = treemapRowHeight <= 72 ? 8 : treemapRowHeight <= 84 ? 10 : 12 const treemapTilePadding = treemapRowHeight <= 72 ? 8 : treemapRowHeight <= 84 ? 10 : 12
const treemapLabelSize = treemapRowHeight <= 72 ? 10 : treemapRowHeight <= 84 ? 11 : 12 const treemapLabelSize = treemapRowHeight <= 72 ? 10 : treemapRowHeight <= 84 ? 11 : 12
@@ -731,6 +760,31 @@ function DataList() {
styles={{ body: { padding: isCompact ? 12 : 16 } }} styles={{ body: { padding: isCompact ? 12 : 16 } }}
> >
<div ref={summaryBodyRef} className="data-list-summary-card-inner"> <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 <div
className="data-list-summary-treemap" className="data-list-summary-treemap"
style={{ style={{
@@ -744,7 +798,7 @@ function DataList() {
['--data-list-treemap-value-size' as '--data-list-treemap-value-size']: `${treemapValueSize}px`, ['--data-list-treemap-value-size' as '--data-list-treemap-value-size']: `${treemapValueSize}px`,
} as CSSProperties} } as CSSProperties}
> >
{treemapItems.map((item) => ( {treemapItems.length > 0 ? treemapItems.map((item) => (
<div <div
key={item.key} key={item.key}
className={`data-list-treemap-tile data-list-treemap-tile--${item.tone}`} className={`data-list-treemap-tile data-list-treemap-tile--${item.tone}`}
@@ -763,7 +817,11 @@ function DataList() {
</Text> </Text>
</div> </div>
</div> </div>
))} )) : (
<div className="data-list-summary-empty">
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无分布数据" />
</div>
)}
</div> </div>
</div> </div>
</Card> </Card>

View File

@@ -144,7 +144,7 @@ function Users() {
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}></Button> <Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}></Button>
</div> </div>
<div className="page-shell__body"> <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 <Table
columns={columns} columns={columns}
dataSource={users} dataSource={users}