From 8f3ab8874392422a62b6b66e157f35991c510b5a Mon Sep 17 00:00:00 2001 From: linkong Date: Thu, 16 Apr 2026 10:04:14 +0800 Subject: [PATCH] release: bump version to 0.27.7 --- VERSION | 2 +- docs/CHANGELOG.md | 15 + docs/version-history.md | 3 +- frontend/package.json | 2 +- frontend/public/earth/js/tv.js | 67 ++- .../components/TableActions/TableActions.tsx | 26 + frontend/src/hooks/index.ts | 1 + frontend/src/hooks/useCollapsedActions.ts | 39 ++ frontend/src/index.css | 37 +- .../src/pages/DataSources/DataSources.tsx | 99 +++- frontend/src/pages/Settings/Settings.tsx | 479 ++++++++++-------- frontend/src/pages/Users/Users.tsx | 29 +- pyproject.toml | 2 +- 13 files changed, 532 insertions(+), 269 deletions(-) create mode 100644 frontend/src/components/TableActions/TableActions.tsx create mode 100644 frontend/src/hooks/useCollapsedActions.ts diff --git a/VERSION b/VERSION index c7c49b4b..fb96d142 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.27.6 +0.27.7 diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index aa35d78e..a362a008 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -8,6 +8,21 @@ This project follows the repository versioning rule: - `improvement` -> `+0.0.1`(bugfix + 小功能混合) - `bugfix` -> `+0.0.1` +## [0.27.7] — 2026-04-16 + +### 🔧 Improvements +- 用户管理、数据源配置、电视直播源表格统一接入可折叠操作列,窄宽度下自动收起到下拉菜单,减少操作区挤压 +- 电视直播设置改为表格总览 + 弹窗编辑模式,主表内容更紧凑,适合控制台一屏浏览 +- Earth TV 面板新增失败源探测与自动回退恢复标记,便于值班时快速识别异常直播源 + +### 🐛 Fixes +- 修复 Settings 电视直播源新增后取消编辑会残留未保存草稿的问题 +- 修复 Settings 删除直播源只改本地状态、刷新后恢复的问题,删除现在会立即持久化 +- 修复 Users / DataSources / Settings 表格“备注/状态”和“操作”之间的空白占位列问题 +- 修复 `useCollapsedActions` 未释放 `ResizeObserver` 导致的潜在内存泄漏与重复回调问题 + +--- + ## [0.27.6] — 2026-04-15 ### 🔧 Improvements diff --git a/docs/version-history.md b/docs/version-history.md index c1de6049..33c2b468 100644 --- a/docs/version-history.md +++ b/docs/version-history.md @@ -16,12 +16,13 @@ ## Current Version - `main` 当前主线历史推导到:`0.16.5` -- `dev` 当前开发分支历史推导到:`0.27.6` +- `dev` 当前开发分支历史推导到:`0.27.7` ## Timeline | Version | Type | Branch | Commit | Summary | | --- | --- | --- | --- | --- | +| `0.27.7` | bugfix | `dev` | `pending` | 修复电视直播源编辑持久化问题,清理表格空白占位列并统一可折叠操作列 | | `0.27.6` | improvement | `dev` | `pending` | BGP/用户表格滚动条修复,Playground 响应式按钮与输入框收起优化 | | `0.27.5` | bugfix | `dev` | `pending` | 统一控制台自定义滚动条,修复 alerts/BGP 响应式滚动与采集进度完成态显示 | | `0.27.4` | improvement | `dev` | — | info-card 懒加载动态挂载,页面初始不再有隐藏节点 | diff --git a/frontend/package.json b/frontend/package.json index 1b4946f2..4628325b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "planet-frontend", - "version": "0.27.6", + "version": "0.27.7", "private": true, "packageManager": "bun@1", "dependencies": { diff --git a/frontend/public/earth/js/tv.js b/frontend/public/earth/js/tv.js index e53fb195..17a68e8c 100644 --- a/frontend/public/earth/js/tv.js +++ b/frontend/public/earth/js/tv.js @@ -21,8 +21,11 @@ let refreshPromise = null; let hlsPlayer = null; let hlsRecoveryAttempts = 0; let metaAutoCollapseTimer = null; +const failedSourceIds = new Set(); +let probeTimer = null; const META_AUTO_COLLAPSE_DELAY = 2500; +const PROBE_INTERVAL_MS = 2 * 60 * 1000; const HLS_MAX_RECOVERY_ATTEMPTS = 3; const HLS_RETRY_CONFIG = { @@ -392,7 +395,7 @@ function attachVideoSource(video, source) { } } - if (!showEmbeddedFallback(source)) { + if (!showEmbeddedFallback(source) && !tryFallbackSource()) { setPanelMessage(TV_STATUS_MESSAGE.videoError); } }); @@ -425,6 +428,59 @@ function findSourceById(sourceId) { return tvPayload?.sources?.find((source) => source.id === sourceId) || null; } +function markSourceFailed(sourceId) { + if (!sourceId) return; + failedSourceIds.add(sourceId); + renderSourceOptions(); + if (!probeTimer) { + probeTimer = setInterval(probeFailedSources, PROBE_INTERVAL_MS); + } +} + +function clearSourceFailed(sourceId) { + if (!failedSourceIds.has(sourceId)) return; + failedSourceIds.delete(sourceId); + renderSourceOptions(); + if (failedSourceIds.size === 0 && probeTimer) { + clearInterval(probeTimer); + probeTimer = null; + } +} + +async function probeFailedSources() { + if (failedSourceIds.size === 0) { + clearInterval(probeTimer); + probeTimer = null; + return; + } + for (const sourceId of [...failedSourceIds]) { + const source = findSourceById(sourceId); + if (!source) { failedSourceIds.delete(sourceId); continue; } + const probeUrl = source.stream_url || source.embed_url; + if (!probeUrl) continue; + try { + const resp = await fetch(probeUrl, { + method: "HEAD", + signal: AbortSignal.timeout(5000), + }); + if (resp.ok) clearSourceFailed(sourceId); + } catch { + // 仍然失效,保持标记 + } + } +} + +function tryFallbackSource() { + const fallback = tvPayload?.fallback_source; + if (!fallback || fallback.id === currentSourceId) return false; + markSourceFailed(currentSourceId); + currentSourceId = fallback.id; + const { select } = getElements(); + if (select) select.value = currentSourceId; + renderSource(fallback); + return true; +} + function getCurrentSource() { return findSourceById(currentSourceId); } @@ -487,10 +543,11 @@ function renderSourceOptions() { const fragment = document.createDocumentFragment(); sources.forEach((source) => { - const marker = source.id === tvPayload?.default_source_id ? " · 默认" : ""; + const defaultMark = source.id === tvPayload?.default_source_id ? " · 默认" : ""; + const failMark = failedSourceIds.has(source.id) ? " ⚠" : ""; const option = document.createElement("option"); option.value = source.id; - option.textContent = `${source.name}${marker}`; + option.textContent = `${source.name}${defaultMark}${failMark}`; fragment.appendChild(option); }); @@ -660,17 +717,19 @@ export function initTVPanel() { iframe?.addEventListener("load", () => { if (iframe.hidden) return; + clearSourceFailed(currentSourceId); setPanelMessage(TV_STATUS_MESSAGE.iframeReady); }); video?.addEventListener("loadedmetadata", () => { if (video.hidden) return; + clearSourceFailed(currentSourceId); setPanelMessage(TV_STATUS_MESSAGE.videoReady); }); video?.addEventListener("error", () => { const currentSource = getCurrentSource(); - if (!showEmbeddedFallback(currentSource)) { + if (!showEmbeddedFallback(currentSource) && !tryFallbackSource()) { setPanelMessage(TV_STATUS_MESSAGE.videoError); } }); diff --git a/frontend/src/components/TableActions/TableActions.tsx b/frontend/src/components/TableActions/TableActions.tsx new file mode 100644 index 00000000..cf7e19f0 --- /dev/null +++ b/frontend/src/components/TableActions/TableActions.tsx @@ -0,0 +1,26 @@ +import type { MenuProps } from 'antd' +import type { ReactNode } from 'react' +import { Button, Dropdown } from 'antd' +import { MoreOutlined } from '@ant-design/icons' + +interface Props { + collapsed: boolean + items: MenuProps['items'] + children: ReactNode +} + +/** onCell style for action columns — prevents overflow ellipsis and text wrapping */ +export const actionCellProps = { + style: { whiteSpace: 'nowrap' as const, textOverflow: 'clip' as const }, +} + +export function TableActions({ collapsed, items, children }: Props) { + if (collapsed) { + return ( + + - + ), }, ] @@ -1001,30 +1024,56 @@ function DataSources() { { title: '操作', key: 'action', - width: 150, fixed: 'right' as const, + width: customActionsCollapsed ? 40 : 228, + onCell: () => actionCellProps, render: (_: unknown, record: CustomDataSource) => ( - - - + + handleDelete(record.id)}> + - + ), }, ] @@ -1034,7 +1083,7 @@ function DataSources() { key: 'builtin', label: '内置数据源', children: ( -
+
采集实时进度
@@ -1120,7 +1169,7 @@ function DataSources() { ), children: ( -
+
+ , + disabled: record.id === tvSettings?.default_source_id, + onClick: () => setDefaultSource(record.id), + }, + { + key: 'edit', + label: '编辑', + icon: , + onClick: () => { + setEditingSource(record) + tvEditForm.setFieldsValue(record) + }, + }, + { type: 'divider' }, + { + key: 'delete', + label: '删除', + icon: , + danger: true, + disabled: record.id === tvSettings?.default_source_id, + onClick: () => { + void removeTvSource(record.id) + }, + }, + ]} + > + + + + ), }, ] @@ -612,51 +621,111 @@ function Settings() { key: 'tv', label: '电视直播', children: ( -
- -
-
-
-
- 默认直播源 - + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + +
+ + +
+
), }, diff --git a/frontend/src/pages/Users/Users.tsx b/frontend/src/pages/Users/Users.tsx index 9decd1d5..43f7f6b1 100644 --- a/frontend/src/pages/Users/Users.tsx +++ b/frontend/src/pages/Users/Users.tsx @@ -1,6 +1,8 @@ import { useEffect, useState } from 'react' -import { Table, Button, Tag, Space, message, Modal, Form, Input, Select } from 'antd' +import { Table, Button, Tag, message, Modal, Form, Input, Select } from 'antd' import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons' +import { useCollapsedActions } from '../../hooks' +import { TableActions, actionCellProps } from '../../components/TableActions/TableActions' import axios from 'axios' import AppLayout from '../../components/AppLayout/AppLayout' import TableScrollRegion from '../../components/Scrollbar/TableScrollRegion' @@ -20,6 +22,7 @@ function Users() { const [modalVisible, setModalVisible] = useState(false) const [editingUser, setEditingUser] = useState(null) const [form] = Form.useForm() + const [actionsCollapsed, containerRef] = useCollapsedActions() const fetchUsers = async () => { setLoading(true) @@ -107,12 +110,21 @@ function Users() { { title: '操作', key: 'action', - width: 180, + fixed: 'right' as const, + width: actionsCollapsed ? 56 : 172, + onCell: () => actionCellProps, render: (_: unknown, record: User) => ( - - - - + , onClick: () => handleEdit(record) }, + { type: 'divider' }, + { key: 'delete', label: '删除', icon: , danger: true, onClick: () => handleDelete(record.id) }, + ]} + > + + + ), }, ] @@ -124,15 +136,16 @@ function Users() {

用户管理

-
+
diff --git a/pyproject.toml b/pyproject.toml index 49b5095c..9d5d02ee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "planet" -version = "0.27.6" +version = "0.27.7" description = "智能星球计划 - 态势感知系统" requires-python = ">=3.14" dependencies = [