文档: 按 Ant/Arco Design 风格重构官网首页,修正 API 参考,完善 i18n (#41)

重构:
- 首页 Hero 重设计:双列布局(标题+CTA+指标 / macOS 风代码窗口)
- 引入渐变文字、pulse 徽章、悬停带动画的主按钮
- 功能卡片加 SVG 图标、悬停提升效果、部分卡片变成可点击链接
- 新增 HomepageShowcase 截图轮播区:Tab 切换四个核心页面(仪表盘/任务/存储/多节点)
- 全站换 Arco 蓝 (#165dff) 作为主色,紫色 (#8f4bff) 作为辅助
- 导航栏加毛玻璃效果、表格加圆角与边框、菜单项圆角化
- 深色模式配色整体收敛

内容修正:
- API 参考补全遗漏的端点:auth logout/profile、records batch-delete、
  storage-targets star/usage/google-drive、notifications test、dashboard timeline、settings
- 把 API 表格改为"方法/端点/说明"三列,加响应结构说明
- 中英文 API 文档同步更新

i18n:
- code.json 补充 Hero、Features、Showcase 全部新翻译键
- 校对:16 个中英文档 frontmatter 完全对齐,无漏译

构建:双语 build 通过、产物 3.3MB
This commit is contained in:
Wu Qing
2026-04-17 13:39:27 +08:00
committed by GitHub
parent bc3d03de7e
commit c629b5f286
14 changed files with 1373 additions and 183 deletions

View File

@@ -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
}
```

View File

@@ -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 <title> 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": "开始阅读文档"}
}

View File

@@ -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
}
```

View File

@@ -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 &amp; 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 &amp; 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>

View File

@@ -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);
}

View File

@@ -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>
);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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}>&lt;token&gt;</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>
);

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB