release: bump version to 0.39.0

This commit is contained in:
rayd1o
2026-04-24 00:48:33 +08:00
parent d5f3784ffb
commit 8b8f7138c0
22 changed files with 1490 additions and 762 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "planet-frontend",
"version": "0.38.0",
"version": "0.39.0",
"private": true,
"packageManager": "bun@1",
"dependencies": {

View File

@@ -634,6 +634,15 @@
gap: 12px;
}
.earth-mobile-drawer-slot--situation,
.earth-mobile-page,
.earth-mobile-stats-grid,
.earth-mobile-situation-card,
.earth-mobile-situation-legend-list {
width: 100%;
min-width: 0;
}
.earth-mobile-drawer-slot--situation.is-active {
display: grid;
}
@@ -1024,11 +1033,28 @@
display: flex;
flex-direction: column;
gap: 8px;
overflow-x: hidden;
}
.earth-mobile-situation-status {
color: var(--hud-text);
line-height: 1.5;
min-width: 0;
overflow-wrap: anywhere;
word-break: break-word;
}
.earth-mobile-situation-legend-list .legend-item,
.earth-mobile-situation-legend-list .legend-label {
min-width: 0;
}
.earth-mobile-situation-legend-list .legend-label {
white-space: normal;
overflow: visible;
text-overflow: clip;
overflow-wrap: anywhere;
word-break: break-word;
}
.earth-mobile-news-focus,

View File

@@ -137,6 +137,11 @@ function getMobileDetailRenderKey(type, data) {
].join('|');
}
function isMobileDetailsDrawerActive() {
const detailsSlot = document.querySelector('[data-drawer-slot="details"]');
return detailsSlot instanceof HTMLElement && detailsSlot.classList.contains('is-active');
}
function ensureMobileDetailsListener() {
if (mobileDetailsListenerBound) return;
mobileDetailsListenerBound = true;
@@ -836,6 +841,9 @@ export function showInfoCard(type, data, options = {}) {
if (content && type !== 'news') {
renderMobileDetailContent(type, config, data);
renderedMobileDetailKey = null;
} else if (content && type === 'news' && isMobileDetailsDrawerActive()) {
renderMobileNewsCardContent(content, data);
renderedMobileDetailKey = getMobileDetailRenderKey(type, data);
}
// Show the floating mini popup near the touch point (requires coordinates)

View File

@@ -3417,7 +3417,7 @@ body {
.system-log-console {
flex: 1 1 auto;
min-height: 0;
min-height: 480px;
height: 100%;
position: relative;
border-radius: 16px;
background: #020617;
@@ -3448,13 +3448,18 @@ body {
.system-log-console__scroll,
.system-log-console__scroll .scrollbar__viewport {
height: 100%;
min-height: 480px;
}
.system-log-console__scroll,
.system-log-console__scroll .scrollbar__viewport,
.system-log-console__content,
.system-log-console__placeholder {
min-height: 100%;
}
.system-log-console__content {
margin: 0;
padding: 18px 88px 18px 20px;
min-height: 480px;
padding: 18px 124px 18px 20px;
color: #e2e8f0;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 12px;
@@ -3464,7 +3469,6 @@ body {
}
.system-log-console__placeholder {
min-height: 480px;
display: flex;
align-items: center;
justify-content: center;
@@ -3472,14 +3476,24 @@ body {
}
.logs-page {
--logs-filter-toggle-size: 32px;
height: 100%;
min-height: 0;
gap: 10px;
}
.logs-page__header-copy {
min-width: 0;
}
.logs-page .page-shell__header {
gap: 8px;
}
.logs-page__header-desc {
line-height: 1.4;
}
.logs-page__card {
flex: 1 1 auto;
min-height: 0;
@@ -3492,6 +3506,7 @@ body {
min-height: 0;
display: flex;
flex-direction: column;
padding: 14px;
}
.logs-page__card-body {
@@ -3499,36 +3514,27 @@ body {
min-height: 0;
display: flex;
flex-direction: column;
gap: 14px;
gap: 12px;
overflow: hidden;
}
.logs-page__summary-card {
flex: 0 0 auto;
padding: 12px 14px;
border-radius: 14px;
background: linear-gradient(180deg, #ffffff 0%, #fafafa 100%);
border: 1px solid rgba(5, 5, 5, 0.06);
}
.logs-page__summary-header {
.logs-page__console-shell {
flex: 1 1 auto;
min-height: 0;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 6px;
flex-direction: column;
}
.logs-page__toolbar {
flex: 0 0 auto;
display: flex;
flex-direction: column;
gap: 10px;
padding: 14px 16px;
gap: 8px;
padding: 10px 12px;
border-radius: 16px;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.98) 0%, rgba(245, 247, 250, 0.98) 100%);
border: 1px solid rgba(5, 5, 5, 0.08);
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.05);
background: linear-gradient(135deg, rgba(255, 255, 255, 0.98) 0%, rgba(247, 249, 252, 0.98) 100%);
border: 1px solid rgba(5, 5, 5, 0.07);
box-shadow: 0 8px 18px rgba(15, 23, 42, 0.04);
}
.logs-page__toolbar-row {
@@ -3540,7 +3546,9 @@ body {
}
.logs-page__toolbar-row--primary {
flex-wrap: wrap;
display: grid;
grid-template-columns: minmax(180px, 220px) minmax(220px, 280px) minmax(280px, 1fr) var(--logs-filter-toggle-size);
align-items: center;
}
.logs-page__source-select {
@@ -3549,12 +3557,13 @@ body {
}
.logs-page__search-input {
width: min(560px, 100%);
min-width: 220px;
width: 100%;
min-width: 240px;
}
.logs-page__level-select {
width: 220px;
width: 100%;
min-width: 220px;
}
.logs-page__date-range {
@@ -3565,15 +3574,62 @@ body {
width: 156px;
}
.logs-page__toolbar-row--search .logs-page__search-input {
flex: 1 1 auto;
.logs-page__filter-toggle {
display: inline-flex;
justify-content: center;
align-items: center;
align-self: center;
width: var(--logs-filter-toggle-size);
height: var(--logs-filter-toggle-size);
padding: 0;
border: 1px solid rgba(5, 5, 5, 0.08);
border-radius: 8px;
background: transparent;
color: rgba(0, 0, 0, 0.65);
cursor: pointer;
transition: color 0.18s ease, border-color 0.18s ease, background 0.18s ease;
}
.logs-page__filter-toggle:hover {
color: rgba(0, 0, 0, 0.88);
border-color: rgba(5, 5, 5, 0.16);
background: rgba(0, 0, 0, 0.02);
}
.logs-page__filter-toggle.is-expanded {
color: #1677ff;
border-color: rgba(22, 119, 255, 0.28);
background: rgba(22, 119, 255, 0.06);
}
.logs-page__filters-panel {
border-top: 1px solid rgba(5, 5, 5, 0.06);
padding-top: 8px;
}
.logs-page__toolbar-row--secondary {
display: grid;
grid-template-columns: 156px minmax(260px, 320px) minmax(0, 1fr);
align-items: start;
}
.logs-page__preset-group {
display: flex;
align-self: center;
align-items: center;
flex-wrap: wrap;
justify-content: flex-start;
}
.logs-page__preset-group .ant-space-item {
display: flex;
align-items: center;
}
.logs-page__preset-group .ant-btn {
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 999px;
}
@@ -3620,6 +3676,74 @@ body {
border-top: 1px solid rgba(5, 5, 5, 0.06);
}
@media (max-width: 1440px), (max-height: 900px) {
.logs-page {
gap: 8px;
}
.logs-page__header-desc {
display: none;
}
.logs-page__card .ant-card-body {
padding: 12px;
}
.logs-page__toolbar {
padding: 8px 10px;
gap: 6px;
}
.logs-page__toolbar-row--primary {
grid-template-columns: minmax(160px, 200px) minmax(180px, 220px) minmax(0, 1fr) var(--logs-filter-toggle-size);
grid-template-areas:
"source level search toggle";
row-gap: 8px;
}
.logs-page__source-select,
.logs-page__line-limit-select,
.logs-page__level-select,
.logs-page__search-input {
width: 100%;
min-width: 0;
}
.logs-page__source-select {
grid-area: source;
}
.logs-page__level-select {
grid-area: level;
}
.logs-page__search-input {
grid-area: search;
}
.logs-page__filter-toggle {
grid-area: toggle;
}
.logs-page__toolbar-row--secondary {
grid-template-columns: 140px minmax(240px, 280px) minmax(0, 1fr);
gap: 8px;
}
.logs-page__date-range {
width: 100%;
min-width: 0;
}
.logs-page__console-shell {
min-height: clamp(340px, 58vh, 760px);
}
.system-log-console__content {
padding: 16px 112px 16px 16px;
}
}
@media (max-width: 768px) {
.dashboard-restart-toolbar__meta {
grid-template-columns: 1fr;
@@ -3630,8 +3754,14 @@ body {
align-items: stretch;
}
.logs-page__toolbar-row--primary {
flex-wrap: wrap;
.logs-page__card .ant-card-body {
padding: 10px;
}
.logs-page__toolbar-row--primary,
.logs-page__toolbar-row--secondary {
grid-template-columns: 1fr;
align-items: stretch;
}
.logs-page__source-select,
@@ -3641,4 +3771,8 @@ body {
.logs-page__line-limit-select {
width: 100%;
}
.logs-page__console-shell {
min-height: clamp(280px, 50vh, 620px);
}
}

View File

@@ -1,6 +1,6 @@
import { useEffect, useMemo, useState } from 'react'
import { Alert, Button, Card, DatePicker, Empty, Input, InputNumber, Select, Space, Spin, Tag, Tooltip, Typography, message } from 'antd'
import { CopyOutlined, ReloadOutlined } from '@ant-design/icons'
import { CopyOutlined, DownOutlined, InfoCircleOutlined, ReloadOutlined, UpOutlined } from '@ant-design/icons'
import axios from 'axios'
import dayjs, { Dayjs } from 'dayjs'
import type { CustomTagProps } from 'rc-select/lib/BaseSelect'
@@ -104,12 +104,6 @@ function readStoredFilters() {
}
}
function getStatusTagColor(status: string): string {
if (status === 'ok') return 'success'
if (status === 'missing') return 'warning'
return 'default'
}
function getStatusLabel(status: string): string {
if (status === 'ok') return '可用'
if (status === 'missing') return '暂无日志'
@@ -173,6 +167,12 @@ function Logs() {
const [sourcesLoading, setSourcesLoading] = useState(false)
const [logLoading, setLogLoading] = useState(false)
const [errorMessage, setErrorMessage] = useState<string | null>(null)
const [filtersExpanded, setFiltersExpanded] = useState(
Boolean(
storedFilters?.selectedLevels?.length
|| (storedFilters?.selectedDateRange?.[0] && storedFilters?.selectedDateRange?.[1]),
),
)
const [messageApi, contextHolder] = message.useMessage()
const fetchSources = async () => {
@@ -279,6 +279,7 @@ function Logs() {
const currentResultLines = snapshot?.lines || []
const lineCountLabel = currentResultLines.length
const hasDateFilter = Boolean(selectedDateRange?.[0] && selectedDateRange?.[1])
const hasAdvancedFilters = selectedLevels.length > 0 || hasDateFilter
const effectiveLevelLabels = selectedLevels.length === 0
? ['ALL']
: normalizeSelectedLevels(selectedLevels).map(
@@ -309,8 +310,8 @@ function Logs() {
<div className="page-shell logs-page">
<div className="page-shell__header">
<div className="logs-page__header-copy">
<Title level={3} style={{ marginBottom: 4 }}></Title>
<Paragraph type="secondary" style={{ marginBottom: 0 }}>
<Title level={3} style={{ marginBottom: 2 }}></Title>
<Paragraph type="secondary" className="logs-page__header-desc" style={{ marginBottom: 0 }}>
Planet Earth
</Paragraph>
</div>
@@ -344,27 +345,6 @@ function Logs() {
tagRender={renderLevelTag}
placeholder="全部级别"
/>
<Select
value={lineLimit}
onChange={(value) => setLineLimit(Number(value))}
options={LOG_LIMIT_OPTIONS.map((value) => ({ value, label: `最近 ${value}` }))}
className="logs-page__line-limit-select"
dropdownRender={(menu) => (
<>
{menu}
<div className="logs-page__line-limit-customizer">
<Text type="secondary"></Text>
<InputNumber
min={1}
max={1000}
value={lineLimit}
onChange={(value) => setLineLimit(Number(value) || 200)}
style={{ width: '100%' }}
/>
</div>
</>
)}
/>
<Input.Search
allowClear
value={searchQuery}
@@ -373,81 +353,118 @@ function Logs() {
placeholder="搜索日志内容、模块名、错误关键字"
className="logs-page__search-input"
/>
<button
type="button"
className={`logs-page__filter-toggle ${filtersExpanded ? 'is-expanded' : ''}`}
onClick={() => setFiltersExpanded((value) => !value)}
aria-expanded={filtersExpanded}
aria-label={filtersExpanded ? '收起筛选' : '展开筛选'}
title={hasAdvancedFilters ? '筛选已启用' : '更多筛选'}
>
{filtersExpanded ? <UpOutlined /> : <DownOutlined />}
</button>
</div>
<div className="logs-page__toolbar-row logs-page__toolbar-row--secondary">
<RangePicker
allowClear
value={selectedDateRange}
onChange={(value) => setSelectedDateRange(normalizeDateRange(value as [Dayjs | null, Dayjs | null] | null))}
cellRender={(current, info) => {
if (info.type !== 'date' || !isDayjsValue(current)) return info.originNode
{filtersExpanded ? (
<div className="logs-page__filters-panel">
<div className="logs-page__toolbar-row logs-page__toolbar-row--secondary">
<Select
value={lineLimit}
onChange={(value) => setLineLimit(Number(value))}
options={LOG_LIMIT_OPTIONS.map((value) => ({ value, label: `最近 ${value}` }))}
className="logs-page__line-limit-select"
popupRender={(menu) => (
<>
{menu}
<div className="logs-page__line-limit-customizer">
<Text type="secondary"></Text>
<InputNumber
min={1}
max={1000}
value={lineLimit}
onChange={(value) => setLineLimit(Number(value) || 200)}
style={{ width: '100%' }}
/>
</div>
</>
)}
/>
<RangePicker
allowClear
value={selectedDateRange}
onChange={(value) => setSelectedDateRange(normalizeDateRange(value as [Dayjs | null, Dayjs | null] | null))}
cellRender={(current, info) => {
if (info.type !== 'date' || !isDayjsValue(current)) return info.originNode
const marker = dailyLogMarkers.get(current.format('YYYY-MM-DD'))
if (!marker) return info.originNode
const marker = dailyLogMarkers.get(current.format('YYYY-MM-DD'))
if (!marker) return info.originNode
return (
<div
className={`logs-page__calendar-cell logs-page__calendar-cell--${marker.dominantLevel}`}
title={`${current.format('YYYY-MM-DD')} · ${marker.total} lines`}
return (
<div
className={`logs-page__calendar-cell logs-page__calendar-cell--${marker.dominantLevel}`}
title={`${current.format('YYYY-MM-DD')} · ${marker.total} lines`}
>
{info.originNode}
</div>
)
}}
format="YYYY-MM-DD"
placeholder={['开始日期', '结束日期']}
className="logs-page__date-range"
/>
<Space size={6} className="logs-page__preset-group">
{DATE_PRESET_OPTIONS.map((option) => (
<Button
key={option.key}
size="small"
type={activeDatePreset === option.key ? 'primary' : 'default'}
onClick={() => applyDatePreset(option.days)}
>
{option.label}
</Button>
))}
<Button
size="small"
onClick={() => setSelectedDateRange(null)}
disabled={!hasDateFilter}
>
{info.originNode}
</div>
)
}}
format="YYYY-MM-DD"
placeholder={['开始日期', '结束日期']}
className="logs-page__date-range"
/>
<Space size={6} className="logs-page__preset-group">
{DATE_PRESET_OPTIONS.map((option) => (
<Button
key={option.key}
size="small"
type={activeDatePreset === option.key ? 'primary' : 'default'}
onClick={() => applyDatePreset(option.days)}
>
{option.label}
</Button>
))}
<Button
size="small"
onClick={() => setSelectedDateRange(null)}
disabled={!hasDateFilter}
>
Clear
</Button>
</Space>
</div>
Clear
</Button>
</Space>
</div>
</div>
) : null}
</div>
<div className="logs-page__summary-card">
<div className="logs-page__summary-header">
<Space wrap size={8}>
<Text strong>{snapshot?.name || selectedMeta?.name || '未选择日志源'}</Text>
<Tag color={getStatusTagColor(snapshot?.status || selectedMeta?.status || 'default')}>
{getStatusLabel(snapshot?.status || selectedMeta?.status || 'default')}
</Tag>
<Tag>{(snapshot?.kind || selectedMeta?.kind || 'unknown').toUpperCase()}</Tag>
<Text type="secondary"> {lineCountLabel} </Text>
</Space>
</div>
<Space direction="vertical" size={4} style={{ width: '100%' }}>
<Text type="secondary">{snapshot?.description || selectedMeta?.description || '-'}</Text>
<Text type="secondary">
: {snapshot?.location || selectedMeta?.location || '-'}
{selectedLevels.length > 0 ? ` · 级别: ${effectiveLevelLabels.join(' / ')}` : ''}
{selectedDateRange?.[0] && selectedDateRange?.[1]
? ` · 日期: ${selectedDateRange[0].format('YYYY-MM-DD')} ~ ${selectedDateRange[1].format('YYYY-MM-DD')}`
: ''}
{searchQuery.trim() ? ` · 检索: ${searchQuery.trim()}` : ''}
</Text>
{statusHelp ? <Alert type="info" showIcon message={statusHelp} /> : null}
</Space>
</div>
<div className="system-log-console">
<div className="logs-page__console-shell">
<div className="system-log-console">
<div className="system-log-console__actions">
<Tooltip
title={
<>
<div><strong>{snapshot?.name || selectedMeta?.name || '未选择日志源'}</strong></div>
<div>: {getStatusLabel(snapshot?.status || selectedMeta?.status || 'default')}</div>
<div>: {(snapshot?.kind || selectedMeta?.kind || 'unknown').toUpperCase()}</div>
<div>: {lineCountLabel} </div>
{selectedLevels.length > 0 ? <div>: {effectiveLevelLabels.join(' / ')}</div> : null}
{selectedDateRange?.[0] && selectedDateRange?.[1]
? <div>: {selectedDateRange[0].format('YYYY-MM-DD')} ~ {selectedDateRange[1].format('YYYY-MM-DD')}</div>
: null}
{searchQuery.trim() ? <div>: {searchQuery.trim()}</div> : null}
<div>{snapshot?.description || selectedMeta?.description || '-'}</div>
<div>: {snapshot?.location || selectedMeta?.location || '-'}</div>
{statusHelp ? <div>{statusHelp}</div> : null}
</>
}
>
<Button
type="text"
size="small"
shape="circle"
icon={<InfoCircleOutlined />}
className="playground-message__actions-btn"
/>
</Tooltip>
<Tooltip title="刷新日志">
<Button
type="text"
@@ -503,6 +520,7 @@ function Logs() {
/>
</div>
)}
</div>
</div>
</div>
</Card>