commit 5591b153e03c6edcfd14d8063cf4dfb1ba0e6dca Author: Ethanfly Date: Fri Dec 26 17:14:06 2025 +0800 first commit diff --git a/README-Phper.md b/README-Phper.md new file mode 100644 index 0000000..2c6fcf6 --- /dev/null +++ b/README-Phper.md @@ -0,0 +1,389 @@ +# PHPer 开发环境管理器 + +

+ PHPer Logo +

+ +

+ 一款功能强大的 Windows PHP 开发环境管理工具 +

+ +

+ 轻松管理 PHP、MySQL、Nginx、Redis、Node.js、Python 等服务,告别繁琐的手动配置 +

+ +

+ 功能特性 • + 安装使用 • + 使用指南 • + 常见问题 +

+ +--- + +## 📸 界面预览 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
仪表盘PHP管理
🏠 仪表盘🐘 PHP 版本管理
MySQL管理Nginx管理
🐬 MySQL 管理🌐 Nginx 管理
Redis管理Node.js管理
🔴 Redis 管理💚 Node.js 管理
Python管理设置
🐍 Python 管理⚙️ 设置
+ +## ✨ 功能特性 + +### 🐘 PHP 版本管理 + +| 功能 | 说明 | +| ------------ | ---------------------------------------------------------- | +| 多版本管理 | 支持同时安装 PHP 8.1、8.2、8.3、8.4、8.5 等多个版本 | +| CGI 独立控制 | 每个 PHP 版本可独立启动/停止 CGI 进程,支持多版本并行运行 | +| 端口自动分配 | 各版本自动分配端口(如 8.4→9084, 8.3→9083) | +| 一键切换 | 点击即可切换 PHP 版本,自动配置系统环境变量 | +| 扩展管理 | 可视化管理 PHP 扩展,支持在线安装(从 PECL) | +| 配置编辑 | 在线编辑 php.ini,无需手动查找配置文件 | +| 自动配置 | 安装时自动启用常用扩展(curl、gd、mbstring、pdo_mysql 等) | +| Composer | 集成 Composer 管理,支持镜像源切换(阿里云、腾讯云等) | +| 日志查看 | 直接查看 PHP 错误日志 | +| 下载源 | 从 [windows.php.net](https://windows.php.net) 官方下载 | + +### 🐬 MySQL 管理 + +| 功能 | 说明 | +| ---------- | -------------------------------------------------------------------- | +| 版本支持 | 支持 MySQL 5.7.x 和 8.0.x 系列 | +| 服务控制 | 启动、停止、重启 MySQL 服务 | +| 密码管理 | 一键修改 root 密码 | +| 配置编辑 | 在线编辑 my.ini 配置文件 | +| 自动初始化 | 安装时自动初始化数据库,开箱即用 | +| 下载源 | 从[阿里云镜像站](https://mirrors.aliyun.com/mysql/)下载,速度更快 | + +### 🌐 Nginx 管理 + +| 功能 | 说明 | +| ------------ | --------------------------------------------- | +| 版本管理 | 支持多个 Nginx 版本,可随时切换 | +| 服务控制 | 启动、停止、重启、热重载配置 | +| 站点管理 | 可视化添加、删除、启用、禁用虚拟主机 | +| Laravel 支持 | 自动生成 Laravel 项目的伪静态配置 | +| SSL 证书 | 支持申请 Let's Encrypt 免费 SSL 证书 | +| 配置编辑 | 在线编辑 nginx.conf 主配置文件 | +| 下载源 | 从 [nginx.org](https://nginx.org) 官方下载 | + +### 🔴 Redis 管理 + +| 功能 | 说明 | +| ------------ | -------------------------------------------------------------------------- | +| Windows 版本 | 使用 Windows 原生编译版 Redis | +| 服务控制 | 启动、停止、重启 Redis 服务 | +| 状态监控 | 实时查看运行状态、内存使用情况 | +| 配置编辑 | 在线编辑 redis.windows.conf 配置 | +| 下载源 | 从 [GitHub (redis-windows)](https://github.com/redis-windows/redis-windows) 下载 | + +### 💚 Node.js 管理 + +| 功能 | 说明 | +| ---------- | -------------------------------------------------------- | +| 多版本管理 | 支持同时安装多个 Node.js 版本 | +| LTS 支持 | 显示 LTS 版本和 Current 版本标识 | +| npm 集成 | 自动显示对应的 npm 版本 | +| 一键切换 | 快速切换默认 Node.js 版本,自动配置环境变量 | +| 下载源 | 从 [nodejs.org](https://nodejs.org) 官方下载 | + +### 🐍 Python 管理 + +| 功能 | 说明 | +| ---------- | -------------------------------------------------------- | +| 嵌入式版本 | 使用免安装的嵌入式版本,不影响系统环境 | +| pip 集成 | 自动配置 pip,支持安装 Python 包 | +| 多版本管理 | 支持同时安装多个 Python 版本 | +| 一键切换 | 快速切换默认 Python 版本 | +| 下载源 | 从 [python.org](https://www.python.org) 官方下载 | + +### 🔧 Git 管理 + +| 功能 | 说明 | +| ---------- | ------------------------------ | +| 版本管理 | 一键安装/卸载 Git for Windows | +| 配置管理 | 可视化配置用户名、邮箱等信息 | +| 环境变量 | 自动配置系统 PATH | + +### 🌍 站点管理 + +- ➕ **快速创建站点** - 填写域名和路径即可创建虚拟主机 +- 🎯 **Laravel 一键配置** - 自动配置 public 目录和伪静态规则 +- 🔒 **SSL 证书申请** - 集成 Let's Encrypt 自动申请 +- 📝 **Hosts 自动配置** - 自动添加域名到系统 hosts 文件 +- 📋 **站点日志查看** - 查看每个站点的访问日志和错误日志 +- 🌐 **一键打开站点** - 点击域名在默认浏览器打开 + +### 📋 日志查看 + +| 功能 | 说明 | +| ------------ | ---------------------------------------------- | +| 多服务日志 | 支持查看 Nginx、PHP、MySQL、Redis 日志 | +| 站点日志 | 查看各站点的访问日志和错误日志 | +| 实时刷新 | 支持刷新日志内容,查看最新记录 | +| 行数控制 | 可配置显示的日志行数(100-5000 行) | +| 快速清空 | 一键清空指定日志文件 | +| 打开目录 | 快速在文件管理器中打开日志目录 | + +### ⚙️ 其他功能 + +- 🚀 **开机自启动** - 可配置各服务开机自动启动(静默模式,无弹窗) +- 🔇 **静默启动** - 所有服务启动无黑色窗口闪烁 +- 📋 **Hosts 管理** - 可视化管理系统 hosts 文件 +- 🌙 **深色/浅色主题** - 支持主题切换 +- 📊 **服务状态监控** - 实时显示各服务运行状态 +- ⚡ **页面切换优化** - 使用 KeepAlive 缓存页面,切换无闪烁 +- 🔢 **自动版本号** - 打包时自动更新版本号 +- 📥 **下载源说明** - 清晰显示各软件的下载来源 +- 🌐 **默认浏览器打开** - 站点链接自动在默认浏览器打开 + +## 🛠️ 技术栈 + +| 技术 | 说明 | +| --------------------------------------------- | ------------ | +| [Vue 3](https://vuejs.org/) | 前端框架 | +| [TypeScript](https://www.typescriptlang.org/) | 类型安全 | +| [Electron](https://www.electronjs.org/) | 桌面应用框架 | +| [Element Plus](https://element-plus.org/) | UI 组件库 | +| [Vite](https://vitejs.dev/) | 构建工具 | +| [Pinia](https://pinia.vuejs.org/) | 状态管理 | + +## 📦 安装使用 + +### 系统要求 + +- ✅ Windows 10/11 (64 位) +- ✅ Node.js 18.0 或更高版本 +- ✅ 管理员权限(用于管理服务和修改 hosts 文件) +- ✅ [Visual C++ Redistributable 2015-2022](https://aka.ms/vs/17/release/vc_redist.x64.exe) + +### 开发环境 + +```bash +# 克隆项目 +git clone https://github.com/your-username/phper.git +cd phper + +# 安装依赖 +npm install + +# 启动开发服务器 +npm run electron:dev +``` + +### 构建生产版本 + +```bash +# 构建 Windows 安装包(自动更新 patch 版本号 +0.0.1) +npm run build + +# 指定版本号更新类型 +npm run build:patch # 1.0.0 -> 1.0.1 +npm run build:minor # 1.0.0 -> 1.1.0 +npm run build:major # 1.0.0 -> 2.0.0 + +# 不更新版本号直接打包 +npm run build:nobump +``` + +构建完成后,安装包将生成在 `release` 目录中。 + +## 📁 项目结构 + +``` +phper/ +├── electron/ # Electron 主进程 +│ ├── main.ts # 主进程入口 +│ ├── preload.ts # 预加载脚本(IPC 通信) +│ └── services/ # 服务管理模块 +│ ├── ConfigStore.ts # 配置存储(使用 electron-store) +│ ├── PhpManager.ts # PHP 版本管理器 +│ ├── MysqlManager.ts # MySQL 服务管理器 +│ ├── NginxManager.ts # Nginx 服务管理器 +│ ├── RedisManager.ts # Redis 服务管理器 +│ ├── NodeManager.ts # Node.js 版本管理器 +│ ├── PythonManager.ts # Python 版本管理器 +│ ├── GitManager.ts # Git 管理器 +│ ├── ServiceManager.ts # 开机自启服务管理器 +│ ├── HostsManager.ts # Hosts 文件管理器 +│ └── LogManager.ts # 日志管理器 +│ +├── src/ # Vue 前端源码 +│ ├── App.vue # 根组件(含 KeepAlive 缓存) +│ ├── main.ts # 入口文件 +│ ├── vite-env.d.ts # 类型声明 +│ ├── router/ # 路由配置 +│ │ └── index.ts +│ ├── stores/ # Pinia 状态管理 +│ │ └── serviceStore.ts # 服务状态存储 +│ ├── components/ # 公共组件 +│ │ └── LogViewer.vue # 日志查看器组件 +│ ├── styles/ # 样式文件 +│ │ └── main.scss # 全局样式(含主题变量) +│ └── views/ # 页面视图 +│ ├── Dashboard.vue # 仪表盘 +│ ├── PhpManager.vue # PHP 管理 +│ ├── MysqlManager.vue # MySQL 管理 +│ ├── NginxManager.vue # Nginx 管理 +│ ├── RedisManager.vue # Redis 管理 +│ ├── NodeManager.vue # Node.js 管理 +│ ├── PythonManager.vue # Python 管理 +│ ├── GitManager.vue # Git 管理 +│ ├── SitesManager.vue # 站点管理 +│ ├── HostsManager.vue # Hosts 管理 +│ └── Settings.vue # 设置 +│ +├── scripts/ # 构建脚本 +│ └── bump-version.js # 版本号自动更新脚本 +│ +├── public/ # 静态资源 +│ ├── icon.svg # 应用图标 +│ └── version.json # 版本信息(构建时生成) +│ +├── index.html # HTML 模板 +├── package.json # 项目配置 +├── vite.config.ts # Vite 配置 +├── tsconfig.json # TypeScript 配置 +└── README.md # 项目说明 +``` + +## 📖 使用指南 + +### 首次使用 + +1. **安装运行时依赖** + + - 确保已安装 [Visual C++ Redistributable 2015-2022](https://aka.ms/vs/17/release/vc_redist.x64.exe) + +2. **以管理员身份运行** + + - 右键点击应用图标,选择"以管理员身份运行" + - 这是管理服务和修改 hosts 文件所必需的 + +3. **安装服务** + - 首次使用需要安装 PHP、MySQL、Nginx、Redis + - 进入对应管理页面,点击"安装"按钮 + +### 创建第一个站点 + +1. 安装并启动 Nginx 和 PHP +2. 进入"站点管理"页面 +3. 点击"添加站点" +4. 填写站点信息: + - 站点名称:如 `myproject` + - 域名:如 `myproject.test` + - 根目录:如 `C:\Projects\myproject`(Laravel 项目无需指定 public) + - 选择 PHP 版本 + - 如果是 Laravel 项目,开启"Laravel 项目"选项 +5. 点击确认,站点即创建完成 +6. 在浏览器访问 http://myproject.test + +### 配置开机自启动 + +1. 进入"设置"页面 +2. 在"开机自启动"部分,开启需要自启的服务 +3. 应用会在 Windows 启动目录创建启动脚本 + +## ❓ 常见问题 + +### Q: 为什么需要管理员权限? + +A: 应用需要管理员权限来: + +- 启动/停止 Windows 服务 +- 修改系统 hosts 文件 +- 配置系统环境变量 + +### Q: PHP 下载很慢怎么办? + +A: PHP 从 windows.php.net 官方下载,如果速度较慢: + +- 可以手动从官网下载 ZIP 文件 +- 解压到 `[安装目录]/php/php-版本号` 目录 +- 重新打开应用即可识别 + +### Q: MySQL 启动失败? + +A: 常见原因: + +- 3306 端口被占用,检查是否有其他 MySQL 实例 +- 防火墙阻止,添加防火墙规则 +- 数据目录权限问题,确保目录可写 + +### Q: 如何卸载服务? + +A: 进入对应服务管理页面,先停止服务,然后点击"卸载"按钮。 + +## 🔗 相关资源 + +| 软件 | 下载源 | +| ------- | ---------------------------------------------------------------------- | +| PHP | [windows.php.net](https://windows.php.net/download/) - 官方 Windows 版 | +| MySQL | [阿里云镜像](https://mirrors.aliyun.com/mysql/) - 国内高速下载 | +| Nginx | [nginx.org](https://nginx.org/en/download.html) - 官方 Windows 版 | +| Redis | [GitHub redis-windows](https://github.com/redis-windows/redis-windows) | +| Node.js | [nodejs.org](https://nodejs.org/en/download/) - 官方下载 | +| Python | [python.org](https://www.python.org/downloads/windows/) - 嵌入式版本 | +| Git | [git-scm.com](https://git-scm.com/download/win) - 官方 Windows 版 | + +## 📄 开源协议 + +本项目基于 [MIT License](LICENSE) 开源。 + +## 🤝 贡献指南 + +欢迎提交 Issue 和 Pull Request! + +1. Fork 本仓库 +2. 创建功能分支 (`git checkout -b feature/AmazingFeature`) +3. 提交更改 (`git commit -m 'Add some AmazingFeature'`) +4. 推送到分支 (`git push origin feature/AmazingFeature`) +5. 提交 Pull Request + +## 📮 反馈建议 + +如果您在使用过程中遇到问题或有任何建议,欢迎: + +- 提交 [Issue](https://github.com/your-username/phper/issues) +- 发送邮件至 your-email@example.com + +--- + +

+ Made with ❤️ for PHP Developers +

+ +

+ ⭐ 如果这个项目对您有帮助,请给我们一个 Star! +

diff --git a/docs/.gitkeep b/docs/.gitkeep new file mode 100644 index 0000000..077cdec --- /dev/null +++ b/docs/.gitkeep @@ -0,0 +1,5 @@ +# 此目录用于存放文档截图 +# 请将以下截图放入此目录: +# - dashboard.png 仪表盘截图 +# - php-manager.png PHP管理页面截图 + diff --git a/docs/dashboard.png b/docs/dashboard.png new file mode 100644 index 0000000..1ecacf7 Binary files /dev/null and b/docs/dashboard.png differ diff --git a/docs/mysql.png b/docs/mysql.png new file mode 100644 index 0000000..2a5d115 Binary files /dev/null and b/docs/mysql.png differ diff --git a/docs/nginx.png b/docs/nginx.png new file mode 100644 index 0000000..3fd2b13 Binary files /dev/null and b/docs/nginx.png differ diff --git a/docs/nodejs.png b/docs/nodejs.png new file mode 100644 index 0000000..71c0890 Binary files /dev/null and b/docs/nodejs.png differ diff --git a/docs/php.png b/docs/php.png new file mode 100644 index 0000000..6d4c7bb Binary files /dev/null and b/docs/php.png differ diff --git a/docs/python.png b/docs/python.png new file mode 100644 index 0000000..f8df66c Binary files /dev/null and b/docs/python.png differ diff --git a/docs/redis.png b/docs/redis.png new file mode 100644 index 0000000..78006f9 Binary files /dev/null and b/docs/redis.png differ diff --git a/docs/setting.png b/docs/setting.png new file mode 100644 index 0000000..5b88e16 Binary files /dev/null and b/docs/setting.png differ diff --git a/favicon.svg b/favicon.svg new file mode 100644 index 0000000..83e7766 --- /dev/null +++ b/favicon.svg @@ -0,0 +1,11 @@ + + + + + + + + + 🐘 + + diff --git a/index.html b/index.html new file mode 100644 index 0000000..5f9f636 --- /dev/null +++ b/index.html @@ -0,0 +1,433 @@ + + + + + + PHPer - Windows PHP 开发环境管理器 + + + + + + + + + + +
+
+
+
+
+
+ + + + + +
+
+
+
+ + Windows 专属 · 开源免费 +
+

+ 告别繁琐配置 + 一键管理开发环境 +

+

+ PHPer 是一款功能强大的 Windows PHP 开发环境管理工具,
+ 轻松管理 PHPMySQLNginxRedisNode.jsPython 等服务 +

+ +
+
+ 7+ + 支持服务 +
+
+
+ 100% + 开源免费 +
+
+
+ Win + 原生支持 +
+
+
+
+
+
+
+ + + +
+ PHPer 管理器 +
+
+
+ 🐘 + PHP 8.4.2 + ● 运行中 + :9084 +
+
+ 🐬 + MySQL 8.0.40 + ● 运行中 + :3306 +
+
+ 🌐 + Nginx 1.27.3 + ● 运行中 + :80 +
+
+ 🔴 + Redis 7.4.1 + ● 运行中 + :6379 +
+
+ 💚 + Node.js 22.12 + ○ 已停止 + +
+
+ + 所有服务已就绪... + +
+
+
+
+
+
+ + +
+
+
+ +

开发环境管理
从未如此简单

+

告别命令行配置,可视化管理所有开发服务

+
+
+
+
🐘
+

PHP 多版本管理

+

支持 PHP 8.1 - 8.5 多版本并行,每个版本独立 CGI 进程,一键切换系统默认版本

+
    +
  • 各版本端口自动分配
  • +
  • 可视化扩展管理
  • +
  • 在线编辑 php.ini
  • +
  • 集成 Composer 管理
  • +
+
+
+
🐬
+

MySQL 管理

+

支持 MySQL 5.7/8.0,一键安装初始化,密码管理,配置编辑

+
+
+
🌐
+

Nginx 管理

+

多版本切换,站点管理,Laravel 自动配置,SSL 证书申请

+
+
+
🔴
+

Redis 管理

+

Windows 原生版 Redis,服务控制,状态监控,配置编辑

+
+
+
💚
+

Node.js 管理

+

多版本管理,LTS 标识,npm 集成,一键切换

+
+
+
🐍
+

Python 管理

+

嵌入式版本,pip 集成,多版本管理,不污染系统

+
+
+
+
+ + +
+
+
+ +

精心设计的用户界面

+

深色/浅色主题,现代化 UI 设计,流畅的操作体验

+
+
+ + + + + + +
+
+
+
+
+ +
+ PHPer +
+
+ 仪表盘 + PHP管理 + MySQL管理 + Nginx管理 + Redis管理 + Node.js管理 +
+
+
+
+
+ + +
+
+
+ + + + +
+
+
+ + +
+
+
+ +

官方渠道安全可靠

+

所有软件均从官方或可信镜像下载,安全有保障

+
+
+
+ 软件 + 下载源 + 说明 +
+
+ 🐘 PHP + windows.php.net + 官方 Windows 版 +
+
+ 🐬 MySQL + 阿里云镜像 + 国内高速下载 +
+
+ 🌐 Nginx + nginx.org + 官方 Windows 版 +
+
+ 🔴 Redis + GitHub + Windows 编译版 +
+
+ 💚 Node.js + nodejs.org + 官方下载 +
+
+ 🐍 Python + python.org + 嵌入式版本 +
+
+ 🔧 Git + git-scm.com + 官方 Windows 版 +
+
+
+
+ + +
+
+
+ +

常见问题

+
+
+
+ +
+

应用需要管理员权限来:

+
    +
  • 启动/停止 Windows 服务
  • +
  • 修改系统 hosts 文件
  • +
  • 配置系统环境变量
  • +
+
+
+
+ +
+

PHP 从 windows.php.net 官方下载,如果速度较慢:

+
    +
  • 可以手动从官网下载 ZIP 文件
  • +
  • 解压到 [安装目录]/php/php-版本号 目录
  • +
  • 重新打开应用即可识别
  • +
+
+
+
+ +
+

常见原因:

+
    +
  • 3306 端口被占用,检查是否有其他 MySQL 实例
  • +
  • 防火墙阻止,添加防火墙规则
  • +
  • 数据目录权限问题,确保目录可写
  • +
+
+
+
+ +
+ +
+
+
+
+
+ + +
+
+
+

🛠️ 技术栈

+
+ Vue 3 + TypeScript + Electron + Element Plus + Vite + Pinia +
+
+
+
+ + +
+
+
+

准备好提升开发效率了吗?

+

立即下载 PHPer,开启高效的 PHP 开发之旅

+ +
+
+
+ + + + + + + + diff --git a/script.js b/script.js new file mode 100644 index 0000000..3a40cb3 --- /dev/null +++ b/script.js @@ -0,0 +1,183 @@ +/** + * PHPer 官网交互脚本 + */ + +document.addEventListener('DOMContentLoaded', () => { + // 初始化所有功能 + initScreenshotTabs(); + initFAQ(); + initScrollAnimations(); + initMobileMenu(); + initSmoothScroll(); +}); + +/** + * 截图标签切换 + */ +function initScreenshotTabs() { + const tabs = document.querySelectorAll('.tab-btn'); + const images = document.querySelectorAll('.screenshot-img'); + + tabs.forEach(tab => { + tab.addEventListener('click', () => { + const target = tab.dataset.tab; + + // 切换标签状态 + tabs.forEach(t => t.classList.remove('active')); + tab.classList.add('active'); + + // 切换图片 + images.forEach(img => { + if (img.dataset.tab === target) { + img.classList.add('active'); + } else { + img.classList.remove('active'); + } + }); + }); + }); +} + +/** + * FAQ 折叠面板 + */ +function initFAQ() { + const faqItems = document.querySelectorAll('.faq-item'); + + faqItems.forEach(item => { + const question = item.querySelector('.faq-question'); + + question.addEventListener('click', () => { + // 关闭其他展开的项目 + faqItems.forEach(other => { + if (other !== item && other.classList.contains('active')) { + other.classList.remove('active'); + } + }); + + // 切换当前项目 + item.classList.toggle('active'); + }); + }); +} + +/** + * 滚动动画 + */ +function initScrollAnimations() { + const observerOptions = { + root: null, + rootMargin: '0px', + threshold: 0.1 + }; + + const observer = new IntersectionObserver((entries) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + entry.target.classList.add('visible'); + } + }); + }, observerOptions); + + // 观察需要动画的元素 + const animatedElements = document.querySelectorAll( + '.feature-card, .faq-item, .table-row:not(.table-header)' + ); + + animatedElements.forEach((el, index) => { + el.style.transitionDelay = `${index * 0.05}s`; + observer.observe(el); + }); +} + +/** + * 移动端菜单 + */ +function initMobileMenu() { + const menuBtn = document.querySelector('.mobile-menu-btn'); + const navLinks = document.querySelector('.nav-links'); + + if (!menuBtn || !navLinks) return; + + menuBtn.addEventListener('click', () => { + menuBtn.classList.toggle('active'); + navLinks.classList.toggle('mobile-open'); + }); + + // 点击链接后关闭菜单 + navLinks.querySelectorAll('a').forEach(link => { + link.addEventListener('click', () => { + menuBtn.classList.remove('active'); + navLinks.classList.remove('mobile-open'); + }); + }); +} + +/** + * 平滑滚动 + */ +function initSmoothScroll() { + document.querySelectorAll('a[href^="#"]').forEach(anchor => { + anchor.addEventListener('click', function(e) { + const href = this.getAttribute('href'); + if (href === '#') return; + + e.preventDefault(); + const target = document.querySelector(href); + + if (target) { + target.scrollIntoView({ + behavior: 'smooth', + block: 'start' + }); + } + }); + }); +} + +/** + * 导航栏滚动效果 + */ +let lastScroll = 0; +const navbar = document.querySelector('.navbar'); + +window.addEventListener('scroll', () => { + const currentScroll = window.pageYOffset; + + if (currentScroll > 100) { + navbar.style.boxShadow = '0 4px 20px rgba(0, 0, 0, 0.3)'; + } else { + navbar.style.boxShadow = 'none'; + } + + lastScroll = currentScroll; +}); + +/** + * 打字机效果(终端提示) + */ +function typeWriter(element, text, speed = 50) { + let i = 0; + element.textContent = ''; + + function type() { + if (i < text.length) { + element.textContent += text.charAt(i); + i++; + setTimeout(type, speed); + } + } + + type(); +} + +// 页面加载完成后启动打字机效果 +window.addEventListener('load', () => { + const promptText = document.querySelector('.prompt-text'); + if (promptText) { + setTimeout(() => { + typeWriter(promptText, '所有服务已就绪,开始开发吧!', 80); + }, 1500); + } +}); + diff --git a/style.css b/style.css new file mode 100644 index 0000000..d19927d --- /dev/null +++ b/style.css @@ -0,0 +1,1269 @@ +/* ============================================ + PHPer 官网样式 + 深色开发者主题 - 绿色/青色配色 + ============================================ */ + +:root { + /* 主色调 - 绿色系 (PHP 象征) */ + --color-primary: #10b981; + --color-primary-light: #34d399; + --color-primary-dark: #059669; + + /* 强调色 - 青色 */ + --color-accent: #06b6d4; + --color-accent-light: #22d3ee; + + /* 背景色 - 深色 */ + --color-bg: #0a0f1a; + --color-bg-secondary: #111827; + --color-bg-tertiary: #1f2937; + --color-bg-card: rgba(17, 24, 39, 0.8); + + /* 文字色 */ + --color-text: #f9fafb; + --color-text-secondary: #9ca3af; + --color-text-muted: #6b7280; + + /* 边框 */ + --color-border: rgba(255, 255, 255, 0.1); + --color-border-light: rgba(255, 255, 255, 0.05); + + /* 状态色 */ + --color-success: #10b981; + --color-warning: #f59e0b; + --color-error: #ef4444; + + /* 渐变 */ + --gradient-primary: linear-gradient(135deg, var(--color-primary) 0%, var(--color-accent) 100%); + --gradient-glow: radial-gradient(circle, var(--color-primary) 0%, transparent 70%); + + /* 字体 */ + --font-mono: 'JetBrains Mono', 'Consolas', monospace; + --font-sans: 'Noto Sans SC', -apple-system, BlinkMacSystemFont, sans-serif; + + /* 尺寸 */ + --header-height: 72px; + --container-width: 1200px; + --border-radius: 12px; + --border-radius-lg: 20px; + + /* 动画 */ + --transition-fast: 0.15s ease; + --transition-base: 0.3s ease; + --transition-slow: 0.5s ease; +} + +/* ============================================ + 基础样式 + ============================================ */ + +*, *::before, *::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html { + scroll-behavior: smooth; + scroll-padding-top: var(--header-height); +} + +body { + font-family: var(--font-sans); + font-size: 16px; + line-height: 1.6; + color: var(--color-text); + background-color: var(--color-bg); + overflow-x: hidden; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + color: var(--color-primary-light); + text-decoration: none; + transition: color var(--transition-fast); +} + +a:hover { + color: var(--color-primary); +} + +img { + max-width: 100%; + height: auto; +} + +code { + font-family: var(--font-mono); + background: var(--color-bg-tertiary); + padding: 0.2em 0.4em; + border-radius: 4px; + font-size: 0.9em; +} + +/* ============================================ + 背景装饰 + ============================================ */ + +.bg-decoration { + position: fixed; + inset: 0; + z-index: -1; + overflow: hidden; + pointer-events: none; +} + +.grid-lines { + position: absolute; + inset: 0; + background-image: + linear-gradient(rgba(255,255,255,0.02) 1px, transparent 1px), + linear-gradient(90deg, rgba(255,255,255,0.02) 1px, transparent 1px); + background-size: 60px 60px; + mask-image: radial-gradient(ellipse at center, black 0%, transparent 70%); +} + +.glow { + position: absolute; + border-radius: 50%; + filter: blur(100px); + opacity: 0.3; +} + +.glow-1 { + width: 600px; + height: 600px; + background: var(--color-primary); + top: -200px; + right: -200px; + opacity: 0.15; +} + +.glow-2 { + width: 400px; + height: 400px; + background: var(--color-accent); + bottom: 20%; + left: -100px; + opacity: 0.1; +} + +.glow-3 { + width: 500px; + height: 500px; + background: var(--color-primary-dark); + bottom: -200px; + right: 20%; + opacity: 0.1; +} + +/* ============================================ + 容器 + ============================================ */ + +.container { + width: 100%; + max-width: var(--container-width); + margin: 0 auto; + padding: 0 24px; +} + +.section { + padding: 100px 0; +} + +.section-header { + text-align: center; + margin-bottom: 60px; +} + +.section-tag { + display: inline-block; + font-family: var(--font-mono); + font-size: 14px; + font-weight: 500; + color: var(--color-primary); + background: rgba(16, 185, 129, 0.1); + border: 1px solid rgba(16, 185, 129, 0.3); + padding: 6px 16px; + border-radius: 20px; + margin-bottom: 20px; +} + +.section-title { + font-size: clamp(32px, 5vw, 48px); + font-weight: 700; + line-height: 1.2; + margin-bottom: 16px; +} + +.section-desc { + font-size: 18px; + color: var(--color-text-secondary); + max-width: 600px; + margin: 0 auto; +} + +.gradient-text { + background: var(--gradient-primary); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +/* ============================================ + 导航栏 + ============================================ */ + +.navbar { + position: fixed; + top: 0; + left: 0; + right: 0; + height: var(--header-height); + z-index: 1000; + background: rgba(10, 15, 26, 0.8); + backdrop-filter: blur(20px); + border-bottom: 1px solid var(--color-border); +} + +.navbar .container { + display: flex; + align-items: center; + justify-content: space-between; + height: 100%; +} + +.logo { + display: flex; + align-items: center; + gap: 10px; + font-size: 24px; + font-weight: 700; + color: var(--color-text); +} + +.logo:hover { + color: var(--color-text); +} + +.logo-icon { + font-size: 28px; +} + +.logo-text { + font-family: var(--font-mono); +} + +.nav-links { + display: flex; + align-items: center; + gap: 32px; +} + +.nav-links a { + font-size: 15px; + font-weight: 500; + color: var(--color-text-secondary); + transition: color var(--transition-fast); +} + +.nav-links a:hover { + color: var(--color-text); +} + +.mobile-menu-btn { + display: none; + flex-direction: column; + gap: 5px; + padding: 8px; + background: none; + border: none; + cursor: pointer; +} + +.mobile-menu-btn span { + display: block; + width: 24px; + height: 2px; + background: var(--color-text); + transition: var(--transition-fast); +} + +/* 移动端导航菜单 */ +@media (max-width: 768px) { + .nav-links { + position: fixed; + top: var(--header-height); + left: 0; + right: 0; + background: var(--color-bg-secondary); + flex-direction: column; + padding: 24px; + gap: 16px; + border-bottom: 1px solid var(--color-border); + transform: translateY(-100%); + opacity: 0; + pointer-events: none; + transition: all var(--transition-base); + } + + .nav-links.mobile-open { + display: flex; + transform: translateY(0); + opacity: 1; + pointer-events: all; + } + + .mobile-menu-btn.active span:nth-child(1) { + transform: rotate(45deg) translate(5px, 5px); + } + + .mobile-menu-btn.active span:nth-child(2) { + opacity: 0; + } + + .mobile-menu-btn.active span:nth-child(3) { + transform: rotate(-45deg) translate(5px, -5px); + } +} + +/* ============================================ + 按钮 + ============================================ */ + +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + font-family: var(--font-sans); + font-size: 15px; + font-weight: 600; + padding: 12px 24px; + border-radius: 10px; + border: none; + cursor: pointer; + transition: all var(--transition-fast); + text-decoration: none; +} + +.btn .icon { + width: 18px; + height: 18px; +} + +.btn-sm { + padding: 8px 16px; + font-size: 14px; +} + +.btn-lg { + padding: 16px 32px; + font-size: 16px; +} + +.btn-primary { + background: var(--gradient-primary); + color: white; + box-shadow: 0 4px 20px rgba(16, 185, 129, 0.3); +} + +.btn-primary:hover { + transform: translateY(-2px); + box-shadow: 0 6px 30px rgba(16, 185, 129, 0.4); + color: white; +} + +.btn-outline { + background: transparent; + color: var(--color-text); + border: 1px solid var(--color-border); +} + +.btn-outline:hover { + background: var(--color-bg-tertiary); + border-color: var(--color-primary); + color: var(--color-text); +} + +.btn-ghost { + background: rgba(255, 255, 255, 0.05); + color: var(--color-text); + border: 1px solid var(--color-border); +} + +.btn-ghost:hover { + background: rgba(255, 255, 255, 0.1); + color: var(--color-text); +} + +/* ============================================ + Hero 区域 + ============================================ */ + +.hero { + min-height: 100vh; + display: flex; + align-items: center; + padding-top: var(--header-height); + position: relative; +} + +.hero .container { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 60px; + align-items: center; +} + +.hero-badge { + display: inline-flex; + align-items: center; + gap: 8px; + font-family: var(--font-mono); + font-size: 14px; + color: var(--color-primary); + background: rgba(16, 185, 129, 0.1); + border: 1px solid rgba(16, 185, 129, 0.2); + padding: 8px 16px; + border-radius: 24px; + margin-bottom: 24px; +} + +.badge-dot { + width: 8px; + height: 8px; + background: var(--color-primary); + border-radius: 50%; + animation: pulse 2s infinite; +} + +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} + +.hero-title { + font-size: clamp(40px, 6vw, 64px); + font-weight: 700; + line-height: 1.1; + margin-bottom: 24px; +} + +.title-line { + display: block; +} + +.hero-description { + font-size: 18px; + color: var(--color-text-secondary); + margin-bottom: 32px; + line-height: 1.8; +} + +.hero-description .highlight { + color: var(--color-primary-light); + font-weight: 500; +} + +.hero-actions { + display: flex; + gap: 16px; + margin-bottom: 48px; +} + +.hero-stats { + display: flex; + align-items: center; + gap: 32px; +} + +.stat { + display: flex; + flex-direction: column; +} + +.stat-value { + font-family: var(--font-mono); + font-size: 28px; + font-weight: 700; + color: var(--color-text); +} + +.stat-label { + font-size: 14px; + color: var(--color-text-muted); +} + +.stat-divider { + width: 1px; + height: 40px; + background: var(--color-border); +} + +/* Hero Visual - Terminal */ +.hero-visual { + position: relative; +} + +.terminal-window { + background: var(--color-bg-secondary); + border-radius: var(--border-radius-lg); + border: 1px solid var(--color-border); + overflow: hidden; + box-shadow: + 0 25px 50px -12px rgba(0, 0, 0, 0.5), + 0 0 0 1px rgba(255, 255, 255, 0.05); +} + +.terminal-header { + display: flex; + align-items: center; + gap: 12px; + padding: 16px 20px; + background: var(--color-bg-tertiary); + border-bottom: 1px solid var(--color-border); +} + +.terminal-buttons { + display: flex; + gap: 8px; +} + +.terminal-buttons span { + width: 12px; + height: 12px; + border-radius: 50%; +} + +.btn-close { background: #ef4444; } +.btn-minimize { background: #f59e0b; } +.btn-maximize { background: #10b981; } + +.terminal-title { + flex: 1; + text-align: center; + font-family: var(--font-mono); + font-size: 13px; + color: var(--color-text-muted); +} + +.terminal-body { + padding: 20px; + font-family: var(--font-mono); + font-size: 14px; +} + +.service-row { + display: grid; + grid-template-columns: 32px 1fr auto auto; + gap: 12px; + align-items: center; + padding: 12px 0; + border-bottom: 1px solid var(--color-border-light); +} + +.service-row:last-of-type { + border-bottom: none; +} + +.service-icon { + font-size: 20px; +} + +.service-name { + color: var(--color-text); +} + +.service-status { + font-size: 12px; + padding: 4px 12px; + border-radius: 12px; +} + +.service-status.running { + color: var(--color-success); + background: rgba(16, 185, 129, 0.1); +} + +.service-status.stopped { + color: var(--color-text-muted); + background: rgba(107, 114, 128, 0.1); +} + +.service-port { + color: var(--color-text-muted); + font-size: 13px; +} + +.terminal-prompt { + display: flex; + align-items: center; + gap: 8px; + margin-top: 16px; + padding-top: 16px; + border-top: 1px solid var(--color-border-light); +} + +.prompt-symbol { + color: var(--color-primary); +} + +.prompt-text { + color: var(--color-text-secondary); +} + +.cursor { + width: 8px; + height: 18px; + background: var(--color-primary); + animation: blink 1s infinite; +} + +@keyframes blink { + 0%, 50% { opacity: 1; } + 51%, 100% { opacity: 0; } +} + +/* ============================================ + 功能特性 + ============================================ */ + +.features { + background: var(--color-bg-secondary); +} + +.features-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 24px; +} + +.feature-card { + background: var(--color-bg-card); + border: 1px solid var(--color-border); + border-radius: var(--border-radius); + padding: 32px; + transition: all var(--transition-base); +} + +.feature-card:hover { + border-color: var(--color-primary); + transform: translateY(-4px); + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3); +} + +.feature-card.feature-highlight { + grid-column: span 1; + grid-row: span 2; + background: linear-gradient(135deg, rgba(16, 185, 129, 0.1) 0%, rgba(6, 182, 212, 0.05) 100%); + border-color: rgba(16, 185, 129, 0.3); +} + +.feature-icon { + font-size: 40px; + margin-bottom: 20px; +} + +.feature-card h3 { + font-size: 20px; + font-weight: 600; + margin-bottom: 12px; + color: var(--color-text); +} + +.feature-card p { + color: var(--color-text-secondary); + font-size: 15px; + line-height: 1.6; +} + +.feature-list { + list-style: none; + margin-top: 20px; +} + +.feature-list li { + position: relative; + padding-left: 20px; + margin-bottom: 10px; + color: var(--color-text-secondary); + font-size: 14px; +} + +.feature-list li::before { + content: '✓'; + position: absolute; + left: 0; + color: var(--color-primary); +} + +/* ============================================ + 截图展示 + ============================================ */ + +.screenshots { + background: var(--color-bg); +} + +.screenshot-tabs { + display: flex; + justify-content: center; + flex-wrap: wrap; + gap: 12px; + margin-bottom: 40px; +} + +.tab-btn { + font-family: var(--font-sans); + font-size: 14px; + font-weight: 500; + color: var(--color-text-secondary); + background: var(--color-bg-secondary); + border: 1px solid var(--color-border); + padding: 10px 20px; + border-radius: 8px; + cursor: pointer; + transition: all var(--transition-fast); +} + +.tab-btn:hover { + color: var(--color-text); + border-color: var(--color-primary); +} + +.tab-btn.active { + color: var(--color-primary); + background: rgba(16, 185, 129, 0.1); + border-color: var(--color-primary); +} + +.screenshot-container { + display: flex; + justify-content: center; +} + +.screenshot-frame { + width: 100%; + max-width: 1000px; + background: var(--color-bg-secondary); + border-radius: var(--border-radius-lg); + border: 1px solid var(--color-border); + overflow: hidden; + box-shadow: 0 25px 50px rgba(0, 0, 0, 0.4); +} + +.screenshot-window-bar { + display: flex; + align-items: center; + gap: 12px; + padding: 14px 20px; + background: var(--color-bg-tertiary); + border-bottom: 1px solid var(--color-border); +} + +.window-buttons { + display: flex; + gap: 8px; +} + +.window-buttons span { + width: 12px; + height: 12px; + border-radius: 50%; + background: var(--color-text-muted); +} + +.window-buttons span:nth-child(1) { background: #ef4444; } +.window-buttons span:nth-child(2) { background: #f59e0b; } +.window-buttons span:nth-child(3) { background: #10b981; } + +.window-title { + flex: 1; + text-align: center; + font-family: var(--font-mono); + font-size: 13px; + color: var(--color-text-muted); +} + +.screenshot-wrapper { + position: relative; + aspect-ratio: 16/10; + overflow: hidden; +} + +.screenshot-img { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + object-fit: cover; + opacity: 0; + transition: opacity var(--transition-base); +} + +.screenshot-img.active { + opacity: 1; +} + +/* ============================================ + 更多功能 + ============================================ */ + +.more-features { + background: var(--color-bg-secondary); + padding: 60px 0; +} + +.features-banner { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 24px; +} + +.banner-content { + text-align: center; + padding: 24px; +} + +.banner-content h3 { + font-size: 18px; + margin-bottom: 12px; + color: var(--color-text); +} + +.banner-content p { + font-size: 14px; + color: var(--color-text-secondary); + line-height: 1.6; +} + +/* ============================================ + 服务表格 + ============================================ */ + +.services { + background: var(--color-bg); +} + +.services-table { + max-width: 800px; + margin: 0 auto; + background: var(--color-bg-secondary); + border-radius: var(--border-radius); + border: 1px solid var(--color-border); + overflow: hidden; +} + +.table-row { + display: grid; + grid-template-columns: 180px 1fr 1fr; + gap: 20px; + padding: 16px 24px; + border-bottom: 1px solid var(--color-border-light); + align-items: center; +} + +.table-row:last-child { + border-bottom: none; +} + +.table-row.table-header { + background: var(--color-bg-tertiary); + font-weight: 600; + font-size: 14px; + color: var(--color-text-muted); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.table-row .service-name { + display: flex; + align-items: center; + gap: 10px; + font-weight: 500; +} + +.table-row .emoji { + font-size: 20px; +} + +.table-row .source a { + color: var(--color-primary-light); +} + +.table-row .desc { + color: var(--color-text-muted); + font-size: 14px; +} + +/* ============================================ + FAQ + ============================================ */ + +.faq { + background: var(--color-bg-secondary); +} + +.faq-list { + max-width: 800px; + margin: 0 auto; +} + +.faq-item { + background: var(--color-bg-card); + border: 1px solid var(--color-border); + border-radius: var(--border-radius); + margin-bottom: 16px; + overflow: hidden; +} + +.faq-question { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + padding: 20px 24px; + background: none; + border: none; + font-family: var(--font-sans); + font-size: 16px; + font-weight: 500; + color: var(--color-text); + text-align: left; + cursor: pointer; + transition: var(--transition-fast); +} + +.faq-question:hover { + color: var(--color-primary); +} + +.faq-icon { + width: 20px; + height: 20px; + color: var(--color-text-muted); + transition: transform var(--transition-fast); +} + +.faq-item.active .faq-icon { + transform: rotate(180deg); +} + +.faq-answer { + display: none; + padding: 0 24px 20px; + color: var(--color-text-secondary); + font-size: 15px; + line-height: 1.7; +} + +.faq-item.active .faq-answer { + display: block; +} + +.faq-answer ul { + margin-top: 12px; + padding-left: 20px; +} + +.faq-answer li { + margin-bottom: 8px; +} + +/* ============================================ + 技术栈 + ============================================ */ + +.tech-stack { + background: var(--color-bg); + padding: 60px 0; +} + +.tech-content { + text-align: center; +} + +.tech-content h3 { + font-size: 20px; + margin-bottom: 24px; + color: var(--color-text); +} + +.tech-tags { + display: flex; + justify-content: center; + flex-wrap: wrap; + gap: 12px; +} + +.tech-tag { + font-family: var(--font-mono); + font-size: 14px; + color: var(--color-text-secondary); + background: var(--color-bg-secondary); + border: 1px solid var(--color-border); + padding: 8px 16px; + border-radius: 6px; +} + +/* ============================================ + CTA 区域 + ============================================ */ + +.cta { + background: linear-gradient(135deg, rgba(16, 185, 129, 0.1) 0%, rgba(6, 182, 212, 0.05) 100%); + border-top: 1px solid rgba(16, 185, 129, 0.2); + border-bottom: 1px solid rgba(16, 185, 129, 0.2); +} + +.cta-content { + text-align: center; +} + +.cta-content h2 { + font-size: clamp(28px, 4vw, 40px); + font-weight: 700; + margin-bottom: 16px; +} + +.cta-content p { + font-size: 18px; + color: var(--color-text-secondary); + margin-bottom: 32px; +} + +.cta-actions { + display: flex; + justify-content: center; + gap: 16px; +} + +/* ============================================ + 页脚 + ============================================ */ + +.footer { + background: var(--color-bg); + padding: 60px 0 40px; + border-top: 1px solid var(--color-border); +} + +.footer-content { + text-align: center; +} + +.footer-brand { + display: inline-flex; + align-items: center; + gap: 10px; + font-size: 24px; + font-weight: 700; + margin-bottom: 16px; +} + +.footer-text { + color: var(--color-text-secondary); + margin-bottom: 24px; +} + +.footer-links { + display: flex; + justify-content: center; + gap: 32px; + margin-bottom: 24px; +} + +.footer-links a { + color: var(--color-text-muted); + font-size: 14px; +} + +.footer-links a:hover { + color: var(--color-primary); +} + +.copyright { + font-size: 14px; + color: var(--color-text-muted); +} + +/* ============================================ + 响应式设计 + ============================================ */ + +@media (max-width: 1024px) { + .hero .container { + grid-template-columns: 1fr; + text-align: center; + } + + .hero-content { + order: 1; + } + + .hero-visual { + order: 2; + max-width: 600px; + margin: 0 auto; + } + + .hero-actions { + justify-content: center; + } + + .hero-stats { + justify-content: center; + } + + .features-grid { + grid-template-columns: repeat(2, 1fr); + } + + .feature-card.feature-highlight { + grid-column: span 2; + grid-row: span 1; + } + + .features-banner { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (max-width: 768px) { + .nav-links { + display: none; + } + + .mobile-menu-btn { + display: flex; + } + + .section { + padding: 60px 0; + } + + .hero { + min-height: auto; + padding: 120px 0 60px; + } + + .hero-description br { + display: none; + } + + .hero-actions { + flex-direction: column; + } + + .hero-stats { + flex-wrap: wrap; + gap: 24px; + } + + .stat-divider { + display: none; + } + + .features-grid { + grid-template-columns: 1fr; + } + + .feature-card.feature-highlight { + grid-column: span 1; + } + + .screenshot-tabs { + gap: 8px; + } + + .tab-btn { + padding: 8px 12px; + font-size: 12px; + } + + .features-banner { + grid-template-columns: 1fr; + } + + .table-row { + grid-template-columns: 1fr; + gap: 8px; + } + + .table-row.table-header { + display: none; + } + + .cta-actions { + flex-direction: column; + align-items: center; + } + + .footer-links { + flex-direction: column; + gap: 16px; + } +} + +/* ============================================ + 动画 + ============================================ */ + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.hero-badge, +.hero-title, +.hero-description, +.hero-actions, +.hero-stats { + animation: fadeInUp 0.6s ease-out backwards; +} + +.hero-badge { animation-delay: 0.1s; } +.hero-title { animation-delay: 0.2s; } +.hero-description { animation-delay: 0.3s; } +.hero-actions { animation-delay: 0.4s; } +.hero-stats { animation-delay: 0.5s; } + +.terminal-window { + animation: fadeInUp 0.8s ease-out 0.3s backwards; +} + +.service-row { + animation: fadeInUp 0.4s ease-out backwards; +} + +.service-row:nth-child(1) { animation-delay: 0.5s; } +.service-row:nth-child(2) { animation-delay: 0.6s; } +.service-row:nth-child(3) { animation-delay: 0.7s; } +.service-row:nth-child(4) { animation-delay: 0.8s; } +.service-row:nth-child(5) { animation-delay: 0.9s; } + +/* 滚动动画 */ +.feature-card, +.faq-item, +.table-row:not(.table-header) { + opacity: 0; + transform: translateY(20px); + transition: opacity 0.5s ease, transform 0.5s ease; +} + +.feature-card.visible, +.faq-item.visible, +.table-row.visible { + opacity: 1; + transform: translateY(0); +} +