Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5602949a4f | ||
|
|
9e5e959f80 | ||
|
|
309de439e8 | ||
|
|
a381d7be03 | ||
|
|
14f5963706 |
No files matched your search
@@ -10,7 +10,7 @@ Electron + electron-vite + React 终端工具(本地终端 / SSH / SFTP)。
|
||||
- 测试:`npm test`(**npm 生命周期先自动跑 `pretest` 做类型检查**,再 `node tests/build-bundles.cjs` 重建 esbuild bundle,然后依次跑可离线运行的 18 个测试:ssh-loopback、commands-store、connections-store、settings-store、local-path-grants、lock-store、lock-controller、lock-shortcuts、hl-split-smoke、hl-rules、reserved-accelerators、ipc-guard、updater-fallback、log-sanitizer、sftp-timeout、terminal-title、zmodem-e2e、ssh-session-e2e、sysinfo-e2e;真实服务器测试需 JD_* 凭据,不在此列)
|
||||
- 依赖分类规则:**只有 `src/main/`/`src/preload/` 实际 import 的包才能进 `dependencies`**(node-pty/ssh2/zmodem.js/font-list/electron-updater);纯渲染层依赖一律 devDependencies(Vite 全量打包进 out/renderer,`externalizeDepsPlugin` 不作用渲染层)——这条让 asar 从 98MB 瘦到 8.1MB,加新依赖时别放错边
|
||||
- 下载量统计:`node scripts/download-stats.cjs`(Gitea + GitHub release 资产的 download_count 汇总;GitHub 优先直连、失败自动回退 `HTTPS_PROXY`/本地 7897;更新通道无计数接口不计入)
|
||||
- 打包:`npm run dist`(**生命周期先自动跑 `predist` → `npm test`,即类型检查 + 18 个离线测试全部通过后才 build/package**,typecheck 全程只跑一次;predist 末尾的 `npm install --package-lock-only` 会把 `package-lock.json` 根版本号对齐 `package.json`,**发布提交必须带上 package-lock.json**),产物在 `release/`(msi + exe + latest.yml + blockmap)
|
||||
- 打包:`npm run dist`(**生命周期先自动跑 `predist` → `npm test`,即类型检查 + 18 个离线测试全部通过后才 build/package**,typecheck 全程只跑一次;predist 末尾的 `npm install --package-lock-only` 会把 `package-lock.json` 根版本号对齐 `package.json`,**发布提交必须带上 package-lock.json**),产物在 `release/`(exe + latest.yml + blockmap)
|
||||
- GitHub Actions:`.github/workflows/ci.yml` 在 windows-latest + Node 22 上跑 `npm ci` / `npm test`(含 pretest typecheck)/ `npm run build`,只做验证,不打包安装器、不发布
|
||||
- 国内网络需镜像:`ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ ELECTRON_BUILDER_BINARIES_MIRROR=https://npmmirror.com/mirrors/electron-builder-binaries/ npm run dist`
|
||||
- 依赖一致性闸门:`predist` 开头跑 `node scripts/verify-deps.cjs`(逐条比对 `node_modules` 与 `package-lock.json` 的版本,跨平台 optional 依赖缺失跳过;不一致就 exit 1)——`npm install --package-lock-only` **只重算 lockfile、不碰 node_modules**(本版 npm 还会把 `node_modules/.package-lock.json` 一并重写成理想树,制造「已经装好了」的假象),所以**改动依赖版本后必须真实 `npm install` 或 `npm ci` 再构建**,别指望 lockfile 对齐就等于树里换了包;v1.0.21 的中文输入法回归正是这个坑(`@xterm/xterm` 锁 6.1.0-beta.304,树里还是 6.0.0,打进去的是旧代码)
|
||||
@@ -34,20 +34,20 @@ Electron + electron-vite + React 终端工具(本地终端 / SSH / SFTP)。
|
||||
2. `node scripts/sync-changelog.cjs` 把发布说明并入 `CHANGELOG*.md` —— **必须在 `npm run dist` 之前**:更新日志以 `?raw` 打进安装包(「关于」页离线读),`release.cjs` 也校验 `CHANGELOG.md` 已含该版本号
|
||||
3. `npm run dist` 构建
|
||||
4. `node scripts/release.cjs <版本号>`,例如 `node scripts/release.cjs 1.0.14`(**不带 skip 参数**,Gitea 与 GitHub 一起发)
|
||||
- Gitea:release(msi + exe 资产)→ Gitea 更新通道(`api/packages/admin/generic/openterminal-update/stable`:exe.blockmap → exe → release-notes.md → **latest.yml 最后**)
|
||||
- Gitea:release(exe 资产)→ Gitea 更新通道(`api/packages/admin/generic/openterminal-update/stable`:exe.blockmap → exe → release-notes.md → **latest.yml 最后**)
|
||||
- GitHub:release 资产再次随版本同步发布(exe + exe.blockmap + latest.yml),electron-updater 标准 GitHub provider 直接吃 release 资产
|
||||
- 通道不再先删旧版:新版本文件全部传完、latest.yml 生效后才清掉上一版 exe/blockmap,中途失败不会把通道打空;同一版本可重复运行(release 复用、已传资产跳过)
|
||||
- 上传前先比通道版本:`assertNoDowngrade()` 读通道 latest.yml,若线上版本**高于**待发布版本就直接拒绝——三条本地护栏只比本地产物,旧分支发旧版本号会一路通过,覆盖 latest.yml 之后 `pruneChannel` 会把线上新版本的 exe/blockmap 删掉
|
||||
- 只补通道:`node scripts/release.cjs <版本号> --channel-only`(不建 release、不发 GitHub)
|
||||
- 国内通道全程直连,**不需要设代理**;GitHub 请求走 `HTTPS_PROXY=http://127.0.0.1:7897`(脚本只把它用于 GitHub 请求)
|
||||
5. 验证更新通道:`curl https://git.codingplan.site/api/packages/admin/generic/openterminal-update/stable/latest.yml` 应返回新版本号
|
||||
6. `git tag v<版本号>` 并推送两个远程(代码/tag 与 release 资产的镜像保持同步)
|
||||
6. `git tag v<版本号>` 并推送两个远程(代码/tag 与 release 资产的镜像保持同步)。注意顺序坑:release.cjs 创建 release 时若远端尚无该 tag,Gitea/GitHub 会在**默认分支 HEAD** 自动建一个指向错误 commit 的 tag,第 6 步推送会被拒——要么先建 tag 推上去再跑 release.cjs,要么事后 `git push -f <远端> v<版本号>` 强制修正到 release commit(v1.0.22 即踩过)
|
||||
|
||||
## 更新机制
|
||||
|
||||
- 检查更新:**GitHub 优先**(走系统代理;前置 20 秒连通性探测 `probeGithub`——探测失败/超时直接兜底 Gitea,不给 electron-updater 挂起的机会),失败回退国内 Gitea 通用包通道(强制直连,不走系统代理)。 electron-updater 用独立 session(partition `electron-updater`),代理模式在 `useFeed` 里按源切换
|
||||
- 更新日志:Gitea 仓库是私有的(匿名 API 404),改为从更新通道的 `release-notes.md` 读取(直连 session `openterminal-update-direct`),再回退 Gitea/GitHub releases API
|
||||
- electron-updater 不支持 MSI 自动更新,自动更新只走 NSIS exe
|
||||
- 自动更新只走 NSIS exe;自 v1.0.22 起不再构建 MSI 安装包(electron-updater 本就不支持 MSI 自动更新)
|
||||
- 每次 checkForUpdates 都被 **30s 整体超时**包住(`withTimeout`):electron-updater 自带的 60s 只是 socket 空闲超时,慢滴流响应能一直占住它。超时按普通失败走 GitHub→Gitea 回退,但**被放弃的检查取消不掉**,而 electron-updater 对并发检查去重(返回同一个 in-flight promise),所以兜底那次共用被放弃的 promise——它同样被超时兜住,最终落到错误态,不会永远停在「检查中」
|
||||
- 更新日志 / releases API 的 fetch 都带 `AbortSignal.timeout(15s)`(`directFetch` 与 `fetchChangelog` 里的 `net.fetch`):卡住的通道必须让位给下一个来源,不能把「关于」页吊住
|
||||
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
### IME
|
||||
- **Fixed: Chinese IME candidate window not following the caret** (regression in v1.0.21): the v1.0.21 build environment had a terminal component dependency that did not match the locked version, so the installer shipped an older build without the upstream IME fix; this release restores the terminal component version used by v1.0.20, and the candidate window follows the caret again.
|
||||
|
||||
### Installer
|
||||
- The MSI installer is discontinued (electron-updater never supported MSI auto-updates); the NSIS exe is now the only installer format.
|
||||
|
||||
### Internal
|
||||
- The packaging pipeline now verifies dependency consistency before building: installed dependencies are compared against the lockfile and the build aborts on any mismatch, preventing this class of issue from recurring.
|
||||
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
### IME
|
||||
- **中国語 IME の候補ウィンドウがキャレットに追従しない問題を修正**(v1.0.21 でのリグレッション):v1.0.21 のビルド環境では端末コンポーネントの依存がロックされたバージョンと一致しておらず、インストーラーには上流の IME 修正を含まない古いビルドが同梱されていました。本バージョンでは v1.0.20 と同じ端末コンポーネントに戻し、候補ウィンドウが再びキャレットに追従します。
|
||||
|
||||
### インストーラー
|
||||
- MSI インストーラーの提供を終了しました(electron-updater は MSI の自動更新に非対応のため)。今後は NSIS exe インストーラーに統一されます。
|
||||
|
||||
### 内部
|
||||
- パッケージング前に依存関係の整合性チェックを追加:インストール済み依存とロックファイルを照合し、不一致があればビルドを中止します。同種の問題の再発を防ぎます。
|
||||
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
### 输入法
|
||||
- **修复中文输入法候选窗不跟随光标**(v1.0.21 回归):v1.0.21 的构建环境中终端组件依赖未按锁定版本安装,安装包实际打进的是不含上游输入法修复的旧版本;本版本恢复 v1.0.20 所用的终端组件版本,候选窗重新紧贴光标。
|
||||
|
||||
### 安装包
|
||||
- 不再提供 MSI 安装包(electron-updater 本就不支持 MSI 自动更新),统一使用 NSIS exe 安装包。
|
||||
|
||||
### 内部
|
||||
- 打包流程新增依赖一致性校验:构建前自动比对实际安装的依赖与锁定清单,不一致即中止,防止同类问题再次发生。
|
||||
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
### 輸入法
|
||||
- **修復中文輸入法候選視窗不跟隨游標**(v1.0.21 回歸):v1.0.21 的建置環境中終端機元件相依未按鎖定版本安裝,安裝包實際打包的是不含上游輸入法修復的舊版本;本版本恢復 v1.0.20 所用的終端機元件版本,候選視窗重新緊貼游標。
|
||||
|
||||
### 安裝包
|
||||
- 不再提供 MSI 安裝包(electron-updater 本就不支援 MSI 自動更新),統一使用 NSIS exe 安裝包。
|
||||
|
||||
### 內部
|
||||
- 打包流程新增相依一致性校驗:建置前自動比對實際安裝的相依與鎖定清單,不一致即中止,防止同類問題再次發生。
|
||||
|
||||
|
||||
@@ -4,6 +4,25 @@
|
||||
|
||||
> 产品官网(GitHub Pages):<https://billowliu2.github.io/OpenTerminal/>,源码在 [`website/`](website/)。
|
||||
|
||||
## 截图
|
||||
|
||||
| 多路分屏并行 AI 编程 Agent | 无背景图的干净分屏 |
|
||||
| --- | --- |
|
||||
|  |  |
|
||||
|
||||
| 主题与个性化(背景图 / 不透明度 / 压暗层) | 锁屏(Ctrl+L,遮罩下会话不断线) |
|
||||
| --- | --- |
|
||||
|  |  |
|
||||
|
||||
## 优势
|
||||
|
||||
- **轻量**:渲染层依赖全量打包进 bundle、不重复进安装包,应用本体 asar 仅 8.1MB,NSIS 安装包约 109MB;单一 exe 安装包,应用内自动更新
|
||||
- **隐私与安全**:SSH 凭据经 DPAPI 加密落盘、渲染层接触不到明文;锁屏密码只存 scrypt 派生值;SFTP/ZMODEM 的本地路径必须经系统对话框授权,阻断构造路径读写
|
||||
- **性能**:xterm.js WebGL 渲染;关键词高亮引擎实测 0.02–0.05ms/KB(200KB 混合输出 4–9ms);SFTP 大目录虚拟滚动;分屏尺寸去抖同步,TUI 不反复重绘
|
||||
- **AI 编程友好**:为 Kimi Code / Claude Code / Codex 等高频刷新 TUI 优化;中文输入法候选窗紧贴光标
|
||||
- **国内可用**:更新检查 GitHub 优先(跟随系统代理),失败自动回退国内直连通道,两边都拿不到时 30s 整体超时兜底
|
||||
- **开源可审计**:MIT 协议;主/预载/渲染三端契约集中在 `src/shared/`,模块间零横向依赖;19 个离线测试覆盖存储、锁屏、高亮、传输与更新回退
|
||||
|
||||
## 快速开始
|
||||
|
||||
```bash
|
||||
@@ -11,7 +30,7 @@ npm install # 已安装可跳过
|
||||
npm run dev # 开发模式(热更新)
|
||||
npm run build # 生产构建到 out/
|
||||
npm run typecheck # 全量类型检查
|
||||
npm test # 先自动跑 typecheck,再重建测试 bundle 并跑 12 个离线测试(详见「测试」)
|
||||
npm test # 先自动跑 typecheck,再重建测试 bundle 并跑 19 个离线测试(详见「测试」)
|
||||
```
|
||||
|
||||
> Electron 二进制下载失败时:`ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ node node_modules/electron/install.js`
|
||||
@@ -60,12 +79,12 @@ npm test # 先自动跑 typecheck,再重建测试 bundle 并跑 12
|
||||
- 锁定期间吞掉 F5 / Ctrl+R / Ctrl+±0 / Ctrl+Shift+I/J/C,并整块摘除应用菜单,防止菜单键绕过遮罩
|
||||
|
||||
**外观与窗口**
|
||||
- 12 款内置主题(Dracula / One Half / Solarized / Gruvbox / Nord / Monokai / GitHub 等)
|
||||
- 11 款内置主题(Dracula / One Half / Solarized / Gruvbox / Nord / Monokai / GitHub 等)
|
||||
- 自定义主题编辑器:16 色 ANSI 调色板 + 前景/背景/光标/选区色,实时预览
|
||||
- 窗口标题栏与背景跟随终端主题
|
||||
- 系统托盘:关闭按钮可设「每次询问 / 最小化到托盘 / 直接退出」,托盘菜单可切换
|
||||
- 单实例运行:重复启动唤出已有窗口
|
||||
- 自动更新:GitHub 优先(走系统代理),失败回退国内 Gitea 更新通道(强制直连);单次检查 30s 整体超时,MSI 仅分发、自动更新走 NSIS 安装包
|
||||
- 自动更新:GitHub 优先(走系统代理),失败回退国内 Gitea 更新通道(强制直连);单次检查 30s 整体超时;统一使用 NSIS exe 安装包(自 v1.0.22 起不再提供 MSI)
|
||||
- 设置持久化(userData/settings.json,原子写入)+ 多窗口实时同步
|
||||
|
||||
**界面与语言**
|
||||
@@ -74,15 +93,22 @@ npm test # 先自动跑 typecheck,再重建测试 bundle 并跑 12
|
||||
|
||||
**测试**
|
||||
|
||||
`npm test` 会先跑 `pretest`(= `npm run typecheck`),再重建 esbuild bundle,最后依次跑下列 12 个测试(全部离线,无需服务器与凭据):
|
||||
`npm test` 会先跑 `pretest`(= `npm run typecheck`),再重建 esbuild bundle,最后依次跑下列 19 个测试(全部离线,无需服务器与凭据):
|
||||
|
||||
- `node tests/ssh-loopback.mjs` — ssh2 客户端/服务端回环(认证、shell、数据、resize、指纹)
|
||||
- `node tests/commands-store.mjs` — 命令库 / 历史 / 会话日志存储
|
||||
- `node tests/connections-store.mjs` — SSH 书签 CRUD、公开/密文字段切分、损坏文件备份
|
||||
- `node tests/settings-store.mjs` — 设置清洗器(closeAction/高亮规则修复/旧预设升级/告警日志)
|
||||
- `node tests/local-path-grants.mjs` — 本地路径准入(对话框授权、realpath+stat 双重校验、大小写折叠)
|
||||
- `node tests/lock-store.mjs` — 锁屏密码校验器(scrypt 往返、文件损坏处理)
|
||||
- `node tests/lock-controller.mjs` — 锁屏控制器(冷却阶梯、并发串行化、落盘恢复、闲置触发、清除联动)
|
||||
- `node tests/lock-shortcuts.mjs` — 锁屏快捷键分类器(表驱动:Ctrl+L 恐慌锁 / 锁定时禁用的组合键)
|
||||
- `node tests/reserved-accelerators.mjs` — 保留快捷键表(设置录制器与全局注册共用同一张表)
|
||||
- `node tests/.terminal-title.cjs` — 终端标签自动标题(四语言反解与重渲染)
|
||||
- `node tests/ipc-guard.mjs` — IPC 发送方守卫(仅信任本应用渲染帧)
|
||||
- `node tests/updater-fallback.mjs` — 更新通道回退(GitHub 优先 / Gitea 兜底、超时、防降级)
|
||||
- `node tests/log-sanitizer.mjs` — 会话日志脱敏(原始 PTY 字节流转可读文本)
|
||||
- `node tests/sftp-timeout.mjs` — SFTP 操作超时(半死通道驱逐与重试)
|
||||
- `node tests/.hl-split-smoke.cjs` — 关键词高亮流分块回归(bundle 由 `node tests/build-bundles.cjs` 生成)
|
||||
- `node tests/.hl-rules.cjs` — 内置高亮预设(词边界、大小写、负向词、危险命令)
|
||||
- `node tests/zmodem-e2e.mjs` — ZMODEM 双向传输(与第二个 zmodem.js Sentry 对接,内容一致性)
|
||||
|
||||
@@ -16,23 +16,12 @@ asarUnpack:
|
||||
# @lydell/node-pty ships prebuilt binaries; do not trigger a rebuild.
|
||||
npmRebuild: false
|
||||
|
||||
# Electron-updater only supports NSIS auto-updates (MSI has no update channel).
|
||||
# The NSIS .exe + latest.yml get uploaded together; MSI is manual/enterprise only.
|
||||
# NSIS exe is the only installer: electron-updater only supports NSIS
|
||||
# auto-updates, so the MSI (no update channel) was dropped in v1.0.22.
|
||||
win:
|
||||
target:
|
||||
- target: msi
|
||||
- target: nsis
|
||||
|
||||
msi:
|
||||
oneClick: false
|
||||
# Fixed upgrade code keeps the upgrade chain consistent across releases.
|
||||
upgradeCode: "{5ab9f79e-e4eb-4052-9df6-3af3a301ab0a}"
|
||||
artifactName: ${productName}-${version}-setup.${ext}
|
||||
# Shortcuts need an <Icon>; build/icon.png (auto-converted to .ico) provides it.
|
||||
# Without an icon the WiX shortcut Icon reference fails to link (LGHT0094).
|
||||
createDesktopShortcut: true
|
||||
createStartMenuShortcut: true
|
||||
|
||||
nsis:
|
||||
oneClick: false
|
||||
allowToChangeInstallationDirectory: true
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"pretest": "npm run typecheck",
|
||||
"test": "node tests/build-bundles.cjs && node tests/ssh-loopback.mjs && node tests/commands-store.mjs && node tests/connections-store.mjs && node tests/settings-store.mjs && node tests/local-path-grants.mjs && node tests/lock-store.mjs && node tests/lock-controller.mjs && node tests/lock-shortcuts.mjs && node tests/reserved-accelerators.mjs && node tests/.terminal-title.cjs && node tests/ipc-guard.mjs && node tests/updater-fallback.mjs && node tests/log-sanitizer.mjs && node tests/sftp-timeout.mjs && node tests/.hl-split-smoke.cjs && node tests/.hl-rules.cjs && node tests/zmodem-e2e.mjs && node tests/ssh-session-e2e.mjs && node tests/sysinfo-e2e.mjs",
|
||||
"predist": "node scripts/verify-deps.cjs && npm test && npm install --package-lock-only",
|
||||
"dist": "electron-vite build && electron-builder --win msi nsis",
|
||||
"dist": "electron-vite build && electron-builder --win nsis",
|
||||
"dist:dir": "electron-vite build && electron-builder --win --dir"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -42,14 +42,12 @@ const env = Object.fromEntries(
|
||||
)
|
||||
const notes = fs.readFileSync(path.join(ROOT, 'RELEASE_NOTES.md'), 'utf8')
|
||||
const R = path.join(ROOT, 'release')
|
||||
const msi = path.join(R, `OpenTerminal-${V}-setup.msi`)
|
||||
const exe = path.join(R, `OpenTerminal-${V}-setup.exe`)
|
||||
const files = [msi, exe]
|
||||
// --channel-only republishes the update channel, which carries the NSIS exe and
|
||||
// its blockmap (electron-updater cannot consume an MSI) — the msi is a release
|
||||
// asset only, so demanding it here would block the repair path this mode exists
|
||||
// for. The channel's own file list is validated in giteaChannel().
|
||||
for (const f of CHANNEL_ONLY ? [exe] : files) {
|
||||
const files = [exe]
|
||||
// Only the NSIS exe is built (the MSI was dropped in v1.0.22 — electron-updater
|
||||
// never supported it). The channel's own file list is validated in
|
||||
// giteaChannel().
|
||||
for (const f of files) {
|
||||
if (!fs.existsSync(f)) { console.error('missing build artifact:', f); process.exit(1) }
|
||||
}
|
||||
// Guard rails: the artifacts must belong to the version being published —
|
||||
|
||||
@@ -14,20 +14,24 @@ website/
|
||||
├── index.html # 单页站点:功能介绍 / 下载 / 更新日志
|
||||
├── .nojekyll # 让 GitHub Pages 原样托管(跳过 Jekyll 处理)
|
||||
└── assets/
|
||||
├── style.css # 全部样式(企业风浅色主题,单一强调色)
|
||||
├── shot-terminal.png # 主界面截图(四路分屏)
|
||||
├── shot-themes.png # 主题设置页截图
|
||||
├── style.css # 全部样式(深色默认 / 浅色可切换,主题令牌在 :root 与 html[data-theme='light'])
|
||||
├── fonts/ # 自托管 woff2:Space Grotesk 400/500/600/700、JetBrains Mono 400/500/600
|
||||
├── shot-terminal.jpg # 主界面截图(三路分屏并行 AI 编程 Agent,含背景图个性化)
|
||||
├── shot-agents.png # 多路 AI Agent 分屏截图(无背景图版)
|
||||
├── shot-sftp.png # SFTP 文件面板:列头、排序、用户/组、字体缩放
|
||||
├── shot-sftp-menu.png # 文件列表空白处右键菜单
|
||||
├── shot-themes.jpg # 主题设置页截图(背景图/不透明度/压暗层)
|
||||
├── shot-lock.png # 锁屏界面截图
|
||||
└── icon.png # 应用图标(来自仓库 build/icon.png)
|
||||
```
|
||||
|
||||
## 发新版本时需要更新的地方
|
||||
|
||||
`index.html` 中搜索 `1.0.15`,共 3 处下载链接(hero 主按钮、下载表格 exe、msi)。
|
||||
下载链接使用 GitHub Releases 原生地址:
|
||||
`index.html` 中搜索当前版本号,共 2 处下载链接(hero 主按钮、下载表格 exe)。
|
||||
自 v1.0.22 起不再提供 MSI,只有 NSIS exe 一条直链:
|
||||
|
||||
```
|
||||
https://github.com/billowliu2/OpenTerminal/releases/download/v<版本>/OpenTerminal-<版本>-setup.exe
|
||||
https://github.com/billowliu2/OpenTerminal/releases/download/v<版本>/OpenTerminal-<版本>-setup.msi
|
||||
```
|
||||
|
||||
注意:发布新版本时需同时把安装包资产上传到 GitHub release
|
||||
@@ -61,7 +65,10 @@ npx serve website
|
||||
|
||||
## 设计约定
|
||||
|
||||
- 浅色企业风,避免营销站常见的霓虹/渐变装饰
|
||||
- 强调色仅一个:`#1a7f37`(应用品牌绿 #3fb950 的深色化,保证浅底对比度 ≥ 4.5:1)
|
||||
- 截图来自真实运行界面,与应用内设置一致
|
||||
- 中文为主要语言;字体使用系统栈(Segoe UI / 微软雅黑),不加载外部字体
|
||||
- **深色为主**:默认深色(`#0b0e12` 底 + 终端绿强调色),右上角按钮可切浅色;两套主题共用同一份令牌,新增样式一律走 CSS 变量,不要写死颜色
|
||||
- 主题选择存 `localStorage['openterminal.theme']`,首帧前由 `index.html` 头部的内联脚本定好,避免闪白/闪黑
|
||||
- 全站只用**一个强调色**:深底 `#3fb950`、浅底 `#1a7f37`(对比度 ≥ 4.5:1)
|
||||
- 字体自托管(`assets/fonts/`),不依赖 Google Fonts:展示字体 Space Grotesk、等宽 JetBrains Mono;中文回落到系统栈
|
||||
- 「核心能力」是横向滚动卡片轨道(箭头翻页 + 拖拽 + 键盘方向键),不再用三栏等高卡片
|
||||
- 截图来自真实运行界面,与应用内设置一致;涉及 IP / 主机名等敏感信息的截图必须打码后再放入 `assets/`
|
||||
- 中文为主要语言;文案保持具体、平实,避免营销套话
|
||||
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 662 KiB |
|
After Width: | Height: | Size: 629 KiB |
|
After Width: | Height: | Size: 315 KiB |
|
Before Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 201 KiB |
|
Before Width: | Height: | Size: 101 KiB |
@@ -1,17 +1,140 @@
|
||||
/* OpenTerminal 官网 - 企业风浅色主题
|
||||
单一强调色: 品牌绿 #1a7f37 (深化的 #3fb950, 保证浅底对比度)
|
||||
无渐变 / 无发光 / 无装饰性动画, 仅保留 hover 与 focus 反馈 */
|
||||
/* OpenTerminal 官网 —— 双主题 (深色默认 / 浅色)
|
||||
排版: Space Grotesk (展示) + JetBrains Mono (等宽), 自托管 woff2
|
||||
单一强调色: 终端绿 (深底 #3fb950, 浅底 #1a7f37) */
|
||||
|
||||
@font-face {
|
||||
font-family: 'Space Grotesk';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('assets/fonts/space-grotesk-400.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Space Grotesk';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url('assets/fonts/space-grotesk-500.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Space Grotesk';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
src: url('assets/fonts/space-grotesk-600.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Space Grotesk';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url('assets/fonts/space-grotesk-700.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'JetBrains Mono';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('assets/fonts/jetbrains-mono-400.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'JetBrains Mono';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url('assets/fonts/jetbrains-mono-500.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'JetBrains Mono';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
src: url('assets/fonts/jetbrains-mono-600.woff2') format('woff2');
|
||||
}
|
||||
|
||||
/* ---- 主题令牌: 深色为默认 ---- */
|
||||
:root {
|
||||
--bg: #ffffff;
|
||||
--bg-soft: #f7f8f8;
|
||||
--text: #1c2128;
|
||||
--text-2: #57606a;
|
||||
--border: #d9dee3;
|
||||
color-scheme: dark;
|
||||
|
||||
--bg: #0b0e12;
|
||||
--bg-alt: #0d1219;
|
||||
--surface: #11161e;
|
||||
--surface-2: #161c26;
|
||||
|
||||
--text: #e6ebe6;
|
||||
--text-2: #99a49c;
|
||||
--text-3: #6f7a72;
|
||||
|
||||
--border: rgba(255, 255, 255, 0.09);
|
||||
--border-strong: rgba(255, 255, 255, 0.16);
|
||||
|
||||
--accent: #3fb950;
|
||||
--accent-hover: #34a24a;
|
||||
--accent-text: #4fd067;
|
||||
--accent-soft: rgba(63, 185, 80, 0.12);
|
||||
--btn-fg: #06210d;
|
||||
|
||||
--overlay-1: rgba(255, 255, 255, 0.03);
|
||||
--overlay-2: rgba(255, 255, 255, 0.06);
|
||||
--overlay-3: rgba(255, 255, 255, 0.08);
|
||||
--overlay-row: rgba(255, 255, 255, 0.02);
|
||||
|
||||
--grid-line: rgba(255, 255, 255, 0.03);
|
||||
--grid-line-soft: rgba(255, 255, 255, 0.022);
|
||||
--nav-bg: rgba(11, 14, 18, 0.82);
|
||||
|
||||
--glow-1: rgba(63, 185, 80, 0.15);
|
||||
--glow-2: rgba(63, 185, 80, 0.07);
|
||||
--grain: 0.045;
|
||||
|
||||
--shadow-card: 0 1px 0 rgba(255, 255, 255, 0.04) inset, 0 18px 48px -24px rgba(0, 0, 0, 0.9);
|
||||
--shadow-lift: 0 1px 0 rgba(255, 255, 255, 0.06) inset, 0 28px 64px -28px rgba(0, 0, 0, 1);
|
||||
--shadow-shot: 0 2px 10px rgba(0, 0, 0, 0.5), 0 36px 88px -32px rgba(0, 0, 0, 0.95);
|
||||
|
||||
--display: 'Space Grotesk', 'Segoe UI', 'Microsoft YaHei', sans-serif;
|
||||
--mono: 'JetBrains Mono', ui-monospace, 'Cascadia Mono', Consolas, monospace;
|
||||
|
||||
--radius: 10px;
|
||||
--radius-lg: 14px;
|
||||
}
|
||||
|
||||
html[data-theme='light'] {
|
||||
color-scheme: light;
|
||||
|
||||
--bg: #fafbf9;
|
||||
--bg-alt: #f1f4f0;
|
||||
--surface: #ffffff;
|
||||
--surface-2: #fbfdfa;
|
||||
|
||||
--text: #151a16;
|
||||
--text-2: #59615a;
|
||||
--text-3: #8b948c;
|
||||
|
||||
--border: #e1e6df;
|
||||
--border-strong: #cbd3ca;
|
||||
|
||||
--accent: #1a7f37;
|
||||
--accent-hover: #116329;
|
||||
--radius: 8px;
|
||||
--mono: ui-monospace, "Cascadia Mono", "SF Mono", Consolas, monospace;
|
||||
--accent-hover: #146227;
|
||||
--accent-text: #1a7f37;
|
||||
--accent-soft: rgba(26, 127, 55, 0.09);
|
||||
--btn-fg: #ffffff;
|
||||
|
||||
--overlay-1: rgba(21, 26, 22, 0.03);
|
||||
--overlay-2: rgba(21, 26, 22, 0.05);
|
||||
--overlay-3: rgba(21, 26, 22, 0.06);
|
||||
--overlay-row: rgba(21, 26, 22, 0.02);
|
||||
|
||||
--grid-line: rgba(21, 26, 22, 0.05);
|
||||
--grid-line-soft: rgba(21, 26, 22, 0.035);
|
||||
--nav-bg: rgba(250, 251, 249, 0.86);
|
||||
|
||||
--glow-1: rgba(26, 127, 55, 0.1);
|
||||
--glow-2: rgba(26, 127, 55, 0.05);
|
||||
--grain: 0.02;
|
||||
|
||||
--shadow-card: 0 1px 2px rgba(23, 32, 24, 0.06), 0 18px 40px -24px rgba(23, 32, 24, 0.22);
|
||||
--shadow-lift: 0 2px 4px rgba(23, 32, 24, 0.08), 0 28px 56px -26px rgba(23, 32, 24, 0.28);
|
||||
--shadow-shot: 0 2px 8px rgba(23, 32, 24, 0.12), 0 32px 72px -32px rgba(23, 32, 24, 0.35);
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
@@ -19,28 +142,63 @@
|
||||
html { scroll-behavior: smooth; }
|
||||
|
||||
body {
|
||||
font-family: "Segoe UI", "Microsoft YaHei", -apple-system, "PingFang SC", "Noto Sans SC", sans-serif;
|
||||
font-family: 'Segoe UI', 'Microsoft YaHei', -apple-system, 'PingFang SC', 'Noto Sans SC', sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
line-height: 1.65;
|
||||
color: var(--text);
|
||||
background: var(--bg);
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1120px;
|
||||
max-width: 1180px;
|
||||
margin: 0 auto;
|
||||
padding: 0 24px;
|
||||
padding: 0 28px;
|
||||
}
|
||||
|
||||
.mono { font-family: var(--mono); font-size: 0.92em; }
|
||||
.mono { font-family: var(--mono); font-variant-numeric: tabular-nums; }
|
||||
|
||||
h1, h2, h3 {
|
||||
font-family: var(--display);
|
||||
font-weight: 700;
|
||||
text-wrap: balance;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
p { text-wrap: pretty; }
|
||||
|
||||
img { max-width: 100%; }
|
||||
|
||||
a:focus-visible,
|
||||
button:focus-visible {
|
||||
outline: 2px solid var(--accent-text);
|
||||
outline-offset: 3px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.skip-link {
|
||||
position: absolute;
|
||||
left: -9999px;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
background: var(--accent);
|
||||
color: var(--btn-fg);
|
||||
padding: 10px 18px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.skip-link:focus { left: 12px; top: 12px; }
|
||||
|
||||
section[id], .site-nav { scroll-margin-top: 76px; }
|
||||
|
||||
/* ---- nav ---- */
|
||||
.site-nav {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
background: rgba(255, 255, 255, 0.94);
|
||||
z-index: 30;
|
||||
background: var(--nav-bg);
|
||||
backdrop-filter: blur(14px);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
@@ -48,58 +206,128 @@ body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 64px;
|
||||
gap: 16px;
|
||||
height: 62px;
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-family: var(--display);
|
||||
font-weight: 600;
|
||||
font-size: 17px;
|
||||
letter-spacing: -0.01em;
|
||||
color: var(--text);
|
||||
text-decoration: none;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.nav-links { display: flex; gap: 28px; }
|
||||
.brand img { border-radius: 6px; }
|
||||
|
||||
.nav-right { display: flex; align-items: center; gap: 10px; }
|
||||
|
||||
.nav-links { display: flex; gap: 2px; }
|
||||
|
||||
.nav-links a {
|
||||
color: var(--text-2);
|
||||
text-decoration: none;
|
||||
font-size: 15px;
|
||||
font-size: 14.5px;
|
||||
padding: 7px 13px;
|
||||
border-radius: 7px;
|
||||
transition: color 0.2s ease, background 0.2s ease;
|
||||
}
|
||||
|
||||
.nav-links a:hover { color: var(--text); }
|
||||
.nav-links a:hover { color: var(--text); background: var(--overlay-2); }
|
||||
.nav-links a.is-current { color: var(--text); background: var(--accent-soft); }
|
||||
|
||||
.theme-toggle {
|
||||
position: relative;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--border-strong);
|
||||
background: var(--surface);
|
||||
color: var(--text-2);
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
transition: color 0.2s ease, border-color 0.2s ease, background 0.2s ease, transform 0.15s ease;
|
||||
}
|
||||
.theme-toggle:hover { color: var(--accent-text); border-color: var(--accent); transform: translateY(-1px); }
|
||||
.theme-toggle:active { transform: translateY(0) scale(0.97); }
|
||||
|
||||
.theme-toggle svg { position: absolute; transition: opacity 0.25s ease, transform 0.35s ease; }
|
||||
.theme-toggle .icon-sun { opacity: 0; transform: rotate(-60deg) scale(0.7); }
|
||||
.theme-toggle .icon-moon { opacity: 1; transform: none; }
|
||||
|
||||
html[data-theme='light'] .theme-toggle .icon-sun { opacity: 1; transform: none; }
|
||||
html[data-theme='light'] .theme-toggle .icon-moon { opacity: 0; transform: rotate(60deg) scale(0.7); }
|
||||
|
||||
/* ---- hero ---- */
|
||||
.hero {
|
||||
padding: 72px 0 0;
|
||||
background: var(--bg-soft);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding: 92px 0 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background:
|
||||
radial-gradient(1100px 500px at 80% -15%, var(--glow-1), transparent 62%),
|
||||
radial-gradient(760px 420px at -10% 115%, var(--glow-2), transparent 58%),
|
||||
var(--bg);
|
||||
}
|
||||
|
||||
.hero::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image:
|
||||
linear-gradient(var(--grid-line) 1px, transparent 1px),
|
||||
linear-gradient(90deg, var(--grid-line) 1px, transparent 1px);
|
||||
background-size: 72px 72px;
|
||||
mask-image: radial-gradient(900px 620px at 50% 0%, #000 32%, transparent 100%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.hero::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.5'/%3E%3C/svg%3E");
|
||||
opacity: var(--grain);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.hero-grid {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1.2fr;
|
||||
gap: 48px;
|
||||
grid-template-columns: 1fr 1.12fr;
|
||||
gap: 56px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.hero-kicker {
|
||||
font-size: 14px;
|
||||
color: var(--text-2);
|
||||
margin-bottom: 16px;
|
||||
font-family: var(--mono);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent-text);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
font-size: 36px;
|
||||
line-height: 1.25;
|
||||
letter-spacing: -0.02em;
|
||||
font-weight: 650;
|
||||
font-size: clamp(34px, 4.6vw, 54px);
|
||||
line-height: 1.08;
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
|
||||
.hero h1 .accent { color: var(--accent-text); }
|
||||
|
||||
.hero-sub {
|
||||
margin-top: 20px;
|
||||
margin-top: 22px;
|
||||
max-width: 46ch;
|
||||
color: var(--text-2);
|
||||
font-size: 17px;
|
||||
@@ -108,118 +336,251 @@ body {
|
||||
.hero-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-top: 28px;
|
||||
margin-top: 32px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 10px 22px;
|
||||
padding: 11px 24px;
|
||||
border-radius: var(--radius);
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
border: 1px solid transparent;
|
||||
transition: background 0.2s ease, border-color 0.2s ease, transform 0.15s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.btn:active { transform: translateY(1px); }
|
||||
.btn:active { transform: translateY(1px) scale(0.99); }
|
||||
|
||||
.btn-primary {
|
||||
background: var(--accent);
|
||||
color: #ffffff;
|
||||
color: var(--btn-fg);
|
||||
box-shadow: 0 10px 28px -10px rgba(63, 185, 80, 0.5);
|
||||
}
|
||||
|
||||
.btn-primary:hover { background: var(--accent-hover); }
|
||||
|
||||
.btn-ghost {
|
||||
color: var(--text);
|
||||
border-color: var(--border);
|
||||
background: var(--bg);
|
||||
border-color: var(--border-strong);
|
||||
background: var(--overlay-1);
|
||||
}
|
||||
|
||||
.btn-ghost:hover { border-color: var(--text-2); }
|
||||
.btn-ghost:hover { border-color: var(--accent); background: var(--overlay-2); }
|
||||
|
||||
.hero-note {
|
||||
margin-top: 14px;
|
||||
margin-top: 16px;
|
||||
font-size: 13px;
|
||||
color: var(--text-2);
|
||||
}
|
||||
.hero-note .mono { color: var(--text); }
|
||||
|
||||
.hero-shot {
|
||||
min-width: 0;
|
||||
}
|
||||
.hero-shot { min-width: 0; }
|
||||
|
||||
.hero-shot img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: 0 8px 24px rgba(28, 33, 40, 0.10);
|
||||
border-radius: 12px;
|
||||
box-shadow: var(--shadow-shot);
|
||||
}
|
||||
|
||||
.hero-shot figcaption {
|
||||
margin-top: 10px;
|
||||
margin-top: 12px;
|
||||
font-size: 13px;
|
||||
color: var(--text-2);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.spec-strip {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
gap: 32px;
|
||||
gap: 40px;
|
||||
flex-wrap: wrap;
|
||||
padding-top: 40px;
|
||||
padding-bottom: 40px;
|
||||
padding: 40px 0 48px;
|
||||
margin-top: 48px;
|
||||
font-size: 14px;
|
||||
color: var(--text-2);
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.spec-strip strong { color: var(--text); font-weight: 600; }
|
||||
.spec-strip .mono { font-size: 13px; }
|
||||
|
||||
/* ---- features ---- */
|
||||
.features { padding: 72px 0; }
|
||||
/* ---- shared section chrome ---- */
|
||||
section { position: relative; }
|
||||
|
||||
.section-head {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: 32px;
|
||||
}
|
||||
|
||||
.section-eyebrow {
|
||||
font-family: var(--mono);
|
||||
font-size: 12.5px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent-text);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.features h2, .download h2, .changelog h2 {
|
||||
font-size: 28px;
|
||||
font-weight: 650;
|
||||
letter-spacing: -0.01em;
|
||||
font-size: clamp(26px, 3vw, 34px);
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
|
||||
.section-sub {
|
||||
margin-top: 10px;
|
||||
margin-top: 14px;
|
||||
color: var(--text-2);
|
||||
max-width: 60ch;
|
||||
max-width: 62ch;
|
||||
font-size: 16.5px;
|
||||
}
|
||||
|
||||
.grid-bg::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image:
|
||||
linear-gradient(var(--grid-line-soft) 1px, transparent 1px),
|
||||
linear-gradient(90deg, var(--grid-line-soft) 1px, transparent 1px);
|
||||
background-size: 64px 64px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.features > .container { position: relative; z-index: 1; }
|
||||
|
||||
/* ---- capability track (horizontal scroll) ---- */
|
||||
.features { padding: 92px 0 100px; background: var(--bg); }
|
||||
|
||||
.cap-controls { display: flex; gap: 8px; flex-shrink: 0; }
|
||||
|
||||
.track-btn {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--border-strong);
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease, border-color 0.2s ease, color 0.2s ease, transform 0.15s ease;
|
||||
}
|
||||
.track-btn:hover:not(:disabled) { border-color: var(--accent); color: var(--accent-text); transform: translateY(-1px); }
|
||||
.track-btn:active:not(:disabled) { transform: translateY(0) scale(0.97); }
|
||||
.track-btn:disabled { opacity: 0.35; cursor: default; }
|
||||
|
||||
.cap-track {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-top: 44px;
|
||||
padding: 4px 28px 22px;
|
||||
margin-left: -28px;
|
||||
margin-right: -28px;
|
||||
overflow-x: auto;
|
||||
overscroll-behavior-x: contain;
|
||||
scroll-snap-type: x proximity;
|
||||
scrollbar-width: none;
|
||||
cursor: grab;
|
||||
mask-image: linear-gradient(90deg, transparent 0, #000 28px, #000 calc(100% - 28px), transparent 100%);
|
||||
}
|
||||
.cap-track::-webkit-scrollbar { display: none; }
|
||||
.cap-track.is-dragging { cursor: grabbing; scroll-snap-type: none; }
|
||||
|
||||
.cap-card {
|
||||
flex: 0 0 clamp(292px, 30vw, 366px);
|
||||
scroll-snap-align: start;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
padding: 26px 24px 28px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-card);
|
||||
transition: border-color 0.25s ease, transform 0.25s ease, box-shadow 0.25s ease, background 0.25s ease;
|
||||
}
|
||||
|
||||
.cap-card:hover {
|
||||
border-color: var(--accent);
|
||||
background: var(--surface-2);
|
||||
transform: translateY(-4px);
|
||||
box-shadow: var(--shadow-lift);
|
||||
}
|
||||
|
||||
.cap-num {
|
||||
font-family: var(--mono);
|
||||
font-size: 12.5px;
|
||||
font-weight: 500;
|
||||
color: var(--accent-text);
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
.cap-card h3 { font-size: 19px; letter-spacing: -0.01em; }
|
||||
|
||||
.cap-card ul { list-style: none; display: grid; gap: 9px; margin-top: 2px; }
|
||||
|
||||
.cap-card li {
|
||||
position: relative;
|
||||
padding-left: 16px;
|
||||
font-size: 14.5px;
|
||||
line-height: 1.55;
|
||||
color: var(--text-2);
|
||||
}
|
||||
|
||||
.cap-card li::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0.66em;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 1px;
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
.cap-hint {
|
||||
margin-top: 18px;
|
||||
font-family: var(--mono);
|
||||
font-size: 12.5px;
|
||||
color: var(--text-3);
|
||||
}
|
||||
|
||||
/* ---- media rows (screenshot + copy) ---- */
|
||||
.feature-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 64px;
|
||||
margin-top: 48px;
|
||||
}
|
||||
|
||||
.media-row {
|
||||
grid-template-columns: 7fr 5fr;
|
||||
gap: 60px;
|
||||
align-items: center;
|
||||
gap: 56px;
|
||||
margin-top: 84px;
|
||||
}
|
||||
.feature-row:first-of-type { margin-top: 76px; }
|
||||
|
||||
.feature-copy { min-width: 0; }
|
||||
|
||||
.feature-copy h3 { font-size: 21px; letter-spacing: -0.015em; margin-bottom: 14px; }
|
||||
|
||||
.eyebrow {
|
||||
font-family: var(--mono);
|
||||
font-size: 12.5px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent-text);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.feature-copy h3 {
|
||||
font-size: 20px;
|
||||
font-weight: 650;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.feature-copy ul {
|
||||
list-style: none;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
.feature-copy ul { list-style: none; display: grid; gap: 10px; }
|
||||
|
||||
.feature-copy li {
|
||||
padding-left: 18px;
|
||||
position: relative;
|
||||
padding-left: 18px;
|
||||
color: var(--text-2);
|
||||
font-size: 15px;
|
||||
}
|
||||
@@ -229,24 +590,38 @@ body {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0.62em;
|
||||
width: 6px;
|
||||
height: 2px;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 1px;
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
.feature-shot { min-width: 0; }
|
||||
|
||||
.feature-shot img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: 0 6px 18px rgba(28, 33, 40, 0.08);
|
||||
border-radius: 12px;
|
||||
box-shadow: var(--shadow-shot);
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.feature-shot img + img { margin-top: 16px; }
|
||||
.feature-shot img.narrow { width: 78%; }
|
||||
|
||||
.feature-shot figcaption {
|
||||
margin-top: 12px;
|
||||
font-size: 13px;
|
||||
color: var(--text-2);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ---- download ---- */
|
||||
.download {
|
||||
padding: 72px 0;
|
||||
background: var(--bg-soft);
|
||||
padding: 92px 0 100px;
|
||||
background: var(--bg-alt);
|
||||
border-top: 1px solid var(--border);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
@@ -254,84 +629,94 @@ body {
|
||||
.dl-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 40px;
|
||||
background: var(--bg);
|
||||
margin-top: 44px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
font-size: 14.5px;
|
||||
}
|
||||
|
||||
.dl-table th, .dl-table td {
|
||||
text-align: left;
|
||||
padding: 14px 18px;
|
||||
padding: 15px 20px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.dl-table thead th {
|
||||
background: var(--bg-soft);
|
||||
background: var(--accent-soft);
|
||||
color: var(--text-2);
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
font-family: var(--mono);
|
||||
font-weight: 500;
|
||||
font-size: 12.5px;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.dl-table tbody td { color: var(--text-2); }
|
||||
.dl-table tbody tr + tr td { border-top: 1px solid var(--border); }
|
||||
.dl-table tbody tr:hover td { background: var(--overlay-row); }
|
||||
|
||||
.dl-table a { color: var(--accent); text-decoration: none; font-weight: 600; }
|
||||
.dl-table a:hover { text-decoration: underline; }
|
||||
.dl-table a {
|
||||
color: var(--accent-text);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
border-bottom: 1px solid transparent;
|
||||
}
|
||||
.dl-table a:hover { border-bottom-color: currentColor; }
|
||||
|
||||
.dl-table .sep { color: var(--border); margin: 0 8px; }
|
||||
.dl-table .sep { color: var(--border-strong); margin: 0 8px; }
|
||||
.dl-table .mono { color: var(--text); }
|
||||
|
||||
.update-note { margin-top: 40px; max-width: 72ch; }
|
||||
.update-note { margin-top: 44px; max-width: 74ch; }
|
||||
|
||||
.update-note h3 { font-size: 16px; margin-bottom: 8px; }
|
||||
|
||||
.update-note p { color: var(--text-2); font-size: 14.5px; }
|
||||
|
||||
/* ---- changelog ---- */
|
||||
.changelog { padding: 72px 0; }
|
||||
.changelog { padding: 92px 0 100px; background: var(--bg); }
|
||||
|
||||
.changelog-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 48px;
|
||||
margin-top: 40px;
|
||||
gap: 40px;
|
||||
margin-top: 44px;
|
||||
}
|
||||
|
||||
.changelog-list article {
|
||||
border-top: 2px solid var(--border);
|
||||
padding-top: 18px;
|
||||
transition: border-color 0.25s ease;
|
||||
}
|
||||
.changelog-list article:hover { border-top-color: var(--accent); }
|
||||
|
||||
.changelog-list article h3 {
|
||||
font-size: 15px;
|
||||
font-family: var(--mono);
|
||||
font-size: 14.5px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
margin-bottom: 14px;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.changelog-list article h3 .mono {
|
||||
color: var(--text);
|
||||
margin-right: 8px;
|
||||
}
|
||||
.changelog-list .rel-date { color: var(--text-3); font-weight: 400; font-size: 12.5px; }
|
||||
|
||||
.changelog-list ul {
|
||||
list-style: none;
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
.changelog-list ul { list-style: none; display: grid; gap: 8px; }
|
||||
|
||||
.changelog-list li {
|
||||
font-size: 14px;
|
||||
color: var(--text-2);
|
||||
}
|
||||
.changelog-list li { font-size: 14px; color: var(--text-2); }
|
||||
|
||||
.changelog-more {
|
||||
margin-top: 40px;
|
||||
font-size: 14px;
|
||||
color: var(--text-2);
|
||||
}
|
||||
.changelog-more { margin-top: 44px; font-size: 14px; color: var(--text-2); }
|
||||
|
||||
.changelog-more a { color: var(--accent); }
|
||||
.changelog-more a { color: var(--accent-text); font-weight: 500; text-decoration: none; }
|
||||
.changelog-more a:hover { border-bottom: 1px solid currentColor; }
|
||||
|
||||
/* ---- footer ---- */
|
||||
.site-footer {
|
||||
background: var(--bg-alt);
|
||||
border-top: 1px solid var(--border);
|
||||
padding: 28px 0;
|
||||
padding: 34px 0;
|
||||
font-size: 13.5px;
|
||||
color: var(--text-2);
|
||||
}
|
||||
@@ -341,24 +726,47 @@ body {
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.footer-inner .mono { font-size: 12.5px; color: var(--text-3); }
|
||||
|
||||
/* ---- scroll reveal ---- */
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.reveal {
|
||||
opacity: 0;
|
||||
transform: translateY(14px);
|
||||
transition: opacity 0.6s ease, transform 0.6s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
.reveal.is-in { opacity: 1; transform: none; }
|
||||
}
|
||||
|
||||
/* ---- responsive ---- */
|
||||
@media (max-width: 960px) {
|
||||
@media (max-width: 1024px) {
|
||||
.feature-row { grid-template-columns: 1fr; gap: 36px; }
|
||||
.feature-row .feature-shot { order: -1; }
|
||||
.feature-shot img.narrow { width: 100%; }
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.hero-grid { grid-template-columns: 1fr; gap: 40px; }
|
||||
.hero { padding-top: 48px; }
|
||||
.feature-row, .media-row { grid-template-columns: 1fr; gap: 32px; }
|
||||
.media-row .feature-shot { order: -1; }
|
||||
.changelog-list { grid-template-columns: 1fr; gap: 32px; }
|
||||
.hero { padding-top: 60px; }
|
||||
.section-head { flex-direction: column; align-items: flex-start; gap: 20px; }
|
||||
.changelog-list { grid-template-columns: 1fr; gap: 28px; }
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.nav-links { gap: 16px; }
|
||||
.nav-links a { font-size: 14px; }
|
||||
.container { padding: 0 20px; }
|
||||
.nav-inner { height: 56px; }
|
||||
.nav-links { gap: 0; }
|
||||
.nav-links a { font-size: 13.5px; padding: 6px 8px; }
|
||||
.hero h1 { font-size: 30px; }
|
||||
.spec-strip { gap: 16px 24px; }
|
||||
.cap-track { padding: 4px 20px 20px; margin-left: -20px; margin-right: -20px; }
|
||||
.dl-table { display: block; overflow-x: auto; white-space: nowrap; }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
html { scroll-behavior: auto; }
|
||||
*, *::before, *::after { transition: none !important; }
|
||||
}
|
||||
@@ -6,111 +6,277 @@
|
||||
<title>OpenTerminal - 本地终端 / SSH / SFTP 桌面终端,开源 Windows 终端工具</title>
|
||||
<meta name="description" content="OpenTerminal 是开源的 Windows 桌面终端:本地终端、SSH 远程会话、SFTP 文件管理、分屏与广播输入、主题定制。MIT 协议,支持自动更新。" />
|
||||
<link rel="icon" type="image/png" href="assets/icon.png" />
|
||||
<link rel="preload" href="assets/fonts/space-grotesk-600.woff2" as="font" type="font/woff2" crossorigin />
|
||||
<link rel="preload" href="assets/fonts/jetbrains-mono-500.woff2" as="font" type="font/woff2" crossorigin />
|
||||
<meta property="og:title" content="OpenTerminal - 本地终端 / SSH / SFTP 桌面终端" />
|
||||
<meta property="og:description" content="开源的 Windows 桌面终端:本地终端、SSH 远程会话、SFTP 文件管理、分屏与广播输入。MIT 协议,支持自动更新。" />
|
||||
<meta property="og:image" content="assets/shot-terminal.jpg" />
|
||||
<meta name="theme-color" content="#0b0e12" />
|
||||
<link rel="stylesheet" href="assets/style.css" />
|
||||
<script>
|
||||
// 主题必须在首帧前定好,避免闪白/闪黑;默认深色,只有用户显式切过才用保存值
|
||||
(function () {
|
||||
var theme = 'dark';
|
||||
try {
|
||||
if (localStorage.getItem('openterminal.theme') === 'light') theme = 'light';
|
||||
} catch (e) {}
|
||||
document.documentElement.dataset.theme = theme;
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<a class="skip-link" href="#main">跳到主要内容</a>
|
||||
|
||||
<header class="site-nav">
|
||||
<div class="container nav-inner">
|
||||
<a class="brand" href="#top">
|
||||
<img src="assets/icon.png" alt="OpenTerminal 图标" width="24" height="24" />
|
||||
<span>OpenTerminal</span>
|
||||
</a>
|
||||
<nav class="nav-links">
|
||||
<a href="#features">功能</a>
|
||||
<a href="#download">下载</a>
|
||||
<a href="#changelog">更新日志</a>
|
||||
<a href="https://github.com/billowliu2/OpenTerminal" target="_blank" rel="noopener">GitHub</a>
|
||||
</nav>
|
||||
<div class="nav-right">
|
||||
<nav class="nav-links" aria-label="站内导航">
|
||||
<a href="#features">功能</a>
|
||||
<a href="#download">下载</a>
|
||||
<a href="#changelog">更新日志</a>
|
||||
<a href="https://github.com/billowliu2/OpenTerminal" target="_blank" rel="noopener">GitHub</a>
|
||||
</nav>
|
||||
<button class="theme-toggle" id="theme-toggle" type="button" aria-label="切换明暗主题" title="切换明暗主题">
|
||||
<svg class="icon-moon" width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z" />
|
||||
</svg>
|
||||
<svg class="icon-sun" width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="4" />
|
||||
<path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main id="top">
|
||||
<main id="main">
|
||||
|
||||
<section class="hero">
|
||||
<section class="hero" id="top">
|
||||
<div class="container hero-grid">
|
||||
<div class="hero-copy">
|
||||
<p class="hero-kicker">开源桌面终端 · MIT 协议</p>
|
||||
<h1>本地终端、SSH 与 SFTP<br />装进同一个桌面应用</h1>
|
||||
<h1>本地终端、SSH 与 SFTP<br />装进<span class="accent">同一个</span>桌面应用</h1>
|
||||
<p class="hero-sub">
|
||||
OpenTerminal 面向开发和运维场景:WebGL 加速的终端渲染、可分屏的 SSH 会话、
|
||||
内置 SFTP 文件管理与广播输入。安装即用,支持自动更新。
|
||||
</p>
|
||||
<div class="hero-actions">
|
||||
<a class="btn btn-primary" id="dl-exe" href="https://github.com/billowliu2/OpenTerminal/releases/download/v1.0.15/OpenTerminal-1.0.15-setup.exe">
|
||||
<a class="btn btn-primary" id="dl-exe" href="https://github.com/billowliu2/OpenTerminal/releases/download/v1.0.22/OpenTerminal-1.0.22-setup.exe">
|
||||
下载 Windows 安装包
|
||||
</a>
|
||||
<a class="btn btn-ghost" href="https://github.com/billowliu2/OpenTerminal" target="_blank" rel="noopener">
|
||||
查看源码
|
||||
</a>
|
||||
</div>
|
||||
<p class="hero-note">当前版本 v1.0.15 · 提供 exe 与 msi 安装包。</p>
|
||||
<p class="hero-note">当前版本 <span class="mono">v1.0.22</span> · NSIS exe 安装包,支持应用内自动更新。</p>
|
||||
</div>
|
||||
<figure class="hero-shot">
|
||||
<img src="assets/shot-terminal.png" alt="OpenTerminal 主界面:四个分屏终端会话" width="1282" height="801" />
|
||||
<figcaption>四路分屏会话,每路独立连接本地或远程主机</figcaption>
|
||||
<img src="assets/shot-terminal.jpg" alt="OpenTerminal 主界面:三路分屏并行运行 Kimi Code、Claude Code 与 Codex" width="1739" height="870" />
|
||||
<figcaption>三路分屏并行运行 AI 编程 Agent,背景图与压暗层可自定义</figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
<div class="container spec-strip">
|
||||
<span>当前版本 <strong class="mono">1.0.15</strong></span>
|
||||
<span>当前版本 <strong class="mono">v1.0.22</strong></span>
|
||||
<span>Windows 10 / 11 · x64</span>
|
||||
<span>MIT 开源协议</span>
|
||||
<span>exe 支持应用内自动更新</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="features" id="features">
|
||||
<section class="features grid-bg" id="features">
|
||||
<div class="container">
|
||||
<h2>核心能力</h2>
|
||||
<p class="section-sub">按使用场景划分的三组功能,全部能力见仓库 README。</p>
|
||||
<div class="section-head">
|
||||
<div>
|
||||
<p class="section-eyebrow">Capabilities</p>
|
||||
<h2>核心能力</h2>
|
||||
<p class="section-sub">按使用场景划分的能力组,左右拖动或用箭头浏览;完整清单见仓库 README。</p>
|
||||
</div>
|
||||
<div class="cap-controls">
|
||||
<button class="track-btn" type="button" data-dir="-1" aria-label="向左浏览能力卡片">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 18l-6-6 6-6" /></svg>
|
||||
</button>
|
||||
<button class="track-btn" type="button" data-dir="1" aria-label="向右浏览能力卡片">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 6l6 6-6 6" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="feature-row">
|
||||
<div class="feature-copy">
|
||||
<div class="cap-track" id="cap-track" tabindex="0" aria-label="核心能力,可横向滚动">
|
||||
<article class="cap-card">
|
||||
<p class="cap-num">01</p>
|
||||
<h3>终端核心</h3>
|
||||
<ul>
|
||||
<li>node-pty 本地会话,自动探测 PowerShell / zsh / bash</li>
|
||||
<li>xterm.js WebGL 渲染,声明 256 色与真彩能力,异常时自动降级</li>
|
||||
<li>xterm.js WebGL 渲染,声明 256 色与真彩,异常自动降级</li>
|
||||
<li>内嵌搜索(Ctrl+F)、命令历史、粘贴风险确认</li>
|
||||
<li>关键词高亮:内置 11 条规则(错误状态、路径、IP、URL 等),支持自定义正则</li>
|
||||
<li>关键词高亮 22 条内置规则,支持自定义正则与三档强度</li>
|
||||
<li>字体、字重、字距、行高、光标样式实时生效</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="feature-copy">
|
||||
<h3>SSH 与 SFTP</h3>
|
||||
</article>
|
||||
|
||||
<article class="cap-card">
|
||||
<p class="cap-num">02</p>
|
||||
<h3>SSH 会话</h3>
|
||||
<ul>
|
||||
<li>ssh2 实现:密码、私钥(文件或粘贴内容)、SSH Agent 三种认证</li>
|
||||
<li>ssh2 实现:密码、私钥(文件或粘贴内容)、SSH Agent</li>
|
||||
<li>多路分屏与广播输入,一次操作下发到多台主机</li>
|
||||
<li>SFTP 文件面板:上传下载、同名覆盖确认、chmod / chown 权限编辑</li>
|
||||
<li>ZMODEM 传输支持;主机指纹校验与首次连接确认</li>
|
||||
<li>目录记忆:OSC 7 上报工作目录,新会话按上次位置恢复</li>
|
||||
<li>主机指纹校验、首次连接确认、变更告警</li>
|
||||
<li>ZMODEM 传输支持(sz / rz)</li>
|
||||
<li>OSC 7 上报工作目录,新会话按上次位置恢复</li>
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
<article class="cap-card">
|
||||
<p class="cap-num">03</p>
|
||||
<h3>SFTP 文件面板</h3>
|
||||
<ul>
|
||||
<li>列表头列分明:名称、大小、修改时间、权限、用户/组</li>
|
||||
<li>点击列头排序;文件名解析为真实用户与组名</li>
|
||||
<li>字体缩放 80%–180%,Ctrl+滚轮或工具栏按钮</li>
|
||||
<li>上传下载、同名覆盖确认、chmod / chown 编辑</li>
|
||||
<li>大目录虚拟滚动,上万条目不卡窗口</li>
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
<article class="cap-card">
|
||||
<p class="cap-num">04</p>
|
||||
<h3>工作区与布局</h3>
|
||||
<ul>
|
||||
<li>dockview 分屏:任意拆分、拖拽、合并与浮动</li>
|
||||
<li>布局模板保存与一键套用</li>
|
||||
<li>启动恢复上次会话布局</li>
|
||||
<li>界面缩放 50% 至 150%</li>
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
<article class="cap-card">
|
||||
<p class="cap-num">05</p>
|
||||
<h3>主题与外观</h3>
|
||||
<ul>
|
||||
<li>主题编辑器:22 个颜色位逐项调整,实时预览</li>
|
||||
<li>内置 11 套主题,跟随系统明暗自动切换</li>
|
||||
<li>自定义背景图片、不透明度与压暗层</li>
|
||||
<li>标签强调色、高亮配色跟随主题</li>
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
<article class="cap-card">
|
||||
<p class="cap-num">06</p>
|
||||
<h3>效率工具</h3>
|
||||
<ul>
|
||||
<li>命令库:保存常用命令片段,跨会话发送到指定终端</li>
|
||||
<li>高亮规则集按主机绑定,不同服务器不同配色</li>
|
||||
<li>规则导入导出、实时预览与命中统计</li>
|
||||
<li>会话快照与恢复、终端标签自动命名</li>
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
<article class="cap-card">
|
||||
<p class="cap-num">07</p>
|
||||
<h3>系统监控</h3>
|
||||
<ul>
|
||||
<li>CPU、负载、内存、磁盘、网络实时面板</li>
|
||||
<li>每核占用与磁盘分区明细</li>
|
||||
<li>运行时长与网络速率曲线</li>
|
||||
<li>随会话打开,不额外占用通道</li>
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
<article class="cap-card">
|
||||
<p class="cap-num">08</p>
|
||||
<h3>锁屏与安全</h3>
|
||||
<ul>
|
||||
<li>Ctrl+L 一键锁屏,支持闲置自动锁定与启动即锁</li>
|
||||
<li>密码经 scrypt 加盐哈希存储,不落盘明文</li>
|
||||
<li>输错触发递增冷却;锁定期间禁用菜单与刷新快捷键</li>
|
||||
<li>锁屏只是遮罩:背后会话与传输不中断</li>
|
||||
</ul>
|
||||
</article>
|
||||
</div>
|
||||
<p class="cap-hint">← 拖动浏览 · 共 8 组能力</p>
|
||||
|
||||
<div class="feature-row">
|
||||
<figure class="feature-shot reveal">
|
||||
<img src="assets/shot-agents.png" alt="OpenTerminal 多路分屏运行 Kimi Code、Claude Code、Codex 等 AI 编程 Agent" width="1296" height="870" />
|
||||
</figure>
|
||||
<div class="feature-copy reveal">
|
||||
<p class="eyebrow">Workbench</p>
|
||||
<h3>AI Agent 工作台</h3>
|
||||
<ul>
|
||||
<li>多路分屏并行运行 Kimi Code、Claude Code、Codex 等 AI 编程工具</li>
|
||||
<li>为高频刷新的 TUI 界面优化:终端尺寸去抖同步,避免反复重绘</li>
|
||||
<li>中文输入法候选窗紧贴光标,TUI 内输入中文不再漂移</li>
|
||||
<li>关键词高亮自动标出错误、警告、HTTP 状态码与百分比</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="feature-row media-row">
|
||||
<figure class="feature-shot">
|
||||
<img src="assets/shot-themes.png" alt="OpenTerminal 主题设置:主题画廊与背景图片配置" width="1282" height="801" />
|
||||
<div class="feature-row">
|
||||
<figure class="feature-shot reveal">
|
||||
<img src="assets/shot-sftp.png" alt="OpenTerminal SFTP 文件面板:列表头、可排序列、完整修改时间与用户/组" width="1282" height="801" />
|
||||
<img class="narrow" src="assets/shot-sftp-menu.png" alt="OpenTerminal 在文件列表空白处右键弹出同款文件菜单" width="1282" height="801" />
|
||||
<figcaption>列表空白处右键同样是完整文件菜单,行内右键则作用于所选条目</figcaption>
|
||||
</figure>
|
||||
<div class="feature-copy">
|
||||
<div class="feature-copy reveal">
|
||||
<p class="eyebrow">SFTP</p>
|
||||
<h3>远程文件面板</h3>
|
||||
<ul>
|
||||
<li>列表头列分明:名称、大小、修改时间、权限、用户/组,点击列头排序</li>
|
||||
<li>文件名解析为真实用户与组名(root/root、eveuser/eveuser),不再只有数字</li>
|
||||
<li>字体缩放 80%–180%:Ctrl+滚轮或工具栏按钮,行高与字号联动并记忆</li>
|
||||
<li>上传 / 下载、同名覆盖确认、chmod 权限与 chown 属主编辑</li>
|
||||
<li>大目录虚拟滚动,上万条目不卡窗口</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="feature-row">
|
||||
<figure class="feature-shot reveal">
|
||||
<img src="assets/shot-themes.jpg" alt="OpenTerminal 主题设置:背景图片、不透明度、压暗层与主题画廊" width="1739" height="870" />
|
||||
</figure>
|
||||
<div class="feature-copy reveal">
|
||||
<p class="eyebrow">Customize</p>
|
||||
<h3>定制与效率</h3>
|
||||
<ul>
|
||||
<li>主题编辑器:22 个颜色位逐项调整,实时预览;支持自定义背景图片与不透明度</li>
|
||||
<li>内置 13 套主题,跟随系统明暗自动切换</li>
|
||||
<li>主题编辑器:22 个颜色位逐项调整,实时预览;支持自定义背景图片、不透明度与压暗层</li>
|
||||
<li>内置 11 套主题,跟随系统明暗自动切换</li>
|
||||
<li>命令库:保存常用命令片段,跨会话发送到指定终端</li>
|
||||
<li>启动恢复上次会话布局;界面缩放 50% 至 150%</li>
|
||||
<li>四语言界面:简体中文、繁体中文、English、日本語</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="feature-row">
|
||||
<figure class="feature-shot reveal">
|
||||
<img src="assets/shot-lock.png" alt="OpenTerminal 锁屏界面:不透明遮罩与密码解锁" width="1739" height="870" />
|
||||
</figure>
|
||||
<div class="feature-copy reveal">
|
||||
<p class="eyebrow">Security</p>
|
||||
<h3>锁屏与安全</h3>
|
||||
<ul>
|
||||
<li>Ctrl+L 一键锁屏,支持闲置自动锁定与启动即锁</li>
|
||||
<li>锁定期间禁用应用菜单、页面刷新与开发者工具快捷键</li>
|
||||
<li>密码经 scrypt 加盐哈希存储,不落盘明文;输错触发递增冷却</li>
|
||||
<li>锁屏只是遮罩:背后的终端会话与文件传输保持运行不中断</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="download" id="download">
|
||||
<div class="container">
|
||||
<p class="section-eyebrow">Download</p>
|
||||
<h2>下载</h2>
|
||||
<p class="section-sub">
|
||||
从 GitHub Releases 下载;全部历史版本在同一页面。
|
||||
<!-- 发新版本时更新下方两条直链与 hero 的 id=dl-exe 链接 -->
|
||||
<!-- 发新版本时更新下方 exe 直链与 hero 的 id=dl-exe 链接 -->
|
||||
</p>
|
||||
<table class="dl-table">
|
||||
<thead>
|
||||
@@ -118,19 +284,11 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="mono">OpenTerminal-1.0.15-setup.exe</td>
|
||||
<td>常规安装,支持应用内自动更新(推荐)</td>
|
||||
<td class="mono">122 MB</td>
|
||||
<td class="mono">OpenTerminal-1.0.22-setup.exe</td>
|
||||
<td>NSIS 安装包,支持应用内自动更新(推荐)</td>
|
||||
<td class="mono">109 MB</td>
|
||||
<td>
|
||||
<a href="https://github.com/billowliu2/OpenTerminal/releases/download/v1.0.15/OpenTerminal-1.0.15-setup.exe">下载</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="mono">OpenTerminal-1.0.15-setup.msi</td>
|
||||
<td>企业批量部署(MSI 无自动更新,需手动升级)</td>
|
||||
<td class="mono">135 MB</td>
|
||||
<td>
|
||||
<a href="https://github.com/billowliu2/OpenTerminal/releases/download/v1.0.15/OpenTerminal-1.0.15-setup.msi">下载</a>
|
||||
<a href="https://github.com/billowliu2/OpenTerminal/releases/download/v1.0.22/OpenTerminal-1.0.22-setup.exe">下载</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -147,8 +305,9 @@
|
||||
<div class="update-note">
|
||||
<h3>自动更新说明</h3>
|
||||
<p>
|
||||
应用内检查更新优先访问 git.codingplan.site 更新通道,失败时回退 GitHub Releases。
|
||||
应用内检查更新优先访问 GitHub Releases(跟随系统代理),失败时自动回退国内 git.codingplan.site 更新通道(直连)。
|
||||
更新包经 SHA-512 校验后安装。更新说明文字从发布通道的 release-notes.md 读取,与每次发布同步更新。
|
||||
自 v1.0.22 起不再提供 MSI 安装包,统一使用 NSIS exe。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -156,29 +315,30 @@
|
||||
|
||||
<section class="changelog" id="changelog">
|
||||
<div class="container">
|
||||
<p class="section-eyebrow">Changelog</p>
|
||||
<h2>近期更新</h2>
|
||||
<div class="changelog-list">
|
||||
<article>
|
||||
<h3><span class="mono">1.0.15</span> 2026-09-20</h3>
|
||||
<h3><span class="mono">1.0.22</span><span class="rel-date">2026-10-08</span></h3>
|
||||
<ul>
|
||||
<li>新增自定义终端背景图片与图片不透明度设置</li>
|
||||
<li>主题编辑器重名提醒与颜色值校验</li>
|
||||
<li>设置导航选中项文字对比度修复</li>
|
||||
<li>修复中文输入法候选窗不跟随光标(v1.0.21 回归)</li>
|
||||
<li>不再提供 MSI 安装包,统一使用 NSIS exe</li>
|
||||
<li>打包流程新增依赖一致性校验,防止旧依赖混入安装包</li>
|
||||
</ul>
|
||||
</article>
|
||||
<article>
|
||||
<h3><span class="mono">1.0.14</span> 2026-09-20</h3>
|
||||
<h3><span class="mono">1.0.21</span><span class="rel-date">2026-10-07</span></h3>
|
||||
<ul>
|
||||
<li>修复大文件(约 250KB 以上)上传损坏与 SFTP 句柄泄漏</li>
|
||||
<li>SSH 中文乱码、命令历史错乱等终端修复</li>
|
||||
<li>权限编辑、连接错误提示等 20 余项修复</li>
|
||||
<li>背景图体验:任意主题下调透明度均为压暗、文字自动提亮到 WCAG AA、新增背景压暗滑杆</li>
|
||||
<li>性能:设置热更不再触发全窗格重排,SFTP 大目录虚拟滚动,面板独立错误边界</li>
|
||||
<li>安装包精简约 90MB;SFTP/ZMODEM 本地路径准入与多处安全加固</li>
|
||||
</ul>
|
||||
</article>
|
||||
<article>
|
||||
<h3><span class="mono">1.0.13</span> 2026-09-20</h3>
|
||||
<h3><span class="mono">1.0.20</span><span class="rel-date">2026-10-01</span></h3>
|
||||
<ul>
|
||||
<li>命令片段库与广播输入改进</li>
|
||||
<li>监控面板历史曲线实时重绘</li>
|
||||
<li>修复中文输入法候选窗不跟随光标</li>
|
||||
<li>锁屏加固:锁定期间移除应用菜单,Ctrl+L 按键判定重构并补测试</li>
|
||||
</ul>
|
||||
</article>
|
||||
</div>
|
||||
@@ -197,5 +357,116 @@
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
|
||||
// 滚动进场
|
||||
var revealables = document.querySelectorAll('.reveal');
|
||||
if (reduce || !('IntersectionObserver' in window)) {
|
||||
revealables.forEach(function (el) { el.classList.add('is-in'); });
|
||||
} else {
|
||||
var io = new IntersectionObserver(function (entries) {
|
||||
entries.forEach(function (e) {
|
||||
if (e.isIntersecting) { e.target.classList.add('is-in'); io.unobserve(e.target); }
|
||||
});
|
||||
}, { rootMargin: '0px 0px -8% 0px', threshold: 0.05 });
|
||||
revealables.forEach(function (el) { io.observe(el); });
|
||||
}
|
||||
|
||||
// 导航当前位置
|
||||
var links = Array.prototype.slice.call(document.querySelectorAll('.nav-links a[href^="#"]'));
|
||||
var sections = links
|
||||
.map(function (a) { return document.querySelector(a.getAttribute('href')); })
|
||||
.filter(Boolean);
|
||||
function syncNav() {
|
||||
var pos = scrollY + 130;
|
||||
var current = null;
|
||||
sections.forEach(function (s) { if (s.offsetTop <= pos) current = s; });
|
||||
links.forEach(function (a) {
|
||||
a.classList.toggle('is-current', !!current && a.getAttribute('href') === '#' + current.id);
|
||||
});
|
||||
}
|
||||
addEventListener('scroll', syncNav, { passive: true });
|
||||
syncNav();
|
||||
|
||||
// 能力卡片横向轨道:箭头翻页 + 按住拖动
|
||||
var track = document.getElementById('cap-track');
|
||||
if (!track) return;
|
||||
var buttons = document.querySelectorAll('.track-btn');
|
||||
|
||||
function step() {
|
||||
var card = track.querySelector('.cap-card');
|
||||
return card ? card.getBoundingClientRect().width + 20 : track.clientWidth * 0.8;
|
||||
}
|
||||
function syncButtons() {
|
||||
var max = track.scrollWidth - track.clientWidth - 1;
|
||||
buttons.forEach(function (b) {
|
||||
var dir = Number(b.getAttribute('data-dir'));
|
||||
b.disabled = dir < 0 ? track.scrollLeft <= 0 : track.scrollLeft >= max;
|
||||
});
|
||||
}
|
||||
buttons.forEach(function (b) {
|
||||
b.addEventListener('click', function () {
|
||||
track.scrollBy({ left: Number(b.getAttribute('data-dir')) * step(), behavior: reduce ? 'auto' : 'smooth' });
|
||||
});
|
||||
});
|
||||
track.addEventListener('scroll', syncButtons, { passive: true });
|
||||
addEventListener('resize', syncButtons);
|
||||
syncButtons();
|
||||
|
||||
var dragging = false;
|
||||
var startX = 0;
|
||||
var startScroll = 0;
|
||||
track.addEventListener('pointerdown', function (e) {
|
||||
if (e.pointerType === 'mouse' && e.button !== 0) return;
|
||||
dragging = true;
|
||||
startX = e.clientX;
|
||||
startScroll = track.scrollLeft;
|
||||
track.classList.add('is-dragging');
|
||||
track.setPointerCapture(e.pointerId);
|
||||
});
|
||||
track.addEventListener('pointermove', function (e) {
|
||||
if (!dragging) return;
|
||||
var delta = e.clientX - startX;
|
||||
if (Math.abs(delta) > 3) e.preventDefault();
|
||||
track.scrollLeft = startScroll - delta;
|
||||
});
|
||||
function endDrag(e) {
|
||||
if (!dragging) return;
|
||||
dragging = false;
|
||||
track.classList.remove('is-dragging');
|
||||
if (e && e.pointerId !== undefined && track.hasPointerCapture(e.pointerId)) {
|
||||
track.releasePointerCapture(e.pointerId);
|
||||
}
|
||||
syncButtons();
|
||||
}
|
||||
track.addEventListener('pointerup', endDrag);
|
||||
track.addEventListener('pointercancel', endDrag);
|
||||
track.addEventListener('dragstart', function (e) { e.preventDefault(); });
|
||||
track.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'ArrowRight') { track.scrollBy({ left: step(), behavior: 'smooth' }); e.preventDefault(); }
|
||||
if (e.key === 'ArrowLeft') { track.scrollBy({ left: -step(), behavior: 'smooth' }); e.preventDefault(); }
|
||||
});
|
||||
|
||||
// 明暗主题切换
|
||||
var toggle = document.getElementById('theme-toggle');
|
||||
var meta = document.querySelector('meta[name="theme-color"]');
|
||||
function applyTheme(theme) {
|
||||
document.documentElement.dataset.theme = theme;
|
||||
if (meta) meta.setAttribute('content', theme === 'light' ? '#fafbf9' : '#0b0e12');
|
||||
if (toggle) toggle.setAttribute('aria-label', theme === 'light' ? '切换到深色主题' : '切换到浅色主题');
|
||||
}
|
||||
if (toggle) {
|
||||
toggle.addEventListener('click', function () {
|
||||
var next = document.documentElement.dataset.theme === 'light' ? 'dark' : 'light';
|
||||
applyTheme(next);
|
||||
try { localStorage.setItem('openterminal.theme', next); } catch (e) {}
|
||||
});
|
||||
}
|
||||
applyTheme(document.documentElement.dataset.theme === 'light' ? 'light' : 'dark');
|
||||
})();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||