diff --git a/docs-site/docs/reference/api.md b/docs-site/docs/reference/api.md index de3aa8a..e694bc1 100644 --- a/docs-site/docs/reference/api.md +++ b/docs-site/docs/reference/api.md @@ -10,74 +10,126 @@ All endpoints are prefixed with `/api` and authenticated with a JWT Bearer token ## Authentication -| Endpoint | Description | -|----------|-------------| -| `POST /api/auth/setup` | Initialize the first admin (only when no user exists) | -| `POST /api/auth/login` | Log in and receive a JWT | -| `PUT /api/auth/password` | Change password | +| Method | Endpoint | Description | +|--------|----------|-------------| +| `GET` | `/api/auth/setup/status` | Check whether admin initialization is needed | +| `POST` | `/api/auth/setup` | Initialize the first admin (only when no user exists) | +| `POST` | `/api/auth/login` | Log in and receive a JWT | +| `POST` | `/api/auth/logout` | Log out (invalidate current token) | +| `GET` | `/api/auth/profile` | Current user profile | +| `PUT` | `/api/auth/password` | Change password | -## Backup tasks +## Backup Tasks -| Endpoint | Description | -|----------|-------------| -| `GET /api/backup/tasks` | List tasks | -| `POST /api/backup/tasks` | Create | -| `GET /api/backup/tasks/:id` | Detail | -| `PUT /api/backup/tasks/:id` | Update | -| `DELETE /api/backup/tasks/:id` | Delete | -| `PUT /api/backup/tasks/:id/toggle` | Enable / disable | -| `POST /api/backup/tasks/:id/run` | Manual run | +| Method | Endpoint | Description | +|--------|----------|-------------| +| `GET` | `/api/backup/tasks` | List tasks | +| `POST` | `/api/backup/tasks` | Create | +| `GET` | `/api/backup/tasks/:id` | Detail | +| `PUT` | `/api/backup/tasks/:id` | Update | +| `DELETE` | `/api/backup/tasks/:id` | Delete | +| `PUT` | `/api/backup/tasks/:id/toggle` | Enable / disable | +| `POST` | `/api/backup/tasks/:id/run` | Trigger a manual run | -## Backup records +## Backup Records -| Endpoint | Description | -|----------|-------------| -| `GET /api/backup/records` | List records with filters | -| `GET /api/backup/records/:id/logs/stream` | Live logs (SSE) | -| `GET /api/backup/records/:id/download` | Download artifact | -| `POST /api/backup/records/:id/restore` | Restore into the original source | +| Method | Endpoint | Description | +|--------|----------|-------------| +| `GET` | `/api/backup/records` | List records with filters | +| `GET` | `/api/backup/records/:id` | Record detail | +| `GET` | `/api/backup/records/:id/logs/stream` | Live logs (SSE) | +| `GET` | `/api/backup/records/:id/download` | Download the artifact | +| `POST` | `/api/backup/records/:id/restore` | Restore to the original source | +| `DELETE` | `/api/backup/records/:id` | Delete a record | +| `POST` | `/api/backup/records/batch-delete` | Bulk delete | -## Storage targets +## Storage Targets -| Endpoint | Description | -|----------|-------------| -| `GET /api/storage-targets` | List | -| `POST /api/storage-targets` | Create | -| `POST /api/storage-targets/test` | Test connection with pending config | -| `GET /api/storage-targets/rclone/backends` | List all available rclone backends | +| Method | Endpoint | Description | +|--------|----------|-------------| +| `GET` | `/api/storage-targets` | List | +| `POST` | `/api/storage-targets` | Create | +| `GET` | `/api/storage-targets/:id` | Detail | +| `PUT` | `/api/storage-targets/:id` | Update | +| `DELETE` | `/api/storage-targets/:id` | Delete | +| `POST` | `/api/storage-targets/test` | Test connection with pending config | +| `POST` | `/api/storage-targets/:id/test` | Re-test a saved target | +| `PUT` | `/api/storage-targets/:id/star` | Toggle favourite | +| `GET` | `/api/storage-targets/:id/usage` | Query remote usage (where supported) | +| `GET` | `/api/storage-targets/rclone/backends` | List all available rclone backends | +| `POST` | `/api/storage-targets/google-drive/auth-url` | Start Google Drive OAuth | +| `POST` | `/api/storage-targets/google-drive/complete` | Complete OAuth flow | -## Nodes (cluster) +## Nodes (Cluster) -| Endpoint | Description | -|----------|-------------| -| `GET /api/nodes` | List nodes | -| `POST /api/nodes` | Create a node and return token | -| `PUT /api/nodes/:id` | Rename | -| `DELETE /api/nodes/:id` | Delete (rejected if tasks are attached) | -| `GET /api/nodes/:id/fs/list` | Browse directory (remote node = async RPC) | +| Method | Endpoint | Description | +|--------|----------|-------------| +| `GET` | `/api/nodes` | List nodes | +| `POST` | `/api/nodes` | Create a node and return its token | +| `GET` | `/api/nodes/:id` | Node detail | +| `PUT` | `/api/nodes/:id` | Rename | +| `DELETE` | `/api/nodes/:id` | Delete (rejected if tasks are still attached) | +| `GET` | `/api/nodes/:id/fs/list` | Browse a directory (remote nodes use an async RPC via Agent) | -## Agent protocol (X-Agent-Token) +## Agent Protocol (X-Agent-Token) -| Endpoint | Description | -|----------|-------------| -| `POST /api/agent/heartbeat` | Report liveness | -| `POST /api/agent/commands/poll` | Claim one pending command | -| `POST /api/agent/commands/:id/result` | Report command result | -| `GET /api/agent/tasks/:id` | Fetch task spec with decrypted storage configs | -| `POST /api/agent/records/:id` | Append logs / update record status | +Dedicated endpoints for the Agent CLI. Authenticated via the `X-Agent-Token` header instead of JWT. + +| Method | Endpoint | Description | +|--------|----------|-------------| +| `POST` | `/api/agent/heartbeat` | Report liveness; returns the node ID | +| `POST` | `/api/agent/commands/poll` | Claim one pending command | +| `POST` | `/api/agent/commands/:id/result` | Report command result | +| `GET` | `/api/agent/tasks/:id` | Fetch task spec with decrypted storage configs | +| `POST` | `/api/agent/records/:id` | Append logs / update record status | ## Notifications -| Endpoint | Description | -|----------|-------------| -| `GET /api/notifications` | List | -| `POST /api/notifications` | Create | +| Method | Endpoint | Description | +|--------|----------|-------------| +| `GET` | `/api/notifications` | List | +| `POST` | `/api/notifications` | Create | +| `GET` | `/api/notifications/:id` | Detail | +| `PUT` | `/api/notifications/:id` | Update | +| `DELETE` | `/api/notifications/:id` | Delete | +| `POST` | `/api/notifications/test` | Test with pending config | +| `POST` | `/api/notifications/:id/test` | Re-test a saved notifier | -## Dashboard / audit / system +## Dashboard -| Endpoint | Description | -|----------|-------------| -| `GET /api/dashboard/stats` | Overview statistics | -| `GET /api/audit-logs` | Audit log list | -| `GET /api/system/info` | System information | -| `GET /api/system/update-check` | Check for a newer release | +| Method | Endpoint | Description | +|--------|----------|-------------| +| `GET` | `/api/dashboard/stats` | Overview statistics | +| `GET` | `/api/dashboard/timeline` | Recent activity timeline | + +## Audit / System / Settings + +| Method | Endpoint | Description | +|--------|----------|-------------| +| `GET` | `/api/audit-logs` | Audit log list | +| `GET` | `/api/system/info` | System information | +| `GET` | `/api/system/update-check` | Check for a newer release | +| `GET` | `/api/settings` | System-level settings | +| `PUT` | `/api/settings` | Update system settings | + +## Response Envelope + +All successful responses follow the shape: + +```json +{ + "code": "OK", + "message": "", + "data": { /* actual payload */ } +} +``` + +Errors return an HTTP 4xx/5xx plus: + +```json +{ + "code": "BACKUP_TASK_NOT_FOUND", + "message": "备份任务不存在", + "data": null +} +``` diff --git a/docs-site/i18n/zh-Hans/code.json b/docs-site/i18n/zh-Hans/code.json index b6b3e41..57998b0 100644 --- a/docs-site/i18n/zh-Hans/code.json +++ b/docs-site/i18n/zh-Hans/code.json @@ -1,26 +1,82 @@ { + "home.badge": { + "message": "开源 · v1.6.0", + "description": "Version badge on the hero" + }, + "home.title.part1": { + "message": "为每一台服务器提供", + "description": "Hero title, first line" + }, + "home.title.part2": { + "message": "自托管备份管理。", + "description": "Hero title accent second line" + }, "home.tagline": { - "message": "自托管服务器备份管理 — 一个二进制,一条命令,管好所有备份", + "message": "一个二进制,一条命令。文件 / 数据库 / SAP HANA 备份直送 70+ 存储后端。", "description": "Tagline on the home page" }, + "home.pageTitle": { + "message": "自托管备份管理", + "description": "Page element on the home page" + }, "home.getStarted": { "message": "快速开始", "description": "Primary CTA on the home page" }, - "home.title": { - "message": "自托管备份管理", - "description": "Title on the home page" + "home.metric.backends": { + "message": "存储后端", + "description": "Hero metric label: storage backends" }, + "home.metric.backupTypes": { + "message": "备份类型", + "description": "Hero metric label: backup types" + }, + "home.metric.license": { + "message": "开源协议", + "description": "Hero metric label: license" + }, + + "section.features.tag": { + "message": "核心能力", + "description": "FEATURES section tag" + }, + "section.features.title": { + "message": "该有的都有,多余的没有", + "description": "Features section title" + }, + "section.features.subtitle": { + "message": "备份 Runner、存储 Provider、调度、集群 — 每一块都经过打磨。", + "description": "Features section subtitle" + }, + "feat.types.title": {"message": "多种备份类型"}, "feat.types.desc": {"message": "文件与目录(支持多源路径),以及 MySQL、PostgreSQL、SQLite、SAP HANA 统一管理。"}, "feat.storage.title": {"message": "70+ 存储后端"}, - "feat.storage.desc": {"message": "内置阿里云 OSS、腾讯云 COS、七牛、S3、Google Drive、WebDAV、FTP,以及通过 rclone 接入的 SFTP、Azure Blob、Dropbox、OneDrive 等数十种。"}, + "feat.storage.desc": {"message": "内置阿里云 OSS、腾讯云 COS、七牛、S3、Google Drive、WebDAV、FTP,以及 SFTP、Azure Blob、Dropbox 等 rclone 后端。"}, "feat.scheduling.title": {"message": "调度与保留策略"}, "feat.scheduling.desc": {"message": "基于 Cron 的可视化调度编辑器,支持按天数/份数自动保留和空目录清理。"}, "feat.cluster.title": {"message": "多节点集群"}, - "feat.cluster.desc": {"message": "Master-Agent 模式跨多台服务器管理备份,Agent 在本地执行任务并直接上传到存储,无需反向连通性。"}, + "feat.cluster.desc": {"message": "Master-Agent 基于 HTTP 长轮询。Agent 在本地执行任务并直接上传到存储 — 无需反向连通性。"}, "feat.security.title": {"message": "默认安全"}, "feat.security.desc": {"message": "JWT 认证、bcrypt、AES-256-GCM 加密配置、可选备份加密、完整审计日志。"}, "feat.deploy.title": {"message": "部署轻量"}, - "feat.deploy.desc": {"message": "单个静态二进制 + 内嵌 SQLite。Docker 一键启动或通过 install.sh 裸机部署 — 零外部依赖。"} + "feat.deploy.desc": {"message": "单个静态二进制 + 内嵌 SQLite。Docker 一键启动或裸机 — 零外部依赖。"}, + "feat.learnMore": {"message": "了解更多"}, + + "showcase.tag": {"message": "产品界面"}, + "showcase.title": {"message": "精心打磨的控制台,而非 DIY 脚本"}, + "showcase.subtitle": {"message": "每个页面都为运维而生 — 可观测优先,可配置次之。"}, + "showcase.tab.dashboard": {"message": "仪表盘"}, + "showcase.tab.tasks": {"message": "备份任务"}, + "showcase.tab.storage": {"message": "存储目标"}, + "showcase.tab.nodes": {"message": "多节点"}, + "showcase.dashboard.title": {"message": "一眼掌握全局"}, + "showcase.dashboard.desc": {"message": "备份成功率、存储使用量、最近执行记录、即将触发的计划 — 一页实时数据。"}, + "showcase.tasks.title": {"message": "可视化任务编辑器"}, + "showcase.tasks.desc": {"message": "文件、MySQL、PostgreSQL、SQLite、SAP HANA — 三步完成。Cron 编辑器、多目标分发、保留策略、压缩、加密 — 点击即用。"}, + "showcase.storage.title": {"message": "70+ 后端,统一体验"}, + "showcase.storage.desc": {"message": "阿里云 OSS、腾讯云 COS、S3、Google Drive、WebDAV — 加上每一种 rclone 后端。测试连接、收藏、查看实时容量。"}, + "showcase.nodes.title": {"message": "几分钟搭起 Master-Agent"}, + "showcase.nodes.desc": {"message": "创建节点、复制令牌、在任意远程主机启动 Agent。路由到节点的任务在本地执行并直接上传到存储 — 无需反向连通性。"}, + "showcase.cta": {"message": "开始阅读文档"} } diff --git a/docs-site/i18n/zh-Hans/docusaurus-plugin-content-docs/current/reference/api.md b/docs-site/i18n/zh-Hans/docusaurus-plugin-content-docs/current/reference/api.md index dbf240f..24bdce7 100644 --- a/docs-site/i18n/zh-Hans/docusaurus-plugin-content-docs/current/reference/api.md +++ b/docs-site/i18n/zh-Hans/docusaurus-plugin-content-docs/current/reference/api.md @@ -10,74 +10,126 @@ description: REST API 端点 — 统一以 /api 为前缀,使用 JWT Bearer ## 认证 -| 端点 | 说明 | -|------|------| -| `POST /api/auth/setup` | 初始化首个管理员(仅当系统无任何用户时) | -| `POST /api/auth/login` | 登录,返回 JWT | -| `PUT /api/auth/password` | 修改密码 | +| 方法 | 端点 | 说明 | +|------|------|------| +| `GET` | `/api/auth/setup/status` | 查询是否需要初始化管理员 | +| `POST` | `/api/auth/setup` | 初始化首个管理员(仅当系统无任何用户时) | +| `POST` | `/api/auth/login` | 登录,返回 JWT | +| `POST` | `/api/auth/logout` | 登出(使当前 Token 失效) | +| `GET` | `/api/auth/profile` | 当前用户信息 | +| `PUT` | `/api/auth/password` | 修改密码 | ## 备份任务 -| 端点 | 说明 | -|------|------| -| `GET /api/backup/tasks` | 列表 | -| `POST /api/backup/tasks` | 创建 | -| `GET /api/backup/tasks/:id` | 详情 | -| `PUT /api/backup/tasks/:id` | 更新 | -| `DELETE /api/backup/tasks/:id` | 删除 | -| `PUT /api/backup/tasks/:id/toggle` | 启用 / 禁用 | -| `POST /api/backup/tasks/:id/run` | 手动执行 | +| 方法 | 端点 | 说明 | +|------|------|------| +| `GET` | `/api/backup/tasks` | 列表 | +| `POST` | `/api/backup/tasks` | 创建 | +| `GET` | `/api/backup/tasks/:id` | 详情 | +| `PUT` | `/api/backup/tasks/:id` | 更新 | +| `DELETE` | `/api/backup/tasks/:id` | 删除 | +| `PUT` | `/api/backup/tasks/:id/toggle` | 启用 / 禁用 | +| `POST` | `/api/backup/tasks/:id/run` | 手动触发一次执行 | ## 备份记录 -| 端点 | 说明 | -|------|------| -| `GET /api/backup/records` | 列表(支持筛选) | -| `GET /api/backup/records/:id/logs/stream` | 实时日志(SSE) | -| `GET /api/backup/records/:id/download` | 下载备份 | -| `POST /api/backup/records/:id/restore` | 恢复到原始源 | +| 方法 | 端点 | 说明 | +|------|------|------| +| `GET` | `/api/backup/records` | 列表(支持筛选) | +| `GET` | `/api/backup/records/:id` | 记录详情 | +| `GET` | `/api/backup/records/:id/logs/stream` | 实时日志(SSE) | +| `GET` | `/api/backup/records/:id/download` | 下载备份产物 | +| `POST` | `/api/backup/records/:id/restore` | 恢复到原始源 | +| `DELETE` | `/api/backup/records/:id` | 删除记录 | +| `POST` | `/api/backup/records/batch-delete` | 批量删除 | ## 存储目标 -| 端点 | 说明 | -|------|------| -| `GET /api/storage-targets` | 列表 | -| `POST /api/storage-targets` | 添加 | -| `POST /api/storage-targets/test` | 用待审核配置测试连接 | -| `GET /api/storage-targets/rclone/backends` | 列出可用 rclone 后端 | +| 方法 | 端点 | 说明 | +|------|------|------| +| `GET` | `/api/storage-targets` | 列表 | +| `POST` | `/api/storage-targets` | 创建 | +| `GET` | `/api/storage-targets/:id` | 详情 | +| `PUT` | `/api/storage-targets/:id` | 更新 | +| `DELETE` | `/api/storage-targets/:id` | 删除 | +| `POST` | `/api/storage-targets/test` | 用待审核配置测试连接 | +| `POST` | `/api/storage-targets/:id/test` | 重测已保存的目标 | +| `PUT` | `/api/storage-targets/:id/star` | 切换收藏状态 | +| `GET` | `/api/storage-targets/:id/usage` | 查询远端存储用量(支持此能力的后端) | +| `GET` | `/api/storage-targets/rclone/backends` | 列出可用的 rclone 后端 | +| `POST` | `/api/storage-targets/google-drive/auth-url` | 启动 Google Drive OAuth | +| `POST` | `/api/storage-targets/google-drive/complete` | 完成 OAuth 流程 | ## 节点(集群) -| 端点 | 说明 | -|------|------| -| `GET /api/nodes` | 节点列表 | -| `POST /api/nodes` | 创建节点并返回 Token | -| `PUT /api/nodes/:id` | 重命名 | -| `DELETE /api/nodes/:id` | 删除(有关联任务时会被拒绝) | -| `GET /api/nodes/:id/fs/list` | 浏览目录(远程节点走异步 RPC) | +| 方法 | 端点 | 说明 | +|------|------|------| +| `GET` | `/api/nodes` | 节点列表 | +| `POST` | `/api/nodes` | 创建节点并返回 Token | +| `GET` | `/api/nodes/:id` | 节点详情 | +| `PUT` | `/api/nodes/:id` | 重命名 | +| `DELETE` | `/api/nodes/:id` | 删除(有关联任务时会被拒绝) | +| `GET` | `/api/nodes/:id/fs/list` | 浏览目录(远程节点走 Agent 异步 RPC) | ## Agent 协议(X-Agent-Token) -| 端点 | 说明 | -|------|------| -| `POST /api/agent/heartbeat` | 上报心跳 | -| `POST /api/agent/commands/poll` | 领取一条待执行命令 | -| `POST /api/agent/commands/:id/result` | 上报命令结果 | -| `GET /api/agent/tasks/:id` | 拉取任务规格(含解密后的存储配置) | -| `POST /api/agent/records/:id` | 追加日志 / 更新记录状态 | +Agent CLI 专用端点,通过 `X-Agent-Token` 头认证而非 JWT。 + +| 方法 | 端点 | 说明 | +|------|------|------| +| `POST` | `/api/agent/heartbeat` | 上报心跳(返回节点 ID) | +| `POST` | `/api/agent/commands/poll` | 领取一条待执行命令 | +| `POST` | `/api/agent/commands/:id/result` | 上报命令结果 | +| `GET` | `/api/agent/tasks/:id` | 拉取任务规格(含解密后的存储配置) | +| `POST` | `/api/agent/records/:id` | 追加日志 / 更新记录状态 | ## 通知 -| 端点 | 说明 | -|------|------| -| `GET /api/notifications` | 列表 | -| `POST /api/notifications` | 创建 | +| 方法 | 端点 | 说明 | +|------|------|------| +| `GET` | `/api/notifications` | 列表 | +| `POST` | `/api/notifications` | 创建 | +| `GET` | `/api/notifications/:id` | 详情 | +| `PUT` | `/api/notifications/:id` | 更新 | +| `DELETE` | `/api/notifications/:id` | 删除 | +| `POST` | `/api/notifications/test` | 用待审核配置测试 | +| `POST` | `/api/notifications/:id/test` | 重测已保存的通知器 | -## 仪表盘 / 审计 / 系统 +## 仪表盘 -| 端点 | 说明 | -|------|------| -| `GET /api/dashboard/stats` | 概览统计 | -| `GET /api/audit-logs` | 审计日志 | -| `GET /api/system/info` | 系统信息 | -| `GET /api/system/update-check` | 检查是否有新版本 | +| 方法 | 端点 | 说明 | +|------|------|------| +| `GET` | `/api/dashboard/stats` | 概览统计 | +| `GET` | `/api/dashboard/timeline` | 最近活动时间线 | + +## 审计 / 系统 / 设置 + +| 方法 | 端点 | 说明 | +|------|------|------| +| `GET` | `/api/audit-logs` | 审计日志 | +| `GET` | `/api/system/info` | 系统信息 | +| `GET` | `/api/system/update-check` | 检查新版本 | +| `GET` | `/api/settings` | 系统级设置 | +| `PUT` | `/api/settings` | 更新系统设置 | + +## 响应结构 + +成功响应统一为: + +```json +{ + "code": "OK", + "message": "", + "data": { /* 实际数据 */ } +} +``` + +错误返回 HTTP 4xx/5xx,并带: + +```json +{ + "code": "BACKUP_TASK_NOT_FOUND", + "message": "备份任务不存在", + "data": null +} +``` diff --git a/docs-site/src/components/HomepageFeatures/index.tsx b/docs-site/src/components/HomepageFeatures/index.tsx index e44f5d1..41010c2 100644 --- a/docs-site/src/components/HomepageFeatures/index.tsx +++ b/docs-site/src/components/HomepageFeatures/index.tsx @@ -1,46 +1,103 @@ import type {ReactNode} from 'react'; -import clsx from 'clsx'; import Heading from '@theme/Heading'; import Translate from '@docusaurus/Translate'; +import Link from '@docusaurus/Link'; import styles from './styles.module.css'; type FeatureItem = { title: ReactNode; description: ReactNode; + icon: ReactNode; + link?: string; }; +const DatabaseIcon = () => ( + <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"> + <ellipse cx="12" cy="5" rx="9" ry="3" /> + <path d="M3 5v6c0 1.66 4 3 9 3s9-1.34 9-3V5" /> + <path d="M3 11v6c0 1.66 4 3 9 3s9-1.34 9-3v-6" /> + </svg> +); + +const CloudIcon = () => ( + <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"> + <path d="M18 10h-1.26A8 8 0 109 20h9a5 5 0 000-10z" /> + </svg> +); + +const ClockIcon = () => ( + <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"> + <circle cx="12" cy="12" r="10" /> + <polyline points="12 6 12 12 16 14" /> + </svg> +); + +const NetworkIcon = () => ( + <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"> + <rect x="9" y="2" width="6" height="6" rx="1" /> + <rect x="2" y="16" width="6" height="6" rx="1" /> + <rect x="16" y="16" width="6" height="6" rx="1" /> + <path d="M12 8v4" /> + <path d="M12 12H5v4" /> + <path d="M12 12h7v4" /> + </svg> +); + +const ShieldIcon = () => ( + <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"> + <path d="M12 2l9 4v6c0 5-3.5 9.5-9 10-5.5-.5-9-5-9-10V6l9-4z" /> + <polyline points="9 12 11 14 15 10" /> + </svg> +); + +const RocketIcon = () => ( + <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"> + <path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 00-2.91-.09z" /> + <path d="M12 15l-3-3a22 22 0 012-3.95A12.88 12.88 0 0122 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 01-4 2z" /> + <path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0" /> + <path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5" /> + </svg> +); + const FEATURES: FeatureItem[] = [ { title: <Translate id="feat.types.title">Many Backup Types</Translate>, description: ( <Translate id="feat.types.desc"> - Files & directories with multi-path sources, plus MySQL, PostgreSQL, SQLite, and SAP HANA — all in one place. + Files and directories with multi-path sources, plus MySQL, PostgreSQL, SQLite, and SAP HANA — all in one place. </Translate> ), + icon: <DatabaseIcon />, + link: '/docs/features/backup-types', }, { title: <Translate id="feat.storage.title">70+ Storage Backends</Translate>, description: ( <Translate id="feat.storage.desc"> - Native Alibaba OSS, Tencent COS, Qiniu, S3, Google Drive, WebDAV, FTP — plus SFTP, Azure Blob, Dropbox, OneDrive and dozens more via rclone. + Native Alibaba OSS, Tencent COS, Qiniu, S3, Google Drive, WebDAV, FTP — plus SFTP, Azure Blob, Dropbox and more via rclone. </Translate> ), + icon: <CloudIcon />, + link: '/docs/features/storage-backends', }, { - title: <Translate id="feat.scheduling.title">Scheduling & Retention</Translate>, + title: <Translate id="feat.scheduling.title">Scheduling & Retention</Translate>, description: ( <Translate id="feat.scheduling.desc"> Cron-based schedules with a visual editor and auto-retention (by days or count), plus empty-directory cleanup. </Translate> ), + icon: <ClockIcon />, }, { title: <Translate id="feat.cluster.title">Multi-Node Cluster</Translate>, description: ( <Translate id="feat.cluster.desc"> - Master-Agent mode manages backups across multiple servers. Agents run tasks locally and upload straight to storage — no reverse connectivity required. + Master-Agent via HTTP long-polling. Agents run tasks locally and upload directly to storage — no reverse connectivity. </Translate> ), + icon: <NetworkIcon />, + link: '/docs/features/multi-node', }, { title: <Translate id="feat.security.title">Secure by Default</Translate>, @@ -49,33 +106,64 @@ const FEATURES: FeatureItem[] = [ JWT auth, bcrypt passwords, AES-256-GCM encrypted config, optional backup encryption, and a full audit log. </Translate> ), + icon: <ShieldIcon />, }, { title: <Translate id="feat.deploy.title">Painless Deployment</Translate>, description: ( <Translate id="feat.deploy.desc"> - Single static binary with embedded SQLite. Docker one-click or bare-metal via install.sh — zero external dependencies. + Single static binary with embedded SQLite. Docker one-click or bare-metal — zero external dependencies. </Translate> ), + icon: <RocketIcon />, + link: '/docs/getting-started/installation', }, ]; -function Feature({title, description}: FeatureItem) { - return ( - <div className={clsx('col col--4', styles.feature)}> - <Heading as="h3">{title}</Heading> - <p>{description}</p> - </div> +function Feature({title, description, icon, link}: FeatureItem) { + const content = ( + <> + <div className={styles.iconWrap}>{icon}</div> + <Heading as="h3" className={styles.featureTitle}>{title}</Heading> + <p className={styles.featureDesc}>{description}</p> + {link && ( + <span className={styles.featureLink}> + <Translate id="feat.learnMore">Learn more</Translate> + <span className={styles.featureArrow} aria-hidden="true">→</span> + </span> + )} + </> ); + if (link) { + return ( + <Link to={link} className={styles.featureCardLink}> + {content} + </Link> + ); + } + return <div className={styles.featureCard}>{content}</div>; } export default function HomepageFeatures(): ReactNode { return ( - <section className={styles.features}> + <section className={styles.section}> <div className="container"> - <div className="row"> - {FEATURES.map((props, idx) => ( - <Feature key={idx} {...props} /> + <div className={styles.sectionHead}> + <div className={styles.sectionTag}> + <Translate id="section.features.tag">FEATURES</Translate> + </div> + <Heading as="h2" className={styles.sectionTitle}> + <Translate id="section.features.title">Everything you need, nothing you don't</Translate> + </Heading> + <p className={styles.sectionSubtitle}> + <Translate id="section.features.subtitle"> + Battle-tested building blocks — backup runners, storage providers, scheduling, and clustering. + </Translate> + </p> + </div> + <div className={styles.grid}> + {FEATURES.map((feat, idx) => ( + <Feature key={idx} {...feat} /> ))} </div> </div> diff --git a/docs-site/src/components/HomepageFeatures/styles.module.css b/docs-site/src/components/HomepageFeatures/styles.module.css index c41be8b..cb50d11 100644 --- a/docs-site/src/components/HomepageFeatures/styles.module.css +++ b/docs-site/src/components/HomepageFeatures/styles.module.css @@ -1,20 +1,148 @@ -.features { +.section { + padding: 6rem 0 4rem; +} + +.sectionHead { + text-align: center; + max-width: 720px; + margin: 0 auto 3rem; +} + +.sectionTag { + display: inline-block; + font-size: 12px; + font-weight: 600; + letter-spacing: 0.15em; + color: var(--ifm-color-primary); + padding: 4px 12px; + background: rgba(22, 93, 255, 0.08); + border-radius: 4px; + margin-bottom: 1rem; +} + +[data-theme='dark'] .sectionTag { + background: rgba(96, 126, 255, 0.18); + color: var(--ifm-color-primary-lighter); +} + +.sectionTitle { + font-size: clamp(1.8rem, 3vw, 2.5rem); + line-height: 1.2; + letter-spacing: -0.02em; + font-weight: 700; + margin: 0 0 1rem; + color: var(--ifm-heading-color); +} + +.sectionSubtitle { + font-size: 1.05rem; + line-height: 1.65; + color: var(--ifm-color-content-secondary); + margin: 0; +} + +.grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1.25rem; +} + +@media (max-width: 996px) { + .section { + padding: 3.5rem 0 2rem; + } + .grid { + grid-template-columns: 1fr; + } +} + +@media (min-width: 997px) and (max-width: 1200px) { + .grid { + grid-template-columns: repeat(2, 1fr); + } +} + +.featureCard, +.featureCardLink { + position: relative; + display: flex; + flex-direction: column; + padding: 1.75rem; + background: var(--ifm-background-color); + border: 1px solid var(--ifm-color-emphasis-200); + border-radius: 12px; + transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease; + text-decoration: none !important; + color: inherit; + height: 100%; +} + +.featureCardLink:hover { + transform: translateY(-3px); + border-color: var(--ifm-color-primary); + box-shadow: 0 12px 30px -8px rgba(22, 93, 255, 0.18); + color: inherit; +} + +[data-theme='dark'] .featureCard, +[data-theme='dark'] .featureCardLink { + background: rgba(255, 255, 255, 0.02); + border-color: rgba(255, 255, 255, 0.08); +} + +[data-theme='dark'] .featureCardLink:hover { + background: rgba(64, 128, 255, 0.05); + border-color: var(--ifm-color-primary); + box-shadow: 0 12px 30px -8px rgba(64, 128, 255, 0.25); +} + +.iconWrap { + width: 48px; + height: 48px; + border-radius: 10px; display: flex; align-items: center; - padding: 3rem 0; - width: 100%; + justify-content: center; + background: linear-gradient(135deg, rgba(22, 93, 255, 0.1) 0%, rgba(143, 75, 255, 0.08) 100%); + color: var(--ifm-color-primary); + margin-bottom: 1.25rem; } -.feature { - padding: 1.2rem 1rem; +[data-theme='dark'] .iconWrap { + background: linear-gradient(135deg, rgba(96, 126, 255, 0.15) 0%, rgba(143, 75, 255, 0.12) 100%); + color: var(--ifm-color-primary-lighter); } -.feature h3 { +.featureTitle { font-size: 1.15rem; - margin-bottom: 0.5rem; + font-weight: 600; + margin: 0 0 0.6rem; + color: var(--ifm-heading-color); + letter-spacing: -0.01em; } -.feature p { - color: var(--ifm-color-content-secondary); +.featureDesc { + font-size: 0.95rem; line-height: 1.65; + color: var(--ifm-color-content-secondary); + margin: 0; + flex: 1; +} + +.featureLink { + display: inline-flex; + align-items: center; + gap: 4px; + margin-top: 1rem; + font-size: 13px; + font-weight: 500; + color: var(--ifm-color-primary); +} + +.featureArrow { + transition: transform 0.2s ease; +} + +.featureCardLink:hover .featureArrow { + transform: translateX(4px); } diff --git a/docs-site/src/components/HomepageShowcase/index.tsx b/docs-site/src/components/HomepageShowcase/index.tsx new file mode 100644 index 0000000..4a869ca --- /dev/null +++ b/docs-site/src/components/HomepageShowcase/index.tsx @@ -0,0 +1,120 @@ +import type {ReactNode} from 'react'; +import {useState} from 'react'; +import clsx from 'clsx'; +import Heading from '@theme/Heading'; +import Translate from '@docusaurus/Translate'; +import useBaseUrl from '@docusaurus/useBaseUrl'; +import Link from '@docusaurus/Link'; +import styles from './styles.module.css'; + +type Tab = { + id: string; + label: ReactNode; + image: string; + title: ReactNode; + description: ReactNode; +}; + +function useTabs(): Tab[] { + return [ + { + id: 'dashboard', + label: <Translate id="showcase.tab.dashboard">Dashboard</Translate>, + image: useBaseUrl('/img/screenshots/dashboard.png'), + title: <Translate id="showcase.dashboard.title">Know at a glance</Translate>, + description: ( + <Translate id="showcase.dashboard.desc"> + Backup success rates, storage usage, recent runs and upcoming schedules — all on one page with live data. + </Translate> + ), + }, + { + id: 'tasks', + label: <Translate id="showcase.tab.tasks">Backup Tasks</Translate>, + image: useBaseUrl('/img/screenshots/backup-tasks.png'), + title: <Translate id="showcase.tasks.title">Visual task editor</Translate>, + description: ( + <Translate id="showcase.tasks.desc"> + Files, MySQL, PostgreSQL, SQLite and SAP HANA with a three-step wizard. Cron editor, multi-target dispatch, retention, compression and encryption — point and click. + </Translate> + ), + }, + { + id: 'storage', + label: <Translate id="showcase.tab.storage">Storage Targets</Translate>, + image: useBaseUrl('/img/screenshots/storage-targets.png'), + title: <Translate id="showcase.storage.title">70+ backends, one flow</Translate>, + description: ( + <Translate id="showcase.storage.desc"> + Alibaba OSS, Tencent COS, S3, Google Drive, WebDAV — plus every rclone backend behind a uniform form. Test connection, favourite, and view live usage. + </Translate> + ), + }, + { + id: 'nodes', + label: <Translate id="showcase.tab.nodes">Multi-Node</Translate>, + image: useBaseUrl('/img/screenshots/nodes.png'), + title: <Translate id="showcase.nodes.title">Master-Agent in minutes</Translate>, + description: ( + <Translate id="showcase.nodes.desc"> + Create a node, copy the token, start the Agent on any remote host. Tasks routed to a node run locally there and upload directly to storage — no reverse connectivity required. + </Translate> + ), + }, + ]; +} + +export default function HomepageShowcase(): ReactNode { + const tabs = useTabs(); + const [active, setActive] = useState(tabs[0].id); + const current = tabs.find(t => t.id === active) ?? tabs[0]; + return ( + <section className={styles.section}> + <div className="container"> + <div className={styles.sectionHead}> + <div className={styles.sectionTag}> + <Translate id="showcase.tag">PRODUCT</Translate> + </div> + <Heading as="h2" className={styles.sectionTitle}> + <Translate id="showcase.title">A polished console, not a DIY script</Translate> + </Heading> + <p className={styles.sectionSubtitle}> + <Translate id="showcase.subtitle"> + Every screen designed for day-2 operations — visibility first, configuration second. + </Translate> + </p> + </div> + <div className={styles.tabs}> + {tabs.map(tab => ( + <button + key={tab.id} + type="button" + className={clsx(styles.tabBtn, active === tab.id && styles.tabBtnActive)} + onClick={() => setActive(tab.id)}> + {tab.label} + </button> + ))} + </div> + <div className={styles.stage}> + <div className={styles.browser}> + <div className={styles.browserBar}> + <span className={clsx(styles.browserDot, styles.browserDotRed)} /> + <span className={clsx(styles.browserDot, styles.browserDotYellow)} /> + <span className={clsx(styles.browserDot, styles.browserDotGreen)} /> + <div className={styles.browserUrl}>backupx.local</div> + </div> + <img src={current.image} alt="" className={styles.screenshot} /> + </div> + <div className={styles.caption}> + <Heading as="h3" className={styles.captionTitle}>{current.title}</Heading> + <p className={styles.captionDesc}>{current.description}</p> + <Link to="/docs/getting-started/quick-start" className={styles.captionLink}> + <Translate id="showcase.cta">Explore the docs</Translate> + <span aria-hidden="true"> →</span> + </Link> + </div> + </div> + </div> + </section> + ); +} diff --git a/docs-site/src/components/HomepageShowcase/styles.module.css b/docs-site/src/components/HomepageShowcase/styles.module.css new file mode 100644 index 0000000..c40b229 --- /dev/null +++ b/docs-site/src/components/HomepageShowcase/styles.module.css @@ -0,0 +1,196 @@ +.section { + padding: 4rem 0 6rem; + background: linear-gradient(180deg, transparent 0%, rgba(22, 93, 255, 0.03) 100%); +} + +[data-theme='dark'] .section { + background: linear-gradient(180deg, transparent 0%, rgba(64, 128, 255, 0.04) 100%); +} + +.sectionHead { + text-align: center; + max-width: 720px; + margin: 0 auto 2.5rem; +} + +.sectionTag { + display: inline-block; + font-size: 12px; + font-weight: 600; + letter-spacing: 0.15em; + color: #8f4bff; + padding: 4px 12px; + background: rgba(143, 75, 255, 0.08); + border-radius: 4px; + margin-bottom: 1rem; +} + +[data-theme='dark'] .sectionTag { + background: rgba(143, 75, 255, 0.18); +} + +.sectionTitle { + font-size: clamp(1.8rem, 3vw, 2.5rem); + line-height: 1.2; + letter-spacing: -0.02em; + font-weight: 700; + margin: 0 0 1rem; + color: var(--ifm-heading-color); +} + +.sectionSubtitle { + font-size: 1.05rem; + line-height: 1.65; + color: var(--ifm-color-content-secondary); + margin: 0; +} + +/* Tab bar */ +.tabs { + display: flex; + justify-content: center; + gap: 8px; + margin-bottom: 2rem; + flex-wrap: wrap; +} + +.tabBtn { + padding: 8px 18px; + background: transparent; + border: 1px solid var(--ifm-color-emphasis-300); + border-radius: 999px; + color: var(--ifm-color-content-secondary); + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; +} + +.tabBtn:hover { + color: var(--ifm-color-primary); + border-color: var(--ifm-color-primary); +} + +.tabBtnActive, +.tabBtnActive:hover { + background: linear-gradient(90deg, #165dff 0%, #4080ff 100%); + color: #fff !important; + border-color: transparent; + box-shadow: 0 4px 14px rgba(22, 93, 255, 0.3); +} + +/* Stage */ +.stage { + display: grid; + grid-template-columns: 1.4fr 1fr; + gap: 3rem; + align-items: center; +} + +@media (max-width: 996px) { + .stage { + grid-template-columns: 1fr; + gap: 1.5rem; + } +} + +.browser { + background: var(--ifm-background-color); + border-radius: 12px; + overflow: hidden; + box-shadow: + 0 30px 60px -20px rgba(22, 93, 255, 0.25), + 0 0 0 1px var(--ifm-color-emphasis-200); +} + +[data-theme='dark'] .browser { + box-shadow: + 0 30px 60px -20px rgba(0, 0, 0, 0.5), + 0 0 0 1px rgba(255, 255, 255, 0.06); +} + +.browserBar { + display: flex; + align-items: center; + gap: 6px; + padding: 10px 14px; + background: var(--ifm-color-emphasis-100); + border-bottom: 1px solid var(--ifm-color-emphasis-200); +} + +[data-theme='dark'] .browserBar { + background: rgba(255, 255, 255, 0.03); + border-bottom-color: rgba(255, 255, 255, 0.06); +} + +.browserDot { + width: 11px; + height: 11px; + border-radius: 50%; +} + +.browserDotRed { background: #ff5f56; } +.browserDotYellow { background: #ffbd2e; } +.browserDotGreen { background: #27c93f; } + +.browserUrl { + margin: 0 auto; + padding: 3px 14px; + background: var(--ifm-background-color); + border-radius: 999px; + font-size: 12px; + color: var(--ifm-color-content-secondary); + font-family: 'SFMono-Regular', Menlo, monospace; + border: 1px solid var(--ifm-color-emphasis-200); +} + +[data-theme='dark'] .browserUrl { + background: rgba(255, 255, 255, 0.04); + border-color: rgba(255, 255, 255, 0.06); +} + +.screenshot { + display: block; + width: 100%; + height: auto; + background: var(--ifm-color-emphasis-100); +} + +.caption { + padding: 0 1rem; +} + +@media (max-width: 996px) { + .caption { + padding: 0; + } +} + +.captionTitle { + font-size: 1.7rem; + line-height: 1.2; + letter-spacing: -0.02em; + font-weight: 700; + margin: 0 0 1rem; + color: var(--ifm-heading-color); +} + +.captionDesc { + font-size: 1.05rem; + line-height: 1.7; + color: var(--ifm-color-content-secondary); + margin: 0 0 1.25rem; +} + +.captionLink { + display: inline-flex; + align-items: center; + gap: 4px; + font-weight: 500; + color: var(--ifm-color-primary); + text-decoration: none !important; +} + +.captionLink:hover { + color: var(--ifm-color-primary-dark); +} diff --git a/docs-site/src/css/custom.css b/docs-site/src/css/custom.css index bac38a9..981db85 100644 --- a/docs-site/src/css/custom.css +++ b/docs-site/src/css/custom.css @@ -1,19 +1,56 @@ /** * BackupX 官方文档站样式 + * 灵感:Ant Design / Arco Design */ :root { + /* Primary palette (Arco blue) */ --ifm-color-primary: #165dff; --ifm-color-primary-dark: #0e4fe6; - --ifm-color-primary-darker: #0e4bd9; - --ifm-color-primary-darkest: #0b3eb3; + --ifm-color-primary-darker: #0b4bd9; + --ifm-color-primary-darkest: #093eb3; --ifm-color-primary-light: #2f6cff; --ifm-color-primary-lighter: #3d75ff; --ifm-color-primary-lightest: #668eff; - --ifm-code-font-size: 92%; - --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.08); - --ifm-font-family-base: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif; + + /* Surfaces */ + --ifm-background-color: #ffffff; + --ifm-background-surface-color: #ffffff; + --ifm-color-emphasis-100: #f7f9fc; + --ifm-color-emphasis-200: #eef1f6; + --ifm-color-emphasis-300: #dde3ec; + + /* Typography */ + --ifm-font-family-base: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif; + --ifm-font-family-monospace: 'SFMono-Regular', Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; --ifm-heading-font-weight: 600; + --ifm-code-font-size: 92%; + --ifm-h1-font-size: 2.25rem; + --ifm-h2-font-size: 1.75rem; + --ifm-h3-font-size: 1.35rem; + --ifm-line-height-base: 1.7; + + --ifm-color-content: #1d2129; + --ifm-color-content-secondary: #4e5969; + --ifm-heading-color: #1d2129; + + /* Navbar */ + --ifm-navbar-height: 64px; + --ifm-navbar-background-color: rgba(255, 255, 255, 0.82); + --ifm-navbar-link-color: #4e5969; + --ifm-navbar-link-hover-color: var(--ifm-color-primary); + + /* Sidebar */ + --ifm-menu-color: #4e5969; + --ifm-menu-color-background-active: rgba(22, 93, 255, 0.08); + --ifm-menu-color-background-hover: var(--ifm-color-emphasis-100); + + /* Code */ + --ifm-code-background: rgba(22, 93, 255, 0.06); + --docusaurus-highlighted-code-line-bg: rgba(22, 93, 255, 0.08); + + /* Hero background helper (consumed in index.module.css) */ + --bx-hero-bg: transparent; } [data-theme='dark'] { @@ -24,14 +61,174 @@ --ifm-color-primary-light: #5a93ff; --ifm-color-primary-lighter: #74a5ff; --ifm-color-primary-lightest: #9dbfff; - --docusaurus-highlighted-code-line-bg: rgba(255, 255, 255, 0.08); + --ifm-background-color: #0f1115; + --ifm-background-surface-color: #16181d; + --ifm-color-emphasis-100: #1a1d23; + --ifm-color-emphasis-200: #23272f; + --ifm-color-emphasis-300: #2e343d; + + --ifm-color-content: #e6e9ef; + --ifm-color-content-secondary: #9aa3b2; + --ifm-heading-color: #f0f2f5; + + --ifm-navbar-background-color: rgba(15, 17, 21, 0.82); + --ifm-navbar-link-color: #c9d1db; + + --ifm-menu-color: #c9d1db; + --ifm-menu-color-background-active: rgba(64, 128, 255, 0.15); + --ifm-menu-color-background-hover: rgba(255, 255, 255, 0.04); + + --ifm-code-background: rgba(64, 128, 255, 0.14); + --docusaurus-highlighted-code-line-bg: rgba(64, 128, 255, 0.18); } -.hero--primary { - background: linear-gradient(135deg, #165dff 0%, #0b3eb3 100%); +/* Frosted-glass navbar */ +.navbar { + backdrop-filter: saturate(180%) blur(10px); + -webkit-backdrop-filter: saturate(180%) blur(10px); + border-bottom: 1px solid var(--ifm-color-emphasis-200); + box-shadow: none; +} + +[data-theme='dark'] .navbar { + border-bottom-color: rgba(255, 255, 255, 0.06); } .navbar__title { font-weight: 700; + letter-spacing: -0.01em; +} + +.navbar__link { + font-weight: 500; + font-size: 14px; +} + +/* Sidebar tweaks */ +.menu__link { + font-size: 14px; + border-radius: 6px; + padding: 6px 10px; + line-height: 1.4; +} + +.menu__link--active, +.menu__link--active:hover { + font-weight: 600; +} + +.theme-doc-sidebar-container { + border-right: 1px solid var(--ifm-color-emphasis-200) !important; +} + +[data-theme='dark'] .theme-doc-sidebar-container { + border-right-color: rgba(255, 255, 255, 0.06) !important; +} + +/* Article: better heading rhythm */ +.markdown h2 { + margin-top: 2.5rem; + padding-top: 0.5rem; + border-top: 1px solid var(--ifm-color-emphasis-200); +} + +[data-theme='dark'] .markdown h2 { + border-top-color: rgba(255, 255, 255, 0.06); +} + +.markdown h3 { + margin-top: 2rem; +} + +/* Tables */ +.markdown table { + border-radius: 8px; + overflow: hidden; + box-shadow: 0 0 0 1px var(--ifm-color-emphasis-200); + border-collapse: separate; + border-spacing: 0; +} + +.markdown table thead tr { + background: var(--ifm-color-emphasis-100); +} + +.markdown table th, +.markdown table td { + border: none; + border-bottom: 1px solid var(--ifm-color-emphasis-200); + padding: 10px 14px; +} + +.markdown table tr:last-child td { + border-bottom: none; +} + +/* Inline code */ +code { + background: var(--ifm-code-background); + border: none; + padding: 2px 6px; + border-radius: 4px; + font-size: 0.92em; +} + +/* Admonitions: softer */ +.theme-admonition { + border-radius: 8px; + border-width: 1px; + border-left-width: 4px; +} + +/* Footer */ +.footer { + --ifm-footer-background-color: #141720; + --ifm-footer-color: #9aa3b2; + --ifm-footer-link-color: #c9d1db; + --ifm-footer-link-hover-color: #ffffff; + --ifm-footer-title-color: #f0f2f5; + padding: 3.5rem 0 2.5rem; +} + +.footer__title { + font-size: 13px; + letter-spacing: 0.08em; + text-transform: uppercase; + font-weight: 600; +} + +.footer__link-item { + font-size: 14px; + transition: color 0.15s ease; +} + +.footer__bottom { + border-top: 1px solid rgba(255, 255, 255, 0.06); + padding-top: 2rem; + margin-top: 2.5rem; +} + +.footer__copyright { + font-size: 13px; + color: #6b7280; +} + +/* Scrollbar */ +::-webkit-scrollbar { + width: 10px; + height: 10px; +} + +::-webkit-scrollbar-thumb { + background: var(--ifm-color-emphasis-300); + border-radius: 10px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--ifm-color-emphasis-400, #adb5bd); +} + +[data-theme='dark'] ::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.15); } diff --git a/docs-site/src/pages/index.module.css b/docs-site/src/pages/index.module.css index 7d7fc7c..bf2d549 100644 --- a/docs-site/src/pages/index.module.css +++ b/docs-site/src/pages/index.module.css @@ -1,31 +1,273 @@ -.heroBanner { - padding: 5rem 0 4rem; - text-align: center; +/* ── Hero ───────────────────────────────────────────── */ +.hero { position: relative; + padding: 7rem 0 6rem; overflow: hidden; + background: var(--bx-hero-bg); } -@media screen and (max-width: 996px) { - .heroBanner { - padding: 3rem 1rem; +.heroBg { + position: absolute; + inset: 0; + background: + radial-gradient(circle at 15% 20%, rgba(104, 127, 255, 0.18) 0%, transparent 45%), + radial-gradient(circle at 85% 70%, rgba(22, 93, 255, 0.15) 0%, transparent 50%), + linear-gradient(180deg, #f7f9ff 0%, #ffffff 100%); + z-index: 0; +} + +[data-theme='dark'] .heroBg { + background: + radial-gradient(circle at 15% 20%, rgba(96, 126, 255, 0.22) 0%, transparent 45%), + radial-gradient(circle at 85% 70%, rgba(118, 70, 255, 0.18) 0%, transparent 50%), + linear-gradient(180deg, #0f1115 0%, #0b0d10 100%); +} + +.heroInner { + position: relative; + z-index: 1; + display: grid; + grid-template-columns: 1.1fr 1fr; + gap: 4rem; + align-items: center; +} + +@media (max-width: 996px) { + .hero { + padding: 4rem 0 3rem; + } + .heroInner { + grid-template-columns: 1fr; + gap: 2.5rem; + text-align: left; } } -.buttons { +.heroContent { display: flex; + flex-direction: column; + align-items: flex-start; + gap: 1.25rem; +} + +.badge { + display: inline-flex; align-items: center; - justify-content: center; - gap: 1rem; + gap: 8px; + padding: 4px 14px; + background: rgba(22, 93, 255, 0.08); + border: 1px solid rgba(22, 93, 255, 0.15); + border-radius: 999px; + font-size: 13px; + color: var(--ifm-color-primary); + font-weight: 500; +} + +[data-theme='dark'] .badge { + background: rgba(96, 126, 255, 0.15); + border-color: rgba(96, 126, 255, 0.3); + color: var(--ifm-color-primary-lighter); +} + +.badgeDot { + width: 6px; + height: 6px; + background: var(--ifm-color-primary); + border-radius: 50%; + box-shadow: 0 0 0 4px rgba(22, 93, 255, 0.18); + animation: pulse 2s infinite; +} + +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} + +.heroTitle { + font-size: clamp(2.25rem, 4vw, 3.4rem); + line-height: 1.15; + letter-spacing: -0.025em; + font-weight: 700; + margin: 0; + color: var(--ifm-heading-color); +} + +.heroTitleAccent { + display: block; + background: linear-gradient(90deg, #4080ff 0%, #8f4bff 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + margin-top: 6px; +} + +.heroSubtitle { + font-size: 1.15rem; + line-height: 1.65; + color: var(--ifm-color-content-secondary); + max-width: 540px; + margin: 0; +} + +.actions { + display: flex; + gap: 12px; + margin-top: 8px; flex-wrap: wrap; - margin-top: 1.5rem; +} + +.primaryBtn { + background: linear-gradient(90deg, #165dff 0%, #4080ff 100%); + border: none; + color: #fff; + display: inline-flex; + align-items: center; + gap: 6px; + font-weight: 600; + box-shadow: 0 6px 20px rgba(22, 93, 255, 0.3); + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.primaryBtn:hover { + transform: translateY(-1px); + box-shadow: 0 10px 25px rgba(22, 93, 255, 0.4); + color: #fff; +} + +.btnArrow { + transition: transform 0.2s ease; +} + +.primaryBtn:hover .btnArrow { + transform: translateX(4px); } .secondaryBtn { - color: #fff !important; - border-color: #fff; + background: var(--ifm-background-color); + border: 1px solid var(--ifm-color-emphasis-300); + color: var(--ifm-font-color-base); + display: inline-flex; + align-items: center; + font-weight: 500; + transition: all 0.2s ease; } .secondaryBtn:hover { - background-color: rgba(255, 255, 255, 0.15); - color: #fff; + border-color: var(--ifm-color-primary); + color: var(--ifm-color-primary); + background: var(--ifm-background-color); +} + +.metrics { + display: flex; + align-items: center; + gap: 1.75rem; + padding-top: 1.5rem; + margin-top: 0.5rem; +} + +.metric { + display: flex; + flex-direction: column; + gap: 2px; +} + +.metricValue { + font-size: 1.6rem; + font-weight: 700; + color: var(--ifm-heading-color); + line-height: 1.1; + letter-spacing: -0.02em; +} + +.metricLabel { + font-size: 12px; + color: var(--ifm-color-content-secondary); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.metricDivider { + width: 1px; + height: 30px; + background: var(--ifm-color-emphasis-300); +} + +/* ── Code window (macOS-style) ─────────────────────── */ +.heroCode { + position: relative; +} + +.codeWindow { + background: #0f1622; + border-radius: 12px; + box-shadow: + 0 20px 50px -10px rgba(15, 22, 34, 0.35), + 0 0 0 1px rgba(255, 255, 255, 0.05); + overflow: hidden; + border: 1px solid rgba(255, 255, 255, 0.06); +} + +[data-theme='light'] .codeWindow { + box-shadow: 0 20px 50px -10px rgba(22, 93, 255, 0.2), 0 0 0 1px rgba(22, 93, 255, 0.06); +} + +.codeHeader { + display: flex; + align-items: center; + gap: 6px; + padding: 10px 14px; + background: #161f2e; + border-bottom: 1px solid rgba(255, 255, 255, 0.04); +} + +.codeDot { + width: 11px; + height: 11px; + border-radius: 50%; +} + +.codeDotRed { background: #ff5f56; } +.codeDotYellow { background: #ffbd2e; } +.codeDotGreen { background: #27c93f; } + +.codeTitle { + margin-left: auto; + font-size: 11px; + color: #7b8696; + letter-spacing: 0.05em; + text-transform: uppercase; +} + +.codeBody { + margin: 0; + padding: 18px 20px; + font-family: 'SFMono-Regular', Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; + font-size: 13px; + line-height: 1.65; + color: #e1e7ef; + background: transparent; + overflow-x: auto; +} + +.codeBody code { + background: transparent; + padding: 0; + border: 0; + color: inherit; +} + +.codePrompt { + color: #4080ff; + margin-right: 6px; + user-select: none; +} + +.codeComment { + color: #6e7889; + font-style: italic; +} + +.codeString { + color: #82d1ff; } diff --git a/docs-site/src/pages/index.tsx b/docs-site/src/pages/index.tsx index 0c2b47e..60c2c50 100644 --- a/docs-site/src/pages/index.tsx +++ b/docs-site/src/pages/index.tsx @@ -6,47 +6,106 @@ import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; import Layout from '@theme/Layout'; import Heading from '@theme/Heading'; import HomepageFeatures from '@site/src/components/HomepageFeatures'; +import HomepageShowcase from '@site/src/components/HomepageShowcase'; import styles from './index.module.css'; function HomepageHeader() { return ( - <header className={clsx('hero hero--primary', styles.heroBanner)}> - <div className="container"> - <Heading as="h1" className="hero__title"> - BackupX - </Heading> - <p className="hero__subtitle"> - <Translate id="home.tagline"> - Self-hosted server backup management — one binary, one command, manage every backup - </Translate> - </p> - <div className={styles.buttons}> - <Link - className="button button--secondary button--lg" - to="/docs/getting-started/quick-start"> - <Translate id="home.getStarted">Get Started</Translate> - </Link> - <Link - className={clsx('button button--outline button--secondary button--lg', styles.secondaryBtn)} - to="https://github.com/Awuqing/BackupX"> - GitHub - </Link> + <header className={styles.hero}> + <div className={styles.heroBg} aria-hidden="true" /> + <div className={clsx('container', styles.heroInner)}> + <div className={styles.heroContent}> + <div className={styles.badge}> + <span className={styles.badgeDot} /> + <Translate id="home.badge">Open-source · v1.6.0</Translate> + </div> + <Heading as="h1" className={styles.heroTitle}> + <Translate id="home.title.part1">Self-hosted backup management</Translate> + <span className={styles.heroTitleAccent}> + <Translate id="home.title.part2">for every server.</Translate> + </span> + </Heading> + <p className={styles.heroSubtitle}> + <Translate id="home.tagline"> + One binary, one command. File / database / SAP HANA backups routed to 70+ storage backends. + </Translate> + </p> + <div className={styles.actions}> + <Link className={clsx('button button--primary button--lg', styles.primaryBtn)} to="/docs/getting-started/quick-start"> + <Translate id="home.getStarted">Get Started</Translate> + <span className={styles.btnArrow} aria-hidden="true">→</span> + </Link> + <Link className={clsx('button button--lg', styles.secondaryBtn)} to="https://github.com/Awuqing/BackupX"> + <svg width="18" height="18" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" style={{marginRight: 6}}> + <path d="M8 0C3.58 0 0 3.58 0 8a8 8 0 005.47 7.59c.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" /> + </svg> + GitHub + </Link> + </div> + <div className={styles.metrics}> + <MetricItem labelId="home.metric.backends" valueClass={styles.metricValue}>70+</MetricItem> + <div className={styles.metricDivider} /> + <MetricItem labelId="home.metric.backupTypes" valueClass={styles.metricValue}>5</MetricItem> + <div className={styles.metricDivider} /> + <MetricItem labelId="home.metric.license" valueClass={styles.metricValue}>Apache 2.0</MetricItem> + </div> + </div> + <div className={styles.heroCode}> + <div className={styles.codeWindow}> + <div className={styles.codeHeader}> + <span className={clsx(styles.codeDot, styles.codeDotRed)} /> + <span className={clsx(styles.codeDot, styles.codeDotYellow)} /> + <span className={clsx(styles.codeDot, styles.codeDotGreen)} /> + <span className={styles.codeTitle}>bash</span> + </div> + <pre className={styles.codeBody}> + <code> + <span className={styles.codeComment}># Docker one-liner</span>{'\n'} + <span className={styles.codePrompt}>$</span> docker run -d --name backupx \{'\n'} + {' '}-p 8340:8340 \{'\n'} + {' '}-v backupx-data:/app/data \{'\n'} + {' '}awuqing/backupx:latest{'\n'} + {'\n'} + <span className={styles.codeComment}># Open http://localhost:8340</span>{'\n'} + <span className={styles.codeComment}># Deploy an Agent on a remote host</span>{'\n'} + <span className={styles.codePrompt}>$</span> backupx agent \{'\n'} + {' '}--master <span className={styles.codeString}>http://master:8340</span> \{'\n'} + {' '}--token <span className={styles.codeString}><token></span> + </code> + </pre> + </div> </div> </div> </header> ); } +function MetricItem({children, labelId, valueClass}: {children: ReactNode; labelId: string; valueClass: string}) { + return ( + <div className={styles.metric}> + <div className={valueClass}>{children}</div> + <div className={styles.metricLabel}> + <Translate id={labelId}> + {labelId === 'home.metric.backends' ? 'Storage backends' + : labelId === 'home.metric.backupTypes' ? 'Backup types' + : 'License'} + </Translate> + </div> + </div> + ); +} + export default function Home(): ReactNode { const {siteConfig} = useDocusaurusContext(); return ( <Layout - title={translate({id: 'home.title', message: 'Self-hosted backup management'})} + title={translate({id: 'home.pageTitle', message: 'Self-hosted backup management'})} description={siteConfig.tagline}> <HomepageHeader /> <main> <HomepageFeatures /> + <HomepageShowcase /> </main> </Layout> ); diff --git a/docs-site/static/img/screenshots/backup-tasks.png b/docs-site/static/img/screenshots/backup-tasks.png new file mode 100644 index 0000000..82afb4a Binary files /dev/null and b/docs-site/static/img/screenshots/backup-tasks.png differ diff --git a/docs-site/static/img/screenshots/dashboard.png b/docs-site/static/img/screenshots/dashboard.png new file mode 100644 index 0000000..ba89f42 Binary files /dev/null and b/docs-site/static/img/screenshots/dashboard.png differ diff --git a/docs-site/static/img/screenshots/nodes.png b/docs-site/static/img/screenshots/nodes.png new file mode 100644 index 0000000..baaa6dc Binary files /dev/null and b/docs-site/static/img/screenshots/nodes.png differ diff --git a/docs-site/static/img/screenshots/storage-targets.png b/docs-site/static/img/screenshots/storage-targets.png new file mode 100644 index 0000000..170cfb7 Binary files /dev/null and b/docs-site/static/img/screenshots/storage-targets.png differ