Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
40e51d5b20 | ||
|
|
93c1c1e550 | ||
|
|
48eb13b993 | ||
|
|
11179e7e67 | ||
|
|
07e26d6d5a |
120
.claude/commands/cleanup.md
Normal file
120
.claude/commands/cleanup.md
Normal file
@@ -0,0 +1,120 @@
|
||||
---
|
||||
description: 审查当前工作区未提交代码中的垃圾代码,并在不影响逻辑的前提下自动清理
|
||||
argument-hint: 可选:指定要检查的文件或目录(默认检查所有未提交修改)
|
||||
allowed-tools: ["Read", "Edit", "Bash", "Grep", "Glob"]
|
||||
---
|
||||
|
||||
# /cleanup — 垃圾代码审查与清理
|
||||
|
||||
分析当前工作区(git diff)中的未提交代码,找出并修复常见垃圾代码,**不得改变任何运行逻辑**。
|
||||
|
||||
## 检查范围
|
||||
|
||||
若 `$ARGUMENTS` 非空,则只检查指定文件/目录;否则检查所有未提交修改(`git diff HEAD`)。
|
||||
|
||||
## 审查清单
|
||||
|
||||
按优先级检查以下问题(只报告在本次 diff 中**新增或修改**的代码里存在的问题):
|
||||
|
||||
### 1. 重复逻辑 (Duplicate Logic)
|
||||
- 完全相同或高度相似的代码块在多处出现
|
||||
- 同一函数/方法被多个地方各自实现,已有公共版本未被复用
|
||||
- 相同的 DOM 查询、正则、模板字符串在同一文件重复
|
||||
|
||||
### 2. Magic Numbers / Magic Strings
|
||||
- 裸数字直接参与计算(如偏移量、时间、尺寸、阈值),没有命名常量
|
||||
- 硬编码字符串(如 id 名、状态值、URL 片段)散落在逻辑中
|
||||
- 例外:`0`, `1`, `-1`, `100`, `""` 等语义明确的惯用值不算
|
||||
|
||||
### 3. 命名问题
|
||||
- 含义不明的缩写变量(如 `or_`, `tmp2`, `x2`)
|
||||
- 命名与实际用途不符
|
||||
- 同一概念在不同地方用不同名字表达
|
||||
|
||||
### 4. 死代码 / 无效代码
|
||||
- 注释掉的旧代码块(3行以上)
|
||||
- 声明后从未使用的变量/参数/导入
|
||||
- 永远不会执行的条件分支
|
||||
|
||||
### 5. 代码风格问题
|
||||
- 尾部空白字符(trailing whitespace)
|
||||
- 同一文件内风格不一致(如混用单双引号、缩进不统一)
|
||||
- 空行使用不一致(连续多个空行等)
|
||||
|
||||
### 6. 其他常见问题
|
||||
- 私有辅助函数应被 export 但没有,导致调用方重复实现
|
||||
- 类型/接口重复定义
|
||||
- 过于冗长的条件表达式可以简化(不改逻辑)
|
||||
|
||||
## 执行步骤
|
||||
|
||||
### Step 1 — 获取待检查文件列表
|
||||
|
||||
```bash
|
||||
# 无参数时:获取所有未提交修改
|
||||
git diff HEAD --name-only
|
||||
|
||||
# 有参数时:用 $ARGUMENTS 过滤
|
||||
```
|
||||
|
||||
### Step 2 — 逐文件阅读并分析
|
||||
|
||||
- 用 Read 工具读取完整文件(不只读 diff)
|
||||
- 对照审查清单,记录每个问题:文件名、行号、问题类型、建议修复方式
|
||||
|
||||
### Step 3 — 报告问题清单
|
||||
|
||||
在修改前,先以列表形式输出所有发现的问题:
|
||||
|
||||
```
|
||||
发现 N 个问题:
|
||||
|
||||
[文件] js/foo.js
|
||||
· L34, L78: 重复逻辑 — 两处都实现了相同的 DOM 查询,可提取到 getPanel()
|
||||
· L91: Magic number — 硬编码 14 作为偏移量,应命名为 TOOLTIP_OFFSET
|
||||
|
||||
[文件] js/bar.js
|
||||
· L12: 命名问题 — 变量 `or_` 语义不明,应命名为 outerR/outerG/outerB
|
||||
...
|
||||
```
|
||||
|
||||
如果没有发现问题,直接输出"未发现垃圾代码,当前代码质量良好。"并停止。
|
||||
|
||||
### Step 4 — 执行修复
|
||||
|
||||
对每个问题,使用 Edit 工具进行**最小化修改**:
|
||||
|
||||
- **重复逻辑**:提取为共享常量/函数,更新所有调用点
|
||||
- **Magic number**:在文件顶部或逻辑附近声明 `const NAME = value`,替换所有引用
|
||||
- **命名问题**:重命名变量,更新所有使用处
|
||||
- **死代码**:直接删除
|
||||
- **尾部空白/风格**:修正
|
||||
- **未 export 的函数**:添加 `export`,在调用方改为导入(不重复实现)
|
||||
|
||||
**修复原则:**
|
||||
- 只改在审查清单中发现的问题,不做额外优化
|
||||
- 每次 Edit 只修改确实有问题的行,保持 diff 最小
|
||||
- 改完后用 `grep` 验证旧的坏代码已消失
|
||||
|
||||
### Step 5 — 输出总结
|
||||
|
||||
```
|
||||
清理完成:
|
||||
|
||||
修复了 N 个问题:
|
||||
✓ earth.js — 提取重复 vertexShader 为 ATMOS_VERTEX_SHADER 常量
|
||||
✓ main.js — 提取 TOOLTIP_CURSOR_OFFSET = 14(4处引用)
|
||||
✓ controls.js — export updateLayerButtonState,移除 main.js 中的重复实现
|
||||
...
|
||||
|
||||
未修改的问题(需人工确认):
|
||||
! foo.js L45 — 注释代码块较长,建议手动确认是否可删除
|
||||
```
|
||||
|
||||
## 约束
|
||||
|
||||
- **禁止**改变函数签名、接口定义、导出 API(除非问题正是私有函数应被 export)
|
||||
- **禁止**添加新功能、新抽象、新参数
|
||||
- **禁止**修改注释内容(只删除注释掉的死代码)
|
||||
- **禁止**修改测试文件逻辑
|
||||
- 如果一个 Magic number 的语义不完全确定,**跳过**,在总结中标记为"需人工确认"
|
||||
146
.claude/commands/release.md
Normal file
146
.claude/commands/release.md
Normal file
@@ -0,0 +1,146 @@
|
||||
---
|
||||
description: 发版工作流:根据变更类型决定版本号,更新所有版本文件和 changelog,运行验证,commit 并 push
|
||||
argument-hint: 可选:feature | bugfix | 或直接描述本次发布内容
|
||||
allowed-tools: ["Read", "Edit", "Bash", "Glob", "Grep"]
|
||||
---
|
||||
|
||||
# /release — Planet 发版工作流
|
||||
|
||||
## 版本号规则
|
||||
|
||||
| 变更类型 | 版本跳动 | 适用场景 |
|
||||
|---------|---------|---------|
|
||||
| `feature` | `+0.1.0` | 纯新功能,无 bugfix |
|
||||
| `improvement` | `+0.0.1` | UI 调整、小功能增强、bugfix 混合,或以 UI/体验改进为主的迭代 |
|
||||
| `bugfix` | `+0.0.1` | 纯 bug 修复,无新功能 |
|
||||
| `docs` / `maintenance` / `refactor` | 默认不发版,除非用户明确要求 |
|
||||
|
||||
意图混合时以用户明确描述为准;bugfix + 小 feature 混合默认判定为 `improvement`(`+0.0.1`)。
|
||||
|
||||
## 必须同步更新的文件
|
||||
|
||||
使用 `git rev-parse --show-toplevel` 获取仓库根目录,以下路径均相对于根目录:
|
||||
|
||||
- `VERSION`
|
||||
- `frontend/package.json`(`"version"` 字段)
|
||||
- `pyproject.toml`(`version =` 字段)
|
||||
- `uv.lock`(**不要手动编辑**,通过 `uv lock` 重新生成)
|
||||
- `docs/CHANGELOG.md`
|
||||
- `docs/version-history.md`
|
||||
|
||||
## 执行步骤
|
||||
|
||||
### Step 1 — 环境检查
|
||||
|
||||
```bash
|
||||
git branch --show-current # 确认在 dev 分支
|
||||
git status --short # 检查是否有无关的未暂存修改
|
||||
cat VERSION # 读取当前版本
|
||||
```
|
||||
|
||||
若当前**不在 `dev` 分支**,停下来告知用户,不要继续。
|
||||
|
||||
若存在无关的未暂存修改,列出并询问用户是否一并提交,或先 stash。
|
||||
|
||||
### Step 2 — 确定发版类型与新版本号
|
||||
|
||||
- 若 `$ARGUMENTS` 提供了明确类型(`feature` / `bugfix`),直接使用
|
||||
- 否则根据当前 `git diff HEAD` 和 `git log` 推断
|
||||
- 计算新版本号(例:`0.26.2` → bugfix → `0.26.3`)
|
||||
- **先输出发版计划供用户确认**:
|
||||
|
||||
```
|
||||
发版计划:
|
||||
类型:bugfix
|
||||
版本:0.26.2 → 0.26.3
|
||||
分支:dev
|
||||
将更新:VERSION, frontend/package.json, pyproject.toml, uv.lock, CHANGELOG.md, version-history.md
|
||||
```
|
||||
|
||||
### Step 3 — 更新版本号文件
|
||||
|
||||
按顺序更新(每步用 Edit 工具,精确替换,不要重写整个文件):
|
||||
|
||||
1. `VERSION` — 直接替换全部内容为新版本号
|
||||
2. `frontend/package.json` — 替换 `"version": "x.x.x"` 行
|
||||
3. `pyproject.toml` — 替换 `version = "x.x.x"` 行
|
||||
4. 运行 `uv lock` 重新生成 `uv.lock`(在仓库根目录下执行)
|
||||
|
||||
### Step 4 — 更新 CHANGELOG.md
|
||||
|
||||
在文件顶部插入新条目,格式:
|
||||
|
||||
```markdown
|
||||
## [x.x.x] — YYYY-MM-DD
|
||||
|
||||
### ✨ Features / 🐛 Fixes / 🔧 Improvements
|
||||
- ...(只列高信号条目,最多 5 条)
|
||||
- ...
|
||||
|
||||
---
|
||||
```
|
||||
|
||||
日期使用 `date +%Y-%m-%d` 获取今天的日期。
|
||||
|
||||
### Step 5 — 更新 docs/version-history.md
|
||||
|
||||
- 更新文件头部的"当前开发版本"字段
|
||||
- 在时间线表格顶部插入新行:`| vx.x.x | YYYY-MM-DD | 一句话摘要 |`
|
||||
|
||||
### Step 6 — 验证
|
||||
|
||||
针对本次变更范围做最小验证:
|
||||
|
||||
- Python 文件有修改:`python3 -m py_compile <changed_files>`
|
||||
- Frontend 文件有修改:运行项目标准检查(若无则跳过并说明)
|
||||
- 版本号一致性检查:用 grep 确认 VERSION、package.json、pyproject.toml 中的版本号完全一致
|
||||
|
||||
```bash
|
||||
grep -h "version" VERSION frontend/package.json pyproject.toml
|
||||
```
|
||||
|
||||
### Step 7 — 提交前预览
|
||||
|
||||
展示将要提交的文件列表:
|
||||
|
||||
```bash
|
||||
git diff --stat HEAD
|
||||
```
|
||||
|
||||
再次确认所有必须文件都在变更列表中,**不包含**非预期文件(如调试文件、.env 等)。
|
||||
|
||||
### Step 8 — Commit & Push(用户确认后)
|
||||
|
||||
```bash
|
||||
git add VERSION frontend/package.json pyproject.toml uv.lock docs/CHANGELOG.md docs/version-history.md
|
||||
# 若有代码变更也一并 stage
|
||||
git add <code_files>
|
||||
|
||||
git commit -m "release: bump version to x.x.x"
|
||||
git tag vx.x.x
|
||||
git push origin dev
|
||||
git push origin vx.x.x
|
||||
```
|
||||
|
||||
commit message 固定格式:`release: bump version to x.x.x`
|
||||
|
||||
### Step 9 — 完成确认
|
||||
|
||||
输出摘要:
|
||||
|
||||
```
|
||||
✓ 版本号已更新:0.26.2 → 0.26.3
|
||||
✓ CHANGELOG 已更新
|
||||
✓ version-history 已更新
|
||||
✓ uv.lock 已重新生成
|
||||
✓ 验证通过
|
||||
✓ commit: release: bump version to 0.26.3
|
||||
✓ tag: v0.26.3
|
||||
✓ 已 push 到 origin/dev
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
- `uv.lock` 只能通过 `uv lock` 生成,绝不手动编辑
|
||||
- 发版 commit 只包含版本文件 + 本次功能代码,不混入无关改动
|
||||
- 若环境中 `uv` 不可用,说明原因并跳过 lockfile 更新,提醒用户手动运行
|
||||
124
.codex/skills/cleanup/SKILL.md
Normal file
124
.codex/skills/cleanup/SKILL.md
Normal file
@@ -0,0 +1,124 @@
|
||||
---
|
||||
name: cleanup
|
||||
description: Use when the user asks to clean up, lint, or review uncommitted code for common code smells — duplicate logic, magic numbers, unclear naming, dead code, style inconsistencies. Fixes issues without changing any runtime behavior.
|
||||
---
|
||||
|
||||
# Cleanup
|
||||
|
||||
Review and fix code quality issues in the current working tree without altering any logic or behavior.
|
||||
|
||||
## When To Use
|
||||
|
||||
- The user asks to clean up, tidy, or lint uncommitted changes
|
||||
- The user wants a code smell review before releasing or committing
|
||||
- The user mentions magic numbers, duplicate logic, dead code, or naming issues
|
||||
|
||||
Do not refactor architecture, add features, or change behavior.
|
||||
|
||||
## Scope
|
||||
|
||||
If the user specifies a file or directory, check only that. Otherwise check all uncommitted changes (`git diff HEAD`).
|
||||
|
||||
Only report issues present in **newly added or modified** lines of this diff — do not audit unchanged code.
|
||||
|
||||
## Checklist
|
||||
|
||||
### 1. Duplicate Logic
|
||||
- Identical or near-identical code blocks appearing in multiple places
|
||||
- A function/helper that already exists but is re-implemented elsewhere instead of being reused
|
||||
- Repeated DOM queries, regex literals, or template strings within the same file
|
||||
|
||||
### 2. Magic Numbers / Magic Strings
|
||||
- Bare numeric literals used in calculations (offsets, timeouts, sizes, thresholds) without a named constant
|
||||
- Hardcoded strings (IDs, status values, URL fragments) scattered through logic
|
||||
- Exceptions: `0`, `1`, `-1`, `100`, `""` and other idiomatically clear values are fine
|
||||
|
||||
### 3. Naming Issues
|
||||
- Cryptic abbreviations (`or_`, `tmp2`, `x2`)
|
||||
- Names that do not match actual behavior
|
||||
- The same concept referred to by different names in different places
|
||||
|
||||
### 4. Dead Code
|
||||
- Commented-out code blocks (3+ lines)
|
||||
- Variables, parameters, or imports declared but never used
|
||||
- Branches that can never execute
|
||||
|
||||
### 5. Style Inconsistencies
|
||||
- Trailing whitespace
|
||||
- Mixed quote styles or indentation within the same file
|
||||
- Inconsistent blank-line usage (multiple consecutive blank lines, etc.)
|
||||
|
||||
### 6. Other
|
||||
- Private helper functions that should be exported but are not, causing callers to duplicate the implementation
|
||||
- Overly verbose conditions that can be simplified without changing logic
|
||||
|
||||
## Steps
|
||||
|
||||
### Step 1 — Get the file list
|
||||
|
||||
```bash
|
||||
git diff HEAD --name-only
|
||||
```
|
||||
|
||||
Filter to the user-specified path if one was provided.
|
||||
|
||||
### Step 2 — Read and analyze each file
|
||||
|
||||
Read the full file (not just the diff) with the Read tool. For each file, record every issue found: filename, line number, category, and suggested fix.
|
||||
|
||||
### Step 3 — Report findings before touching anything
|
||||
|
||||
Print a structured list:
|
||||
|
||||
```
|
||||
Found N issues:
|
||||
|
||||
[file] js/foo.js
|
||||
· L34, L78: Duplicate logic — same DOM query implemented twice; extract to getPanel()
|
||||
· L91: Magic number — bare 14 used as pixel offset; name it TOOLTIP_OFFSET
|
||||
|
||||
[file] js/bar.js
|
||||
· L12: Naming — variable `or_` is unclear; rename to outerR, outerG, outerB
|
||||
...
|
||||
```
|
||||
|
||||
If no issues are found, output "No code smells detected. Code quality looks good." and stop.
|
||||
|
||||
### Step 4 — Fix each issue
|
||||
|
||||
Use the Edit tool for **minimal, targeted changes**:
|
||||
|
||||
- **Duplicate logic**: extract to a shared constant or function; update all call sites
|
||||
- **Magic number/string**: declare `const NAME = value` near the top of the relevant scope; replace all usages
|
||||
- **Naming**: rename the variable/function; update all references
|
||||
- **Dead code**: delete it
|
||||
- **Trailing whitespace / style**: fix in place
|
||||
- **Unexported helper**: add `export`; update callers to import instead of re-implementing
|
||||
|
||||
Principles:
|
||||
- Only fix issues identified in the checklist — no extra improvements
|
||||
- Keep each Edit as small as possible
|
||||
- After fixing, verify the old bad pattern is gone with grep
|
||||
|
||||
### Step 5 — Summary
|
||||
|
||||
```
|
||||
Cleanup complete:
|
||||
|
||||
Fixed N issues:
|
||||
✓ earth.js — extracted duplicate vertexShader into ATMOS_VERTEX_SHADER constant
|
||||
✓ main.js — extracted TOOLTIP_CURSOR_OFFSET = 14 (4 references updated)
|
||||
✓ controls.js — exported updateLayerButtonState; removed duplicate implementation in main.js
|
||||
...
|
||||
|
||||
Skipped (needs manual review):
|
||||
! foo.js L45 — large commented-out block; confirm it is safe to delete
|
||||
```
|
||||
|
||||
## Constraints
|
||||
|
||||
- **Do not** change function signatures, exported interfaces, or public APIs (unless the issue is a missing export)
|
||||
- **Do not** add new features, abstractions, or parameters
|
||||
- **Do not** rewrite comments (only delete commented-out dead code)
|
||||
- **Do not** touch test file logic
|
||||
- If a magic number's intent is uncertain, skip it and flag it in the summary
|
||||
@@ -1,78 +0,0 @@
|
||||
---
|
||||
name: release-workflow
|
||||
description: Use when the user asks to release, bump version, update changelog/version files, or commit/push a repository release for the Planet repo. Applies the repo's versioning rules, updates all required version-bearing files, updates changelog/version-history, runs minimal relevant validation, and then commits/pushes when requested.
|
||||
---
|
||||
|
||||
# Release Workflow
|
||||
|
||||
Use this skill for release-oriented work in this repository.
|
||||
|
||||
## When To Use
|
||||
|
||||
- The user asks to `发版`
|
||||
- The user asks to bump a version
|
||||
- The user asks to update `CHANGELOG`, `version-history`, or version files as part of a release
|
||||
- The user asks to commit/push a release or a publishable bugfix/feature bundle
|
||||
|
||||
Do not use this skill for ordinary commits that are not being released.
|
||||
|
||||
## Versioning Rules
|
||||
|
||||
- `feature` -> bump `+0.1.0`
|
||||
- `bugfix` -> bump `+0.0.1`
|
||||
- `docs`, `maintenance`, and `refactor` do not bump by default unless the user explicitly wants a release
|
||||
|
||||
When intent is mixed, prefer the user’s stated release intent. If they ask to release a bugfix bundle, use a patch bump.
|
||||
|
||||
## Required Files
|
||||
|
||||
Every release bump must update these files together:
|
||||
|
||||
- `/home/ray/dev/linkong/planet/VERSION`
|
||||
- `/home/ray/dev/linkong/planet/frontend/package.json`
|
||||
- `/home/ray/dev/linkong/planet/pyproject.toml`
|
||||
- `/home/ray/dev/linkong/planet/uv.lock`
|
||||
- `/home/ray/dev/linkong/planet/docs/CHANGELOG.md`
|
||||
- `/home/ray/dev/linkong/planet/docs/version-history.md`
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Inspect the current worktree and current version.
|
||||
2. Decide the release type from the user request:
|
||||
- feature
|
||||
- bugfix
|
||||
- release without code changes
|
||||
3. Compute the next version.
|
||||
4. Update all required version-bearing files.
|
||||
5. Add a concise but specific changelog entry:
|
||||
- highlights
|
||||
- important added/improved/fixed items
|
||||
- mention the highest-signal files only
|
||||
6. Update `docs/version-history.md`:
|
||||
- current dev version
|
||||
- new timeline row with summary
|
||||
7. Run the smallest relevant validation available.
|
||||
8. Before commit, verify the target version is present in all required files.
|
||||
9. If the user asked for commit/push:
|
||||
- stage the release files and code changes
|
||||
- commit with a conventional message
|
||||
- push to the requested branch, usually `dev`
|
||||
|
||||
## Validation Guidance
|
||||
|
||||
- Prefer scope-matched validation over broad expensive checks
|
||||
- Typical examples:
|
||||
- Python backend edits: `python3 -m py_compile ...`
|
||||
- Frontend edits: use the project-standard frontend build/check if available
|
||||
- If the environment prevents a check, say that explicitly in the final summary
|
||||
|
||||
## Release Checklist
|
||||
|
||||
Before closing the task, confirm:
|
||||
|
||||
- version bump applied consistently
|
||||
- changelog updated
|
||||
- version history updated
|
||||
- generated/runtime artifacts are not accidentally staged
|
||||
- validation status recorded
|
||||
- commit and push completed if requested
|
||||
157
.codex/skills/release/SKILL.md
Normal file
157
.codex/skills/release/SKILL.md
Normal file
@@ -0,0 +1,157 @@
|
||||
---
|
||||
name: release
|
||||
description: Use when the user asks to release, bump version, update changelog/version files, or commit/push a repository release for the Planet repo. Determines version bump type from changes, updates all required version-bearing files, updates changelog and version-history, runs minimal validation, then commits, tags, and pushes.
|
||||
---
|
||||
|
||||
# Release Workflow
|
||||
|
||||
Use this skill for release-oriented work in this repository.
|
||||
|
||||
## When To Use
|
||||
|
||||
- The user asks to `发版`
|
||||
- The user asks to bump a version
|
||||
- The user asks to update `CHANGELOG`, `version-history`, or version files as part of a release
|
||||
- The user asks to commit/push a release or a publishable bugfix/feature bundle
|
||||
|
||||
Do not use this skill for ordinary commits that are not being released.
|
||||
|
||||
## Versioning Rules
|
||||
|
||||
- `feature` -> bump `+0.1.0`
|
||||
- `bugfix` -> bump `+0.0.1`
|
||||
- `docs`, `maintenance`, and `refactor` do not bump by default unless the user explicitly wants a release
|
||||
|
||||
When intent is mixed, prefer the user's stated release intent.
|
||||
|
||||
## Required Files
|
||||
|
||||
Use `git rev-parse --show-toplevel` to get the repo root. All paths are relative to it:
|
||||
|
||||
- `VERSION`
|
||||
- `frontend/package.json` (`"version"` field)
|
||||
- `pyproject.toml` (`version =` field)
|
||||
- `uv.lock` (**never edit manually** — regenerate by running `uv lock`)
|
||||
- `docs/CHANGELOG.md`
|
||||
- `docs/version-history.md`
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1 — Environment check
|
||||
|
||||
```bash
|
||||
git branch --show-current # must be on dev
|
||||
git status --short # check for unrelated uncommitted changes
|
||||
cat VERSION # read current version
|
||||
```
|
||||
|
||||
If not on `dev`, stop and tell the user. Do not proceed.
|
||||
|
||||
If unrelated uncommitted changes exist, list them and ask the user whether to include them or stash first.
|
||||
|
||||
### Step 2 — Determine release type and next version
|
||||
|
||||
- If the user provided an explicit type (`feature` / `bugfix`), use it
|
||||
- Otherwise infer from `git diff HEAD` and recent `git log`
|
||||
- Compute the next version (e.g. `0.26.2` → bugfix → `0.26.3`)
|
||||
- **Show the release plan before making any changes:**
|
||||
|
||||
```
|
||||
Release plan:
|
||||
Type: bugfix
|
||||
Version: 0.26.2 → 0.26.3
|
||||
Branch: dev
|
||||
Will update: VERSION, frontend/package.json, pyproject.toml, uv.lock, CHANGELOG.md, version-history.md
|
||||
```
|
||||
|
||||
### Step 3 — Update version files
|
||||
|
||||
Update in order (use Edit for precise replacement, never rewrite whole files):
|
||||
|
||||
1. `VERSION` — replace entire content with new version string
|
||||
2. `frontend/package.json` — replace `"version": "x.x.x"` line
|
||||
3. `pyproject.toml` — replace `version = "x.x.x"` line
|
||||
4. Run `uv lock` at repo root to regenerate `uv.lock`
|
||||
|
||||
### Step 4 — Update CHANGELOG.md
|
||||
|
||||
Insert a new entry at the top of the file:
|
||||
|
||||
```markdown
|
||||
## x.x.x
|
||||
|
||||
Released: YYYY-MM-DD
|
||||
|
||||
### Highlights
|
||||
|
||||
- ...
|
||||
|
||||
### Added / Fixed / Improved
|
||||
|
||||
- ... (high-signal items only, max 5)
|
||||
|
||||
---
|
||||
```
|
||||
|
||||
Get today's date with `date +%Y-%m-%d`.
|
||||
|
||||
### Step 5 — Update docs/version-history.md
|
||||
|
||||
- Update the "current dev version" field in the file header
|
||||
- Insert a new row at the top of the timeline table: `| vx.x.x | YYYY-MM-DD | one-line summary |`
|
||||
|
||||
### Step 6 — Validate
|
||||
|
||||
Run the smallest relevant validation for the changes in scope:
|
||||
|
||||
- Python files changed: `python3 -m py_compile <changed_files>`
|
||||
- Frontend files changed: run the project-standard check if available; otherwise skip and say so
|
||||
- Version consistency: confirm VERSION, package.json, pyproject.toml, and uv.lock all show the same version
|
||||
|
||||
```bash
|
||||
grep -h "version" VERSION frontend/package.json pyproject.toml
|
||||
```
|
||||
|
||||
### Step 7 — Pre-commit preview
|
||||
|
||||
Show what will be committed:
|
||||
|
||||
```bash
|
||||
git diff --stat HEAD
|
||||
```
|
||||
|
||||
Confirm all required files are present and no unexpected files (debug files, `.env`, etc.) are included.
|
||||
|
||||
### Step 8 — Commit, tag, and push
|
||||
|
||||
```bash
|
||||
git add VERSION frontend/package.json pyproject.toml uv.lock docs/CHANGELOG.md docs/version-history.md
|
||||
# also stage any code changes included in this release
|
||||
git add <code_files>
|
||||
|
||||
git commit -m "release: bump version to x.x.x"
|
||||
git tag vx.x.x
|
||||
git push origin dev
|
||||
git push origin vx.x.x
|
||||
```
|
||||
|
||||
Commit message format is fixed: `release: bump version to x.x.x`
|
||||
|
||||
### Step 9 — Completion summary
|
||||
|
||||
```
|
||||
✓ Version bumped: 0.26.2 → 0.26.3
|
||||
✓ CHANGELOG updated
|
||||
✓ version-history updated
|
||||
✓ uv.lock regenerated
|
||||
✓ Validation passed
|
||||
✓ commit: release: bump version to 0.26.3
|
||||
✓ tag: v0.26.3
|
||||
✓ Pushed to origin/dev
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- `uv.lock` must only be updated by running `uv lock`, never manually
|
||||
- The release commit should include only version files + the code for this release — no unrelated changes
|
||||
- If `uv` is unavailable in the environment, say so explicitly and remind the user to run it manually
|
||||
@@ -5,8 +5,48 @@ All notable changes to `planet` are documented here.
|
||||
This project follows the repository versioning rule:
|
||||
|
||||
- `feature` -> `+0.1.0`
|
||||
- `improvement` -> `+0.0.1`(bugfix + 小功能混合)
|
||||
- `bugfix` -> `+0.0.1`
|
||||
|
||||
## [0.27.4] — 2026-04-14
|
||||
|
||||
### 🔧 Improvements
|
||||
- info-card 改为懒加载动态挂载:页面初始 DOM 不再含隐藏的 `#info-panel` 节点,仅首次点击交互元素时创建
|
||||
|
||||
---
|
||||
|
||||
## [0.27.3] — 2026-04-14
|
||||
|
||||
### 🔧 Improvements
|
||||
- TV panel meta 折叠展开方向稳定:底部锚定时向上生长,拖拽后(顶部锚定)通过 JS 补偿 top 保持播放器底部位置不变
|
||||
- 修复 TV panel 展开/折叠时视频区域跳动问题:移除面板 min-height,使播放器高度在两种状态下保持一致
|
||||
- 修正 TV panel meta toggle 箭头方向:展开朝下,折叠朝上
|
||||
- 修复图例面板折叠按钮失效(legend-bar-btn 补充进拖拽排除列表)
|
||||
- 调整图层搜索框图标尺寸为 20px,BR 缩放角标改为直角 L 形
|
||||
|
||||
---
|
||||
|
||||
## [0.27.2] — 2026-04-14
|
||||
|
||||
### 🔧 Improvements
|
||||
- 修复 brand copy 宽度不随内容收缩的问题,现在与 title 图片宽度保持一致
|
||||
- 提取 `--brand-copy-width` CSS 自定义属性,消除 160px / 172px 魔法数字重复
|
||||
|
||||
---
|
||||
|
||||
## [0.27.1] — 2026-04-14
|
||||
|
||||
### 🔧 Improvements
|
||||
- 面板拖拽新增 L 形边界约束,其他面板无法覆盖 brand 面板区域,并从右侧/底部自然卡边
|
||||
- brand 组件引入 `--brand-scale` 整体缩放变量,padding 与内容尺寸独立控制
|
||||
- 图层控制面板宽度收窄(260px),与 brand 面板错落排列,间距调大
|
||||
|
||||
### 🐛 Fixes
|
||||
- 修复搜索框 `type="search"` 导致清除按钮重复显示的问题
|
||||
- 修复 `[hidden]` 属性被组件 `display` 规则覆盖的问题
|
||||
|
||||
---
|
||||
|
||||
## 0.27.0
|
||||
|
||||
Released: 2026-04-14
|
||||
|
||||
@@ -16,12 +16,16 @@
|
||||
## Current Version
|
||||
|
||||
- `main` 当前主线历史推导到:`0.16.5`
|
||||
- `dev` 当前开发分支历史推导到:`0.27.0`
|
||||
- `dev` 当前开发分支历史推导到:`0.27.4`
|
||||
|
||||
## Timeline
|
||||
|
||||
| Version | Type | Branch | Commit | Summary |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `0.27.4` | improvement | `dev` | — | info-card 懒加载动态挂载,页面初始不再有隐藏节点 |
|
||||
| `0.27.3` | improvement | `dev` | — | TV panel 折叠方向稳定、视频跳动修复、图例折叠按钮修复、搜索图标调整 |
|
||||
| `0.27.2` | improvement | `dev` | — | 修复 brand copy 宽度问题,提取 --brand-copy-width CSS 变量 |
|
||||
| `0.27.1` | improvement | `dev` | — | HUD 面板拖拽 L 形边界约束、brand 组件整体缩放、图层面板宽度优化、搜索叉叉修复 |
|
||||
| `0.27.0` | feature | `dev` | — | Earth HUD 重构:图层面板、信息卡片悬浮定位、Fresnel 大气层渲染 |
|
||||
| `0.0.1-beta` | bootstrap | `main` | `e7033775` | first commit |
|
||||
| `0.1.0` | feature | `main` | `6cb4398f` | Modularize 3D Earth page with ES Modules |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "planet-frontend",
|
||||
"version": "0.27.0",
|
||||
"version": "0.27.4",
|
||||
"private": true,
|
||||
"packageManager": "bun@1",
|
||||
"dependencies": {
|
||||
|
||||
@@ -55,6 +55,9 @@ body,
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Ensure [hidden] always wins over component display rules */
|
||||
[hidden] { display: none !important; }
|
||||
|
||||
body.earth-page {
|
||||
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
||||
background-color: #0a0a1a;
|
||||
|
||||
@@ -77,6 +77,8 @@
|
||||
|
||||
.hud-panel-header .hud-panel-title {
|
||||
margin-bottom: 0;
|
||||
color: var(--hud-text-soft);
|
||||
font-size: calc(0.82rem * var(--hud-scale));
|
||||
}
|
||||
|
||||
.hud-panel-drag-handle {
|
||||
|
||||
@@ -24,8 +24,10 @@
|
||||
/* ── Brand panel ──────────────────────────────────────────────── */
|
||||
|
||||
.hud-panel-brand {
|
||||
--brand-scale: 0.88;
|
||||
--brand-copy-width: 160px;
|
||||
border-radius: 0;
|
||||
padding: calc(12px * var(--hud-scale)) calc(14px * var(--hud-scale));
|
||||
padding: calc(18px * var(--hud-scale)) calc(20px * var(--hud-scale));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -36,52 +38,49 @@
|
||||
.hud-panel-brand .earth-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: calc(10px * var(--hud-scale));
|
||||
gap: calc(10px * var(--hud-scale) * var(--brand-scale));
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.hud-panel-brand .earth-brand__logo {
|
||||
display: block;
|
||||
flex: 0 0 auto;
|
||||
width: calc(128px * var(--hud-scale));
|
||||
height: calc(128px * var(--hud-scale));
|
||||
width: calc(128px * var(--hud-scale) * var(--brand-scale));
|
||||
height: calc(128px * var(--hud-scale) * var(--brand-scale));
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.hud-panel-brand .earth-brand__copy {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
flex: 0 0 auto;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: calc(5px * var(--hud-scale));
|
||||
gap: calc(5px * var(--hud-scale) * var(--brand-scale));
|
||||
width: calc(var(--brand-copy-width) * var(--hud-scale) * var(--brand-scale));
|
||||
}
|
||||
|
||||
.hud-panel-brand .earth-brand__title {
|
||||
display: block;
|
||||
width: min(100%, calc(160px * var(--hud-scale)));
|
||||
width: min(100%, calc(var(--brand-copy-width) * var(--hud-scale) * var(--brand-scale)));
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
min-height: calc(20px * var(--hud-scale));
|
||||
min-height: calc(20px * var(--hud-scale) * var(--brand-scale));
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.hud-panel-brand .earth-brand__meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc(2px * var(--hud-scale));
|
||||
gap: calc(2px * var(--hud-scale) * var(--brand-scale));
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.hud-panel-brand .earth-brand__subtitle {
|
||||
color: var(--hud-text-muted);
|
||||
font-size: calc(0.74rem * var(--hud-scale));
|
||||
font-size: calc(0.74rem * var(--hud-scale) * var(--brand-scale));
|
||||
line-height: 1.3;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.01em;
|
||||
/* Prevent text from pushing brand wider than logo column */
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
@@ -89,18 +88,24 @@
|
||||
|
||||
.hud-panel-brand .earth-brand__description {
|
||||
color: var(--hud-text-soft);
|
||||
font-size: calc(0.6rem * var(--hud-scale));
|
||||
font-size: calc(0.6rem * var(--hud-scale) * var(--brand-scale));
|
||||
line-height: 1.3;
|
||||
letter-spacing: 0.08em;
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.hud-panel-brand .earth-brand--en {
|
||||
--brand-copy-width: 172px;
|
||||
}
|
||||
|
||||
.hud-panel-brand .earth-brand--en .earth-brand__title {
|
||||
width: min(100%, calc(172px * var(--hud-scale)));
|
||||
width: min(100%, calc(var(--brand-copy-width) * var(--hud-scale) * var(--brand-scale)));
|
||||
}
|
||||
|
||||
.hud-panel-brand .earth-brand--en .earth-brand__copy {
|
||||
width: calc(var(--brand-copy-width) * var(--hud-scale) * var(--brand-scale));
|
||||
}
|
||||
|
||||
.hud-panel-brand .earth-brand--en .earth-brand__subtitle,
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
/* layer-panel.css — layer toggle panel (below brand, in left column) */
|
||||
|
||||
.hud-panel-layers {
|
||||
/* Lives inside .earth-left-column — position is relative via column rule */
|
||||
/* Lives inside .earth-left-column — narrower than brand panel intentionally */
|
||||
border-radius: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
width: calc(260px * var(--hud-scale));
|
||||
z-index: 10;
|
||||
overflow: hidden;
|
||||
margin-top: calc(6px * var(--hud-scale));
|
||||
margin-top: calc(12px * var(--hud-scale));
|
||||
}
|
||||
|
||||
/* ── Header / drag handle ─────────────────────────────────────── */
|
||||
@@ -38,7 +38,7 @@
|
||||
.layer-panel-title {
|
||||
flex: 1 1 auto;
|
||||
margin: 0;
|
||||
color: var(--hud-title);
|
||||
color: var(--hud-text-soft);
|
||||
font-size: calc(0.82rem * var(--hud-scale));
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
@@ -76,30 +76,51 @@
|
||||
transition: transform 0.22s ease;
|
||||
}
|
||||
|
||||
/* Chevron rotates when collapsed */
|
||||
.layer-panel--collapsed .layer-panel-btn .material-symbols-rounded {
|
||||
/* Chevron:展开时朝上(可折叠),折叠时朝下(可展开) */
|
||||
.layer-panel-btn .material-symbols-rounded {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.layer-panel--collapsed .layer-panel-btn .material-symbols-rounded {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
/* ── Search bar ───────────────────────────────────────────────── */
|
||||
|
||||
.layer-panel-search {
|
||||
padding: calc(6px * var(--hud-scale)) calc(8px * var(--hud-scale));
|
||||
border-bottom: 1px solid var(--hud-line);
|
||||
}
|
||||
|
||||
.layer-panel-search-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: calc(5px * var(--hud-scale));
|
||||
padding: calc(6px * var(--hud-scale)) calc(10px * var(--hud-scale));
|
||||
border-bottom: 1px solid var(--hud-line);
|
||||
padding: calc(5px * var(--hud-scale)) calc(8px * var(--hud-scale));
|
||||
border: 1px solid rgba(201, 225, 247, 0.14);
|
||||
border-radius: calc(8px * var(--hud-scale));
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
transition: border-color 0.18s ease;
|
||||
box-sizing: border-box;
|
||||
height: calc(38px * var(--hud-scale));
|
||||
}
|
||||
|
||||
.layer-panel-search-box:focus-within {
|
||||
border-color: rgba(201, 225, 247, 0.28);
|
||||
}
|
||||
|
||||
.layer-panel-search-icon {
|
||||
flex-shrink: 0;
|
||||
font-size: calc(14px * var(--hud-scale));
|
||||
color: var(--hud-text-soft);
|
||||
line-height: 1;
|
||||
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 20;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.layer-panel-search-icon.material-symbols-rounded {
|
||||
font-size: calc(20px * var(--hud-scale));
|
||||
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 20;
|
||||
}
|
||||
|
||||
.layer-panel-search-input {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
|
||||
@@ -6,17 +6,34 @@
|
||||
width: calc(420px * var(--hud-scale));
|
||||
max-width: calc(100vw - 32px);
|
||||
min-width: calc(300px * var(--hud-scale));
|
||||
min-height: calc(340px * var(--hud-scale));
|
||||
padding: calc(18px * var(--hud-scale));
|
||||
padding: calc(10px * var(--hud-scale));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--hud-gap-sm);
|
||||
z-index: 18;
|
||||
}
|
||||
|
||||
.tv-panel-header-copy {
|
||||
display: grid;
|
||||
gap: calc(3px * var(--hud-scale));
|
||||
/* header 内嵌 select + actions */
|
||||
.hud-panel-tv .hud-panel-header {
|
||||
align-items: center;
|
||||
gap: var(--hud-gap-xs);
|
||||
}
|
||||
|
||||
.hud-panel-tv .hud-panel-header .hud-panel-close {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.tv-panel-header-title {
|
||||
flex: 0 0 auto;
|
||||
white-space: nowrap;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* select / action 在 drag-handle 内,恢复正常指针 */
|
||||
.hud-panel-tv .hud-panel-header .tv-panel-select,
|
||||
.hud-panel-tv .hud-panel-header .tv-panel-action {
|
||||
cursor: pointer;
|
||||
user-select: auto;
|
||||
}
|
||||
|
||||
.tv-panel-status {
|
||||
@@ -26,13 +43,6 @@
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.tv-panel-controls {
|
||||
display: flex;
|
||||
gap: var(--hud-gap-sm);
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.tv-panel-select {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
@@ -59,28 +69,25 @@
|
||||
}
|
||||
|
||||
.tv-panel-action {
|
||||
border: 1px solid rgba(201, 225, 247, 0.12);
|
||||
border-radius: calc(12px * var(--hud-scale));
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: var(--hud-text);
|
||||
padding: calc(10px * var(--hud-scale)) calc(12px * var(--hud-scale));
|
||||
border: 1px solid transparent;
|
||||
border-radius: calc(4px * var(--hud-scale));
|
||||
background: transparent;
|
||||
color: var(--hud-text-muted);
|
||||
padding: calc(7px * var(--hud-scale));
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
white-space: nowrap;
|
||||
font-size: calc(0.84rem * var(--hud-scale));
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
transition: background 0.18s ease, border-color 0.18s ease, color 0.18s ease;
|
||||
}
|
||||
|
||||
.tv-panel-action--icon {
|
||||
padding: calc(9px * var(--hud-scale));
|
||||
border-radius: calc(10px * var(--hud-scale));
|
||||
transition:
|
||||
background 0.18s ease,
|
||||
border-color 0.18s ease,
|
||||
color 0.18s ease,
|
||||
transform 0.18s ease,
|
||||
opacity 0.18s ease;
|
||||
}
|
||||
|
||||
.tv-panel-action--icon .material-symbols-rounded {
|
||||
font-size: calc(18px * var(--hud-scale));
|
||||
font-size: calc(16px * var(--hud-scale));
|
||||
line-height: 1;
|
||||
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 20;
|
||||
pointer-events: none;
|
||||
@@ -88,7 +95,7 @@
|
||||
|
||||
.tv-panel-action:hover:not(:disabled) {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border-color: rgba(225, 239, 255, 0.2);
|
||||
border-color: rgba(225, 239, 255, 0.14);
|
||||
color: var(--hud-accent-strong);
|
||||
}
|
||||
|
||||
@@ -97,6 +104,32 @@
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* toggle 箭头:展开时朝下,折叠时朝上 */
|
||||
.tv-panel-meta-toggle .material-symbols-rounded {
|
||||
transition: transform 0.22s ease;
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
.tv-panel-meta-toggle.is-collapsed .material-symbols-rounded {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* meta wrap:折叠时用负 margin 抵消 flex gap,无死区 */
|
||||
.tv-panel-meta-wrap {
|
||||
overflow: hidden;
|
||||
max-height: calc(120px * var(--hud-scale));
|
||||
opacity: 1;
|
||||
transition: max-height 0.22s ease, opacity 0.18s ease, margin 0.22s ease;
|
||||
}
|
||||
|
||||
.tv-panel-meta-wrap.is-collapsed {
|
||||
max-height: 0;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
margin-top: calc(-1 * var(--hud-gap-sm));
|
||||
margin-bottom: calc(-1 * var(--hud-gap-sm));
|
||||
}
|
||||
|
||||
.tv-panel-meta {
|
||||
display: grid;
|
||||
gap: calc(4px * var(--hud-scale));
|
||||
@@ -129,7 +162,7 @@
|
||||
|
||||
.tv-panel-player {
|
||||
position: relative;
|
||||
flex: 1 1 auto;
|
||||
flex: 1 0 auto;
|
||||
min-height: calc(220px * var(--hud-scale));
|
||||
border-radius: calc(16px * var(--hud-scale));
|
||||
overflow: hidden;
|
||||
@@ -163,33 +196,65 @@
|
||||
background: #050a14;
|
||||
}
|
||||
|
||||
.tv-panel-resize-handle {
|
||||
/* ── Multi-edge resize handles ───────────────────────────────── */
|
||||
|
||||
.tv-panel-edge {
|
||||
position: absolute;
|
||||
right: calc(8px * var(--hud-scale));
|
||||
bottom: calc(8px * var(--hud-scale));
|
||||
width: calc(18px * var(--hud-scale));
|
||||
height: calc(18px * var(--hud-scale));
|
||||
border: 0;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
cursor: nwse-resize;
|
||||
z-index: 2;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.tv-panel-resize-handle::before {
|
||||
.tv-panel-edge[data-edge="r"] {
|
||||
right: 0;
|
||||
top: calc(12px * var(--hud-scale));
|
||||
bottom: calc(12px * var(--hud-scale));
|
||||
width: calc(6px * var(--hud-scale));
|
||||
cursor: ew-resize;
|
||||
}
|
||||
|
||||
.tv-panel-edge[data-edge="b"] {
|
||||
bottom: 0;
|
||||
left: calc(12px * var(--hud-scale));
|
||||
right: calc(12px * var(--hud-scale));
|
||||
height: calc(6px * var(--hud-scale));
|
||||
cursor: ns-resize;
|
||||
}
|
||||
|
||||
.tv-panel-edge[data-edge="l"] {
|
||||
left: 0;
|
||||
top: calc(12px * var(--hud-scale));
|
||||
bottom: calc(12px * var(--hud-scale));
|
||||
width: calc(6px * var(--hud-scale));
|
||||
cursor: ew-resize;
|
||||
}
|
||||
|
||||
.tv-panel-edge[data-edge="br"] {
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: calc(20px * var(--hud-scale));
|
||||
height: calc(20px * var(--hud-scale));
|
||||
cursor: nwse-resize;
|
||||
}
|
||||
|
||||
.tv-panel-edge[data-edge="bl"] {
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: calc(20px * var(--hud-scale));
|
||||
height: calc(20px * var(--hud-scale));
|
||||
cursor: nesw-resize;
|
||||
}
|
||||
|
||||
/* 右下角视觉标记 */
|
||||
.tv-panel-edge[data-edge="br"]::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-right: 2px solid rgba(223, 235, 248, 0.46);
|
||||
border-bottom: 2px solid rgba(223, 235, 248, 0.46);
|
||||
border-bottom-right-radius: calc(10px * var(--hud-scale));
|
||||
opacity: 0.78;
|
||||
transition: opacity 0.18s ease, border-color 0.18s ease;
|
||||
inset: calc(4px * var(--hud-scale));
|
||||
border-right: 2px solid rgba(223, 235, 248, 0.4);
|
||||
border-bottom: 2px solid rgba(223, 235, 248, 0.4);
|
||||
transition: border-color 0.18s ease;
|
||||
}
|
||||
|
||||
.tv-panel-resize-handle:hover::before {
|
||||
opacity: 1;
|
||||
border-color: rgba(244, 249, 255, 0.78);
|
||||
.tv-panel-edge[data-edge="br"]:hover::before {
|
||||
border-color: rgba(244, 249, 255, 0.75);
|
||||
}
|
||||
|
||||
.hud-panel-tv.is-resizing {
|
||||
@@ -197,6 +262,25 @@
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* Whole panel is draggable; player overrides back to default */
|
||||
.hud-panel-tv:not(.is-resizing) {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.hud-panel-tv.is-dragging {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.hud-panel-tv .tv-panel-player {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* Disable iframe/video pointer capture while dragging so mouse events pass through */
|
||||
.hud-panel-tv.is-dragging .tv-panel-iframe,
|
||||
.hud-panel-tv.is-dragging .tv-panel-video {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.earth-app.layout-expanded .hud-panel-tv:not([data-dragged="true"]) {
|
||||
bottom: var(--hud-offset);
|
||||
right: var(--hud-offset);
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
</defs>
|
||||
</svg>
|
||||
<div id="container" class="earth-app">
|
||||
<div class="earth-left-column">
|
||||
<div id="left-column" class="earth-left-column">
|
||||
<div id="brand-panel" class="hud-panel hud-panel-brand">
|
||||
<div id="brand-root"></div>
|
||||
</div>
|
||||
@@ -69,18 +69,20 @@
|
||||
<div class="layer-panel-body" id="layer-panel-body">
|
||||
<!-- Search -->
|
||||
<div class="layer-panel-search">
|
||||
<span class="material-symbols-rounded layer-panel-search-icon">search</span>
|
||||
<input
|
||||
type="search"
|
||||
id="layer-search-input"
|
||||
class="layer-panel-search-input"
|
||||
placeholder="搜索图层..."
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
>
|
||||
<button id="layer-search-clear" class="layer-panel-btn layer-search-clear" type="button" aria-label="清除搜索" title="清除" hidden>
|
||||
<span class="material-symbols-rounded">close</span>
|
||||
</button>
|
||||
<div class="layer-panel-search-box">
|
||||
<span class="material-symbols-rounded layer-panel-search-icon">search</span>
|
||||
<input
|
||||
type="text"
|
||||
id="layer-search-input"
|
||||
class="layer-panel-search-input"
|
||||
placeholder="搜索图层..."
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
>
|
||||
<button id="layer-search-clear" class="layer-panel-btn layer-search-clear" type="button" aria-label="清除搜索" title="清除" hidden>
|
||||
<span class="material-symbols-rounded">close</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Layer rows -->
|
||||
@@ -143,20 +145,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Floating detail panel — positioned near click by JS -->
|
||||
<div id="info-panel" class="hud-panel hud-panel-info hud-panel-draggable" aria-live="polite">
|
||||
<div id="info-card" class="info-card">
|
||||
<div class="info-card-header hud-panel-drag-handle">
|
||||
<span class="info-card-icon" id="info-card-icon">🛰️</span>
|
||||
<h3 id="info-card-title">详情</h3>
|
||||
<button class="info-card-close hud-panel-close" type="button" aria-label="关闭详情">
|
||||
<span class="material-symbols-rounded">close</span>
|
||||
</button>
|
||||
</div>
|
||||
<div id="info-card-content" class="info-card-content"></div>
|
||||
</div>
|
||||
<div id="error-message" class="hud-error-message"></div>
|
||||
</div>
|
||||
<div id="error-message" class="hud-error-message"></div>
|
||||
|
||||
<div id="right-toolbar-group" class="earth-toolbar-group">
|
||||
<div id="control-toolbar" class="earth-toolbar">
|
||||
@@ -299,17 +288,9 @@
|
||||
<span id="camera-distance" hidden></span>
|
||||
</div>
|
||||
|
||||
<div id="tv-panel" class="hud-panel hud-panel-tv hud-panel-draggable" data-panel-key="tv-panel">
|
||||
<div id="tv-panel" class="hud-panel hud-panel-tv hud-panel-draggable" data-panel-key="tv-panel" data-drag-self="true">
|
||||
<div class="hud-panel-header hud-panel-drag-handle">
|
||||
<div class="tv-panel-header-copy">
|
||||
<h3 class="hud-panel-title">新闻直播</h3>
|
||||
<span id="tv-source-status" class="tv-panel-status">等待加载直播源</span>
|
||||
</div>
|
||||
<button class="hud-panel-close" type="button" data-close-panel="tv-panel" aria-label="关闭电视直播">
|
||||
<span class="material-symbols-rounded">close</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="tv-panel-controls">
|
||||
<span class="hud-panel-title tv-panel-header-title">新闻直播</span>
|
||||
<select id="tv-source-select" class="tv-panel-select" aria-label="选择新闻直播源"></select>
|
||||
<div class="tv-panel-actions">
|
||||
<button id="tv-refresh" class="tv-panel-action tv-panel-action--icon" type="button" title="刷新直播源" aria-label="刷新直播源">
|
||||
@@ -318,13 +299,22 @@
|
||||
<button id="tv-open-external" class="tv-panel-action tv-panel-action--icon" type="button" title="访问官网" aria-label="访问官网">
|
||||
<span class="material-symbols-rounded">open_in_new</span>
|
||||
</button>
|
||||
<button id="tv-meta-toggle" class="tv-panel-action tv-panel-action--icon tv-panel-meta-toggle" type="button" title="频道信息" aria-label="频道信息">
|
||||
<span class="material-symbols-rounded">expand_more</span>
|
||||
</button>
|
||||
</div>
|
||||
<button class="hud-panel-close" type="button" data-close-panel="tv-panel" aria-label="关闭电视直播">
|
||||
<span class="material-symbols-rounded">close</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="tv-panel-meta">
|
||||
<div id="tv-source-title" class="tv-panel-title">暂无可用频道</div>
|
||||
<div id="tv-source-meta" class="tv-panel-subtitle">当前未配置可播放新闻直播源</div>
|
||||
<div id="tv-source-catalog" class="tv-panel-catalog">频道目录待同步</div>
|
||||
<div id="tv-source-notes" class="tv-panel-notes">支持后台配置默认源与采集器补充源。</div>
|
||||
<div class="tv-panel-meta-wrap" id="tv-meta-wrap">
|
||||
<div class="tv-panel-meta" id="tv-panel-meta">
|
||||
<span id="tv-source-status" class="tv-panel-status">等待加载直播源</span>
|
||||
<div id="tv-source-title" class="tv-panel-title">暂无可用频道</div>
|
||||
<div id="tv-source-meta" class="tv-panel-subtitle">当前未配置可播放新闻直播源</div>
|
||||
<div id="tv-source-catalog" class="tv-panel-catalog">频道目录待同步</div>
|
||||
<div id="tv-source-notes" class="tv-panel-notes">支持后台配置默认源与采集器补充源。</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tv-panel-player">
|
||||
<div id="tv-empty-state" class="tv-panel-empty">暂无可播放直播源,请先在系统配置中添加频道。</div>
|
||||
@@ -338,13 +328,11 @@
|
||||
></iframe>
|
||||
<video id="tv-video" class="tv-panel-video" hidden controls autoplay muted playsinline></video>
|
||||
</div>
|
||||
<button
|
||||
id="tv-resize-handle"
|
||||
class="tv-panel-resize-handle"
|
||||
type="button"
|
||||
aria-label="调整电视直播窗口大小"
|
||||
title="调整大小"
|
||||
></button>
|
||||
<div class="tv-panel-edge" data-edge="r"></div>
|
||||
<div class="tv-panel-edge" data-edge="b"></div>
|
||||
<div class="tv-panel-edge" data-edge="l"></div>
|
||||
<div class="tv-panel-edge" data-edge="br"></div>
|
||||
<div class="tv-panel-edge" data-edge="bl"></div>
|
||||
</div>
|
||||
|
||||
<div id="loading" class="earth-loading">
|
||||
|
||||
46
frontend/public/earth/js/controls.js
vendored
46
frontend/public/earth/js/controls.js
vendored
@@ -174,7 +174,9 @@ function setupDraggableHudPanels() {
|
||||
if (!app || draggablePanels.length === 0) return;
|
||||
|
||||
draggablePanels.forEach((panel) => {
|
||||
const handle = panel.querySelector(".hud-panel-drag-handle");
|
||||
const handle = panel.dataset.dragSelf === "true"
|
||||
? panel
|
||||
: panel.querySelector(".hud-panel-drag-handle");
|
||||
if (!handle) return;
|
||||
|
||||
let isDragging = false;
|
||||
@@ -193,14 +195,31 @@ function setupDraggableHudPanels() {
|
||||
if (!isDragging) return;
|
||||
const appRect = app.getBoundingClientRect();
|
||||
const panelRect = panel.getBoundingClientRect();
|
||||
const nextLeft = Math.min(
|
||||
const brandPanel = document.getElementById("brand-panel");
|
||||
const brandRect = brandPanel ? brandPanel.getBoundingClientRect() : null;
|
||||
const brandBottom = brandRect ? brandRect.bottom - appRect.top : 0;
|
||||
const brandRight = brandRect ? brandRect.right - appRect.left : 0;
|
||||
|
||||
let nextLeft = Math.min(
|
||||
Math.max(startLeft + (event.clientX - startPointerX), 0),
|
||||
appRect.width - panelRect.width,
|
||||
);
|
||||
const nextTop = Math.min(
|
||||
let nextTop = Math.min(
|
||||
Math.max(startTop + (event.clientY - startPointerY), 0),
|
||||
appRect.height - panelRect.height,
|
||||
);
|
||||
// Brand 面板形成 L 形禁区:panel 不能进入 brand 左上角矩形区域。
|
||||
// 当两个轴同时越界时,比较两侧超出量——哪侧需要的调整量更小就卡哪侧。
|
||||
// 从右侧滑入 → leftAdjust 小 → 卡右边;从下方滑入 → topAdjust 小 → 卡底边。
|
||||
if (brandRect && nextLeft < brandRight && nextTop < brandBottom) {
|
||||
const leftAdjust = brandRight - nextLeft;
|
||||
const topAdjust = brandBottom - nextTop;
|
||||
if (leftAdjust <= topAdjust) {
|
||||
nextLeft = brandRight;
|
||||
} else {
|
||||
nextTop = brandBottom;
|
||||
}
|
||||
}
|
||||
|
||||
panel.style.left = `${nextLeft}px`;
|
||||
panel.style.top = `${nextTop}px`;
|
||||
@@ -211,7 +230,7 @@ function setupDraggableHudPanels() {
|
||||
};
|
||||
|
||||
bindListener(handle, "pointerdown", (event) => {
|
||||
if (event.target.closest(".hud-panel-close, .layer-panel-btn, .info-card-close")) return;
|
||||
if (event.target.closest(".hud-panel-close, .layer-panel-btn, .info-card-close, .tv-panel-select, .tv-panel-action, .tv-panel-player, .tv-panel-edge, .legend-bar-btn")) return;
|
||||
isDragging = true;
|
||||
startPointerX = event.clientX;
|
||||
startPointerY = event.clientY;
|
||||
@@ -221,6 +240,8 @@ function setupDraggableHudPanels() {
|
||||
// If panel is inside a flow container (not a direct child of app), reparent
|
||||
// it so absolute positioning is relative to the app container.
|
||||
if (panel.parentElement !== app) {
|
||||
panel.dataset.originalParentId = panel.parentElement?.id || "";
|
||||
panel.dataset.originalNextSiblingId = panel.nextElementSibling?.id || "";
|
||||
const capturedWidth = panelRect.width;
|
||||
panel.style.position = "absolute";
|
||||
panel.style.width = `${capturedWidth}px`;
|
||||
@@ -912,11 +933,28 @@ function updateLayoutUI(container) {
|
||||
}
|
||||
|
||||
function resetPanelInlineLayout(panel) {
|
||||
const originalParentId = panel.dataset.originalParentId;
|
||||
if (originalParentId) {
|
||||
const originalParent = document.getElementById(originalParentId);
|
||||
if (originalParent) {
|
||||
const nextId = panel.dataset.originalNextSiblingId;
|
||||
const nextSibling = nextId ? document.getElementById(nextId) : null;
|
||||
if (nextSibling) {
|
||||
originalParent.insertBefore(panel, nextSibling);
|
||||
} else {
|
||||
originalParent.appendChild(panel);
|
||||
}
|
||||
}
|
||||
delete panel.dataset.originalParentId;
|
||||
delete panel.dataset.originalNextSiblingId;
|
||||
}
|
||||
panel.style.left = "";
|
||||
panel.style.top = "";
|
||||
panel.style.right = "";
|
||||
panel.style.bottom = "";
|
||||
panel.style.transform = "";
|
||||
panel.style.position = "";
|
||||
panel.style.width = "";
|
||||
delete panel.dataset.dragged;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { showStatusMessage } from './ui.js';
|
||||
|
||||
let currentType = null;
|
||||
let cardMounted = false;
|
||||
|
||||
const CARD_CONFIG = {
|
||||
cable: {
|
||||
@@ -105,6 +106,138 @@ function getPanel() {
|
||||
return document.getElementById('info-panel');
|
||||
}
|
||||
|
||||
function setupInfoCardDrag(panel) {
|
||||
const app = document.getElementById('container');
|
||||
if (!app) return;
|
||||
|
||||
const handle = panel.querySelector('.hud-panel-drag-handle');
|
||||
if (!handle) return;
|
||||
|
||||
let isDragging = false;
|
||||
let startPointerX = 0;
|
||||
let startPointerY = 0;
|
||||
let startLeft = 0;
|
||||
let startTop = 0;
|
||||
|
||||
const stopDragging = () => {
|
||||
isDragging = false;
|
||||
panel.classList.remove('is-dragging');
|
||||
document.body.style.userSelect = '';
|
||||
};
|
||||
|
||||
const onMove = (event) => {
|
||||
if (!isDragging) return;
|
||||
const appRect = app.getBoundingClientRect();
|
||||
const panelRect = panel.getBoundingClientRect();
|
||||
const nextLeft = Math.min(
|
||||
Math.max(startLeft + (event.clientX - startPointerX), 0),
|
||||
appRect.width - panelRect.width,
|
||||
);
|
||||
const nextTop = Math.min(
|
||||
Math.max(startTop + (event.clientY - startPointerY), 0),
|
||||
appRect.height - panelRect.height,
|
||||
);
|
||||
panel.style.left = `${nextLeft}px`;
|
||||
panel.style.top = `${nextTop}px`;
|
||||
};
|
||||
|
||||
handle.addEventListener('pointerdown', (event) => {
|
||||
if (event.target.closest('.hud-panel-close, .info-card-close')) return;
|
||||
isDragging = true;
|
||||
startPointerX = event.clientX;
|
||||
startPointerY = event.clientY;
|
||||
const appRect = app.getBoundingClientRect();
|
||||
const panelRect = panel.getBoundingClientRect();
|
||||
startLeft = panelRect.left - appRect.left;
|
||||
startTop = panelRect.top - appRect.top;
|
||||
panel.style.left = `${startLeft}px`;
|
||||
panel.style.top = `${startTop}px`;
|
||||
panel.style.right = 'auto';
|
||||
panel.style.bottom = 'auto';
|
||||
panel.classList.add('is-dragging');
|
||||
document.body.style.userSelect = 'none';
|
||||
handle.setPointerCapture?.(event.pointerId);
|
||||
});
|
||||
|
||||
handle.addEventListener('pointermove', onMove);
|
||||
handle.addEventListener('pointerup', stopDragging);
|
||||
handle.addEventListener('pointercancel', stopDragging);
|
||||
handle.addEventListener('lostpointercapture', stopDragging);
|
||||
}
|
||||
|
||||
function mountCard() {
|
||||
if (cardMounted) return;
|
||||
|
||||
const container = document.getElementById('container');
|
||||
if (!container) return;
|
||||
|
||||
const panel = document.createElement('div');
|
||||
panel.id = 'info-panel';
|
||||
panel.className = 'hud-panel hud-panel-info';
|
||||
panel.setAttribute('aria-live', 'polite');
|
||||
panel.innerHTML = `
|
||||
<div id="info-card" class="info-card">
|
||||
<div class="info-card-header hud-panel-drag-handle">
|
||||
<span class="info-card-icon" id="info-card-icon">🛰️</span>
|
||||
<h3 id="info-card-title">详情</h3>
|
||||
<button class="info-card-close hud-panel-close" type="button" aria-label="关闭详情">
|
||||
<span class="material-symbols-rounded">close</span>
|
||||
</button>
|
||||
</div>
|
||||
<div id="info-card-content" class="info-card-content"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.appendChild(panel);
|
||||
|
||||
const card = panel.querySelector('#info-card');
|
||||
const content = panel.querySelector('#info-card-content');
|
||||
|
||||
// Prevent pointer events from reaching the earth canvas
|
||||
const stopEvent = (event) => { event.stopPropagation(); };
|
||||
[
|
||||
'mousemove', 'mousedown', 'mouseup', 'click', 'dblclick', 'wheel',
|
||||
'pointerdown', 'pointerup', 'pointermove',
|
||||
'touchstart', 'touchmove', 'touchend',
|
||||
].forEach((evt) => card.addEventListener(evt, stopEvent, { passive: false }));
|
||||
|
||||
// Close button
|
||||
const closeBtn = card.querySelector('.info-card-close');
|
||||
if (closeBtn) {
|
||||
closeBtn.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
hideInfoCard();
|
||||
});
|
||||
}
|
||||
|
||||
// Copy value on label click
|
||||
content.addEventListener('click', async (event) => {
|
||||
const label = event.target.closest('.info-card-label');
|
||||
if (!label) return;
|
||||
|
||||
const property = label.closest('.info-card-property');
|
||||
const valueEl = property?.querySelector('.info-card-value');
|
||||
const value = valueEl?.textContent?.trim();
|
||||
|
||||
if (!value || value === '-') {
|
||||
showStatusMessage('无可复制内容', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(value);
|
||||
showStatusMessage(`已复制${label.textContent}:${value}`, 'success');
|
||||
} catch (error) {
|
||||
console.error('Copy failed:', error);
|
||||
showStatusMessage('复制失败', 'error');
|
||||
}
|
||||
});
|
||||
|
||||
setupInfoCardDrag(panel);
|
||||
|
||||
cardMounted = true;
|
||||
}
|
||||
|
||||
function positionPanel(panel, x, y) {
|
||||
if (!panel) return;
|
||||
const margin = 12;
|
||||
@@ -142,71 +275,8 @@ function hidePanel() {
|
||||
if (panel) panel.classList.remove('is-visible');
|
||||
}
|
||||
|
||||
export function initInfoCard() {
|
||||
const card = document.getElementById('info-card');
|
||||
const content = document.getElementById('info-card-content');
|
||||
if (!card || !content) return;
|
||||
|
||||
if (card.dataset.interactionBound !== 'true') {
|
||||
const stopEvent = (event) => {
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
[
|
||||
'mousemove',
|
||||
'mousedown',
|
||||
'mouseup',
|
||||
'click',
|
||||
'dblclick',
|
||||
'wheel',
|
||||
'pointerdown',
|
||||
'pointerup',
|
||||
'pointermove',
|
||||
'touchstart',
|
||||
'touchmove',
|
||||
'touchend',
|
||||
].forEach((eventName) => {
|
||||
card.addEventListener(eventName, stopEvent, { passive: false });
|
||||
});
|
||||
|
||||
// Close button wires the panel hide
|
||||
const closeBtn = card.querySelector('.info-card-close');
|
||||
if (closeBtn) {
|
||||
closeBtn.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
hideInfoCard();
|
||||
});
|
||||
}
|
||||
|
||||
card.dataset.interactionBound = 'true';
|
||||
}
|
||||
|
||||
if (content.dataset.copyBound === 'true') return;
|
||||
|
||||
content.addEventListener('click', async (event) => {
|
||||
const label = event.target.closest('.info-card-label');
|
||||
if (!label) return;
|
||||
|
||||
const property = label.closest('.info-card-property');
|
||||
const valueEl = property?.querySelector('.info-card-value');
|
||||
const value = valueEl?.textContent?.trim();
|
||||
|
||||
if (!value || value === '-') {
|
||||
showStatusMessage('无可复制内容', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(value);
|
||||
showStatusMessage(`已复制${label.textContent}:${value}`, 'success');
|
||||
} catch (error) {
|
||||
console.error('Copy failed:', error);
|
||||
showStatusMessage('复制失败', 'error');
|
||||
}
|
||||
});
|
||||
|
||||
content.dataset.copyBound = 'true';
|
||||
}
|
||||
// No-op: event binding now happens lazily in mountCard()
|
||||
export function initInfoCard() {}
|
||||
|
||||
export function setInfoCardNoBorder(noBorder = true) {
|
||||
const card = document.getElementById('info-card');
|
||||
@@ -222,6 +292,8 @@ export function showInfoCard(type, data, options = {}) {
|
||||
return;
|
||||
}
|
||||
|
||||
mountCard();
|
||||
|
||||
currentType = type;
|
||||
const card = document.getElementById('info-card');
|
||||
const icon = document.getElementById('info-card-icon');
|
||||
|
||||
@@ -20,6 +20,9 @@ let initialized = false;
|
||||
let refreshPromise = null;
|
||||
let hlsPlayer = null;
|
||||
let hlsRecoveryAttempts = 0;
|
||||
let metaAutoCollapseTimer = null;
|
||||
|
||||
const META_AUTO_COLLAPSE_DELAY = 2500;
|
||||
|
||||
const HLS_MAX_RECOVERY_ATTEMPTS = 3;
|
||||
const HLS_RETRY_CONFIG = {
|
||||
@@ -33,7 +36,7 @@ function getElements() {
|
||||
return {
|
||||
panel: document.getElementById("tv-panel"),
|
||||
toggleBtn: document.getElementById("toggle-tv"),
|
||||
resizeHandle: document.getElementById("tv-resize-handle"),
|
||||
resizeHandle: null,
|
||||
select: document.getElementById("tv-source-select"),
|
||||
title: document.getElementById("tv-source-title"),
|
||||
meta: document.getElementById("tv-source-meta"),
|
||||
@@ -45,9 +48,51 @@ function getElements() {
|
||||
empty: document.getElementById("tv-empty-state"),
|
||||
refreshBtn: document.getElementById("tv-refresh"),
|
||||
openBtn: document.getElementById("tv-open-external"),
|
||||
metaPanel: document.getElementById("tv-panel-meta"),
|
||||
metaWrap: document.getElementById("tv-meta-wrap"),
|
||||
metaToggle: document.getElementById("tv-meta-toggle"),
|
||||
};
|
||||
}
|
||||
|
||||
function setMetaCollapsed(collapsed) {
|
||||
const { metaWrap, metaToggle, panel } = getElements();
|
||||
if (!metaWrap) return;
|
||||
|
||||
const isDragged = panel?.dataset.dragged === "true" && panel.style.top;
|
||||
|
||||
if (isDragged) {
|
||||
// Top-anchored panel: toggle instantly and compensate top so the player
|
||||
// (panel bottom) stays visually fixed.
|
||||
const bottomBefore = panel.getBoundingClientRect().bottom;
|
||||
|
||||
metaWrap.style.transition = "none";
|
||||
metaWrap.classList.toggle("is-collapsed", collapsed);
|
||||
metaToggle?.classList.toggle("is-collapsed", collapsed);
|
||||
|
||||
// Force synchronous reflow to get updated panel height
|
||||
void panel.offsetHeight;
|
||||
|
||||
const delta = panel.getBoundingClientRect().bottom - bottomBefore;
|
||||
if (delta !== 0) {
|
||||
panel.style.top = `${parseFloat(panel.style.top) - delta}px`;
|
||||
}
|
||||
|
||||
// Restore CSS transition after this paint
|
||||
requestAnimationFrame(() => {
|
||||
metaWrap.style.transition = "";
|
||||
});
|
||||
} else {
|
||||
metaWrap.classList.toggle("is-collapsed", collapsed);
|
||||
metaToggle?.classList.toggle("is-collapsed", collapsed);
|
||||
}
|
||||
}
|
||||
|
||||
function autoExpandMeta() {
|
||||
clearTimeout(metaAutoCollapseTimer);
|
||||
setMetaCollapsed(false);
|
||||
metaAutoCollapseTimer = setTimeout(() => setMetaCollapsed(true), META_AUTO_COLLAPSE_DELAY);
|
||||
}
|
||||
|
||||
function clearPanelPositioningForResize(panel) {
|
||||
panel.style.left = `${panel.offsetLeft}px`;
|
||||
panel.style.top = `${panel.offsetTop}px`;
|
||||
@@ -65,71 +110,80 @@ function getHudScale() {
|
||||
}
|
||||
|
||||
function setupResizeHandle() {
|
||||
const { panel, resizeHandle } = getElements();
|
||||
const { panel } = getElements();
|
||||
const container = document.getElementById("container");
|
||||
if (!(panel instanceof HTMLElement) || !(resizeHandle instanceof HTMLElement) || !(container instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
if (!(panel instanceof HTMLElement) || !(container instanceof HTMLElement)) return;
|
||||
|
||||
let resizing = false;
|
||||
let activeEdge = "";
|
||||
let startX = 0;
|
||||
let startY = 0;
|
||||
let startWidth = 0;
|
||||
let startHeight = 0;
|
||||
let startLeft = 0;
|
||||
let startTop = 0;
|
||||
|
||||
const stopResize = () => {
|
||||
resizing = false;
|
||||
activeEdge = "";
|
||||
panel.classList.remove("is-resizing");
|
||||
document.body.style.userSelect = "";
|
||||
};
|
||||
|
||||
resizeHandle.addEventListener("pointerdown", (event) => {
|
||||
if (document.getElementById("container")?.classList.contains("layout-expanded")) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
resizing = true;
|
||||
startX = event.clientX;
|
||||
startY = event.clientY;
|
||||
|
||||
clearPanelPositioningForResize(panel);
|
||||
|
||||
const rect = panel.getBoundingClientRect();
|
||||
startWidth = rect.width;
|
||||
startHeight = rect.height;
|
||||
panel.classList.add("is-resizing");
|
||||
document.body.style.userSelect = "none";
|
||||
resizeHandle.setPointerCapture?.(event.pointerId);
|
||||
});
|
||||
|
||||
resizeHandle.addEventListener("pointermove", (event) => {
|
||||
const onMove = (event) => {
|
||||
if (!resizing) return;
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
const panelRect = panel.getBoundingClientRect();
|
||||
const currentLeft = panelRect.left - containerRect.left;
|
||||
const currentTop = panelRect.top - containerRect.top;
|
||||
const hudScale = getHudScale();
|
||||
const minWidth = Math.max(320, Math.round(360 * hudScale));
|
||||
const minHeight = Math.max(260, Math.round(340 * hudScale));
|
||||
const maxWidth = Math.max(minWidth, containerRect.width - currentLeft - 12);
|
||||
const maxHeight = Math.max(minHeight, containerRect.height - currentTop - 12);
|
||||
const nextWidth = Math.min(
|
||||
maxWidth,
|
||||
Math.max(minWidth, startWidth + (event.clientX - startX)),
|
||||
);
|
||||
const nextHeight = Math.min(
|
||||
maxHeight,
|
||||
Math.max(minHeight, startHeight + (event.clientY - startY)),
|
||||
);
|
||||
const dx = event.clientX - startX;
|
||||
const dy = event.clientY - startY;
|
||||
|
||||
panel.style.width = `${nextWidth}px`;
|
||||
panel.style.minHeight = `${nextHeight}px`;
|
||||
if (activeEdge.includes("r")) {
|
||||
const maxW = containerRect.width - startLeft - 12;
|
||||
panel.style.width = `${Math.min(maxW, Math.max(minWidth, startWidth + dx))}px`;
|
||||
}
|
||||
if (activeEdge.includes("l")) {
|
||||
const newW = Math.max(minWidth, startWidth - dx);
|
||||
panel.style.width = `${newW}px`;
|
||||
panel.style.left = `${Math.max(0, startLeft + startWidth - newW)}px`;
|
||||
}
|
||||
if (activeEdge.includes("b")) {
|
||||
const maxH = containerRect.height - startTop - 12;
|
||||
panel.style.minHeight = `${Math.min(maxH, Math.max(minHeight, startHeight + dy))}px`;
|
||||
}
|
||||
};
|
||||
|
||||
panel.querySelectorAll(".tv-panel-edge[data-edge]").forEach((edgeEl) => {
|
||||
edgeEl.addEventListener("pointerdown", (event) => {
|
||||
if (container.classList.contains("layout-expanded")) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
activeEdge = edgeEl.dataset.edge ?? "";
|
||||
resizing = true;
|
||||
startX = event.clientX;
|
||||
startY = event.clientY;
|
||||
|
||||
clearPanelPositioningForResize(panel);
|
||||
|
||||
const rect = panel.getBoundingClientRect();
|
||||
const cRect = container.getBoundingClientRect();
|
||||
startWidth = rect.width;
|
||||
startHeight = rect.height;
|
||||
startLeft = rect.left - cRect.left;
|
||||
startTop = rect.top - cRect.top;
|
||||
|
||||
panel.classList.add("is-resizing");
|
||||
document.body.style.userSelect = "none";
|
||||
edgeEl.setPointerCapture?.(event.pointerId);
|
||||
});
|
||||
|
||||
edgeEl.addEventListener("pointermove", onMove);
|
||||
edgeEl.addEventListener("pointerup", stopResize);
|
||||
edgeEl.addEventListener("pointercancel", stopResize);
|
||||
edgeEl.addEventListener("lostpointercapture", stopResize);
|
||||
});
|
||||
|
||||
resizeHandle.addEventListener("pointerup", stopResize);
|
||||
resizeHandle.addEventListener("pointercancel", stopResize);
|
||||
resizeHandle.addEventListener("lostpointercapture", stopResize);
|
||||
}
|
||||
|
||||
function updateToggleButton(visible) {
|
||||
@@ -502,6 +556,7 @@ function renderSource(source) {
|
||||
source.id === tvPayload?.default_source_id ? "当前正在播放默认源" : "当前正在播放已选频道",
|
||||
);
|
||||
updateOpenButton(source);
|
||||
autoExpandMeta();
|
||||
}
|
||||
|
||||
function resolveInitialSourceId() {
|
||||
@@ -592,6 +647,13 @@ export function initTVPanel() {
|
||||
renderSource(findSourceById(currentSourceId));
|
||||
});
|
||||
|
||||
const { metaToggle } = getElements();
|
||||
metaToggle?.addEventListener("click", () => {
|
||||
clearTimeout(metaAutoCollapseTimer);
|
||||
const isNowCollapsed = !metaToggle.classList.contains("is-collapsed");
|
||||
setMetaCollapsed(isNowCollapsed);
|
||||
});
|
||||
|
||||
refreshBtn?.addEventListener("click", () => {
|
||||
refreshTVPanel();
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "planet"
|
||||
version = "0.27.0"
|
||||
version = "0.27.4"
|
||||
description = "智能星球计划 - 态势感知系统"
|
||||
requires-python = ">=3.14"
|
||||
dependencies = [
|
||||
|
||||
Reference in New Issue
Block a user