Compare commits
2 Commits
79bfeb0e18
...
8b9229257c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b9229257c | ||
|
314e53bf9c
|
206
README.md
206
README.md
@@ -1,2 +1,206 @@
|
||||
# PerToolBoxFront
|
||||
# PerToolBox Front - 前端界面
|
||||
|
||||
> 一个基于 PHP + HTML/CSS/JS 的个人工具箱前端项目,采用响应式侧边栏设计,适配多端设备。
|
||||
|
||||
---
|
||||
|
||||
## 项目信息
|
||||
|
||||
- 版权所有:Sea Network Technology Studio
|
||||
- 权利人:Canglan
|
||||
- 联系方式:admin@sea-studio.top
|
||||
- 开源协议:AGPL v3
|
||||
|
||||
---
|
||||
|
||||
## 功能特性
|
||||
|
||||
- 响应式侧边栏(支持手机 / PC)
|
||||
- 用户认证(登录 / 注册 / 验证码)
|
||||
- 待办事项管理(CRUD)
|
||||
- 便签本(CRUD)
|
||||
- 密码生成器
|
||||
- 二维码生成
|
||||
- 加密工具箱:
|
||||
- Hash
|
||||
- Base64
|
||||
- URL 编码
|
||||
- AES 加密
|
||||
- JSON 校验与格式化
|
||||
- 热度统计展示
|
||||
|
||||
---
|
||||
|
||||
## 环境要求
|
||||
|
||||
- PHP >= 8.0
|
||||
- Nginx 或 Apache
|
||||
|
||||
---
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 克隆项目
|
||||
|
||||
```bash
|
||||
git clone https://hz-gitea.sea-studio.top/Sea-Studio/PerToolBoxFront.git
|
||||
cd PerToolBoxFront
|
||||
```
|
||||
|
||||
### 2. 配置后端接口
|
||||
|
||||
编辑 config.php:
|
||||
|
||||
```php
|
||||
define('API_BASE_URL', 'https://your-domain/api/v1');
|
||||
```
|
||||
|
||||
### 3. 配置 Nginx
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name your-domain;
|
||||
|
||||
root /path/to/PerToolBoxFront;
|
||||
index index.php;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.php;
|
||||
}
|
||||
|
||||
location ~ \.php$ {
|
||||
fastcgi_pass unix:/var/run/php/php8.0-fpm.sock;
|
||||
fastcgi_index index.php;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
include fastcgi_params;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 访问项目
|
||||
|
||||
```
|
||||
http://your-domain
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 项目结构
|
||||
|
||||
```text
|
||||
PerToolBoxFront/
|
||||
├── index.php # 首页
|
||||
├── login.php # 登录/注册
|
||||
├── profile.php # 个人中心
|
||||
├── config.php # 配置文件
|
||||
├── header.php # 公共头部
|
||||
├── footer.php # 公共底部
|
||||
├── sidebar.php # 侧边栏
|
||||
├── css/
|
||||
│ └── style.css # 样式文件
|
||||
├── js/
|
||||
│ └── common.js # 公共脚本
|
||||
└── pages/ # 功能页面
|
||||
├── todos.php # 待办事项
|
||||
├── notes.php # 便签
|
||||
├── password.php # 密码生成
|
||||
├── qrcode.php # 二维码
|
||||
├── crypto.php # 加密工具
|
||||
└── json.php # JSON 工具
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 后端 API 要求
|
||||
|
||||
后端服务基础路径:
|
||||
|
||||
```
|
||||
https://your-domain/api/v1
|
||||
```
|
||||
|
||||
实现以下接口:
|
||||
|
||||
### 认证模块
|
||||
|
||||
- 用户注册
|
||||
- 用户登录
|
||||
- 验证码发送
|
||||
|
||||
### 数据模块
|
||||
|
||||
- 待办事项 CRUD
|
||||
- 便签 CRUD
|
||||
|
||||
### 工具模块
|
||||
|
||||
- 密码生成
|
||||
- 二维码生成
|
||||
- 加密 / 解密
|
||||
- JSON 处理
|
||||
|
||||
### 统计模块
|
||||
|
||||
- 热度统计接口
|
||||
|
||||
---
|
||||
|
||||
## 部署指南
|
||||
|
||||
### 后端部署
|
||||
|
||||
```bash
|
||||
# 1. 安装依赖
|
||||
sudo dnf install -y python python-pip mysql redis nginx git # CentOS/Rocky
|
||||
sudo apt install -y python python-pip mysql redis nginx git # Ubuntu/Debian
|
||||
|
||||
# 2. 克隆项目
|
||||
git clone https://hz-gitea.sea-studio.top/Sea-Studio/PerToolBoxServer.git
|
||||
cd PerToolBoxServer
|
||||
|
||||
# 3. 创建虚拟环境
|
||||
python -m venv venv
|
||||
source venv/bin/activate
|
||||
|
||||
# 4. 安装依赖
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 5. 配置环境变量
|
||||
cp .env.example .env
|
||||
# 编辑 .env 文件(数据库 / Redis / 短信服务)
|
||||
|
||||
# 6. 初始化数据库
|
||||
mysql -u root -p < scripts/init_db.sql
|
||||
|
||||
# 7. 启动服务
|
||||
uvicorn backend.main:app --host 0.0.0.0 --port 8000
|
||||
```
|
||||
|
||||
### 前端部署(Linux + Nginx + PHP)
|
||||
|
||||
```bash
|
||||
# 1. 安装依赖
|
||||
sudo dnf install -y php php-fpm nginx # CentOS/Rocky
|
||||
sudo apt install -y php php-fpm nginx # Ubuntu/Debian
|
||||
|
||||
# 2. 克隆项目
|
||||
git clone https://hz-gitea.sea-studio.top/Sea-Studio/PerToolBoxFront.git
|
||||
cd PerToolBoxFront
|
||||
|
||||
# 3. 配置 API 地址
|
||||
# 编辑 config.php
|
||||
|
||||
# 4. 配置 Nginx
|
||||
# 参考上方配置
|
||||
|
||||
# 5. 启动服务
|
||||
sudo systemctl start php-fpm
|
||||
sudo systemctl start nginx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 许可证
|
||||
|
||||
本项目基于 AGPL v3 协议开源。
|
||||
30
config.php
Normal file
30
config.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
/**
|
||||
* PerToolBox Front - 配置文件
|
||||
*
|
||||
* Copyright (C) 2024 Sea Network Technology Studio
|
||||
* Author: Canglan <admin@sea-studio.top>
|
||||
* License: AGPL v3
|
||||
*/
|
||||
|
||||
// API 基础地址(后端服务地址)
|
||||
define('API_BASE_URL', getenv('API_BASE_URL') ?: 'http://your-domain/api/v1');
|
||||
|
||||
// 网站名称
|
||||
define('SITE_NAME', 'PerToolBox');
|
||||
|
||||
// 调试模式
|
||||
define('DEBUG', getenv('DEBUG') === 'true');
|
||||
|
||||
// 错误报告
|
||||
if (DEBUG) {
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
} else {
|
||||
error_reporting(0);
|
||||
ini_set('display_errors', 0);
|
||||
}
|
||||
|
||||
// 时区设置
|
||||
date_default_timezone_set('Asia/Shanghai');
|
||||
?>
|
||||
259
css/style.css
Normal file
259
css/style.css
Normal file
@@ -0,0 +1,259 @@
|
||||
/**
|
||||
* PerToolBox Front - 响应式侧边栏样式
|
||||
* Copyright (C) 2024 Sea Network Technology Studio
|
||||
* Author: Canglan <admin@sea-studio.top>
|
||||
* License: AGPL v3
|
||||
*/
|
||||
|
||||
/* 侧边栏基础样式 */
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 280px;
|
||||
height: 100vh;
|
||||
background: linear-gradient(180deg, #1e3a8a 0%, #1e40af 100%);
|
||||
color: white;
|
||||
z-index: 40;
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.3s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* 桌面端显示 */
|
||||
@media (min-width: 768px) {
|
||||
.sidebar {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
body {
|
||||
margin-left: 280px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 移动端打开状态 */
|
||||
.sidebar.open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
/* 侧边栏头部 */
|
||||
.sidebar-header {
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
/* 导航菜单 */
|
||||
.sidebar-nav {
|
||||
flex: 1;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.sidebar-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem 1.5rem;
|
||||
color: #e2e8f0;
|
||||
transition: all 0.2s;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.sidebar-link:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 侧边栏底部 */
|
||||
.sidebar-footer {
|
||||
padding: 1rem 0;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
/* 遮罩层 */
|
||||
.overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 35;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.overlay.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.overlay {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 主要内容区域容器 */
|
||||
.main-content {
|
||||
padding: 1.5rem;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* 卡片样式 */
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
/* 工具卡片网格 */
|
||||
.tool-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.tool-card {
|
||||
background: white;
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.5rem;
|
||||
text-align: center;
|
||||
transition: all 0.2s;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.tool-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.tool-icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.tool-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.tool-desc {
|
||||
font-size: 0.875rem;
|
||||
color: #6b7280;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.tool-stats {
|
||||
font-size: 0.75rem;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
/* 表单样式 */
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 0.5rem;
|
||||
outline: none;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
border-color: #3b82f6;
|
||||
ring: 2px solid #3b82f6;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.25rem;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #2563eb;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6b7280;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #4b5563;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #ef4444;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #dc2626;
|
||||
}
|
||||
|
||||
/* 加载动画 */
|
||||
.loading {
|
||||
display: inline-block;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border: 2px solid #e5e7eb;
|
||||
border-top-color: #3b82f6;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.6s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 768px) {
|
||||
.main-content {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.tool-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* 滚动条 */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #a8a8a8;
|
||||
}
|
||||
25
footer.php
Normal file
25
footer.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
/**
|
||||
* PerToolBox Front - 公共底部
|
||||
*
|
||||
* Copyright (C) 2024 Sea Network Technology Studio
|
||||
* Author: Canglan <admin@sea-studio.top>
|
||||
* License: AGPL v3
|
||||
*/
|
||||
?>
|
||||
<!-- 公共 JS -->
|
||||
<script src="/js/common.js"></script>
|
||||
<footer class="bg-white mt-12 py-6 border-t">
|
||||
<div class="container mx-auto px-4 text-center text-gray-500 text-sm">
|
||||
<p>© 2024 Sea Network Technology Studio | Powered by Canglan</p>
|
||||
<p class="mt-1">
|
||||
开源协议: AGPL v3 |
|
||||
<a href="https://hz-gitea.sea-studio.top/yourname/PerToolBoxFront"
|
||||
class="text-blue-500 hover:underline">前端仓库</a> |
|
||||
<a href="https://hz-gitea.sea-studio.top/yourname/PerToolBoxServer"
|
||||
class="text-blue-500 hover:underline">后端仓库</a>
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
28
header.php
Normal file
28
header.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
/**
|
||||
* PerToolBox Front - 公共头部
|
||||
*
|
||||
* Copyright (C) 2024 Sea Network Technology Studio
|
||||
* Author: Canglan <admin@sea-studio.top>
|
||||
* License: AGPL v3
|
||||
*/
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
|
||||
<meta name="copyright" content="Sea Network Technology Studio">
|
||||
<meta name="author" content="Canglan">
|
||||
<title><?php echo SITE_NAME; ?> - 个人工具箱</title>
|
||||
<!-- TailwindCSS CDN -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<!-- 自定义样式 -->
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<!-- 配置全局变量 -->
|
||||
<script>
|
||||
window.API_BASE = '<?php echo API_BASE_URL; ?>';
|
||||
window.SITE_NAME = '<?php echo SITE_NAME; ?>';
|
||||
</script>
|
||||
</head>
|
||||
<body class="bg-gray-100">
|
||||
94
index.php
Normal file
94
index.php
Normal file
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
/**
|
||||
* PerToolBox Front - 首页
|
||||
*
|
||||
* Copyright (C) 2024 Sea Network Technology Studio
|
||||
* Author: Canglan <admin@sea-studio.top>
|
||||
* License: AGPL v3
|
||||
*/
|
||||
|
||||
require_once 'config.php';
|
||||
include_once 'header.php';
|
||||
include_once 'sidebar.php';
|
||||
?>
|
||||
|
||||
<div class="main-content">
|
||||
<div class="text-center mb-8">
|
||||
<h1 class="text-4xl font-bold text-gray-800 mb-2"><?php echo SITE_NAME; ?></h1>
|
||||
<p class="text-gray-600">实用工具集 · 即开即用</p>
|
||||
</div>
|
||||
|
||||
<!-- 工具网格 -->
|
||||
<div class="tool-grid" id="toolGrid">
|
||||
<a href="/pages/todos.php" class="tool-card" data-tool="todos">
|
||||
<div class="tool-icon">✅</div>
|
||||
<div class="tool-title">待办事项</div>
|
||||
<div class="tool-desc">管理任务,提高效率</div>
|
||||
<div class="tool-stats" id="stats-todos">加载中...</div>
|
||||
</a>
|
||||
|
||||
<a href="/pages/notes.php" class="tool-card" data-tool="notes">
|
||||
<div class="tool-icon">📝</div>
|
||||
<div class="tool-title">便签本</div>
|
||||
<div class="tool-desc">记录灵感和想法</div>
|
||||
<div class="tool-stats" id="stats-notes">加载中...</div>
|
||||
</a>
|
||||
|
||||
<a href="/pages/password.php" class="tool-card" data-tool="password">
|
||||
<div class="tool-icon">🔑</div>
|
||||
<div class="tool-title">密码生成器</div>
|
||||
<div class="tool-desc">生成强密码</div>
|
||||
<div class="tool-stats" id="stats-password">加载中...</div>
|
||||
</a>
|
||||
|
||||
<a href="/pages/qrcode.php" class="tool-card" data-tool="qrcode">
|
||||
<div class="tool-icon">📱</div>
|
||||
<div class="tool-title">二维码生成</div>
|
||||
<div class="tool-desc">快速分享链接</div>
|
||||
<div class="tool-stats" id="stats-qrcode">加载中...</div>
|
||||
</a>
|
||||
|
||||
<a href="/pages/crypto.php" class="tool-card" data-tool="crypto_hash">
|
||||
<div class="tool-icon">🔒</div>
|
||||
<div class="tool-title">加密工具</div>
|
||||
<div class="tool-desc">哈希、Base64、AES等</div>
|
||||
<div class="tool-stats" id="stats-crypto_hash">加载中...</div>
|
||||
</a>
|
||||
|
||||
<a href="/pages/json.php" class="tool-card" data-tool="json">
|
||||
<div class="tool-icon">📋</div>
|
||||
<div class="tool-title">JSON校验</div>
|
||||
<div class="tool-desc">验证并格式化JSON</div>
|
||||
<div class="tool-stats" id="stats-json">加载中...</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 加载热度统计
|
||||
async function loadStats() {
|
||||
try {
|
||||
const stats = await apiRequest('/tool/stats');
|
||||
|
||||
// 更新每个工具的统计显示
|
||||
for (const [toolName, data] of Object.entries(stats)) {
|
||||
const el = document.getElementById(`stats-${toolName}`);
|
||||
if (el) {
|
||||
el.innerHTML = `今日访问 ${data.today} | 总访问 ${data.total}`;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载统计失败:', error);
|
||||
document.querySelectorAll('.tool-stats').forEach(el => {
|
||||
el.innerHTML = '加载失败';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载时上报首页访问(可选)
|
||||
// recordUsage('home');
|
||||
|
||||
loadStats();
|
||||
</script>
|
||||
|
||||
<?php include_once 'footer.php'; ?>
|
||||
197
js/common.js
Normal file
197
js/common.js
Normal file
@@ -0,0 +1,197 @@
|
||||
/**
|
||||
* PerToolBox Front - 公共 JavaScript
|
||||
* Copyright (C) 2024 Sea Network Technology Studio
|
||||
* Author: Canglan <admin@sea-studio.top>
|
||||
* License: AGPL v3
|
||||
*/
|
||||
|
||||
// ========== 全局变量 ==========
|
||||
let currentUser = null;
|
||||
|
||||
// ========== 工具函数 ==========
|
||||
function getToken() {
|
||||
return localStorage.getItem('token');
|
||||
}
|
||||
|
||||
function setToken(token) {
|
||||
if (token) {
|
||||
localStorage.setItem('token', token);
|
||||
} else {
|
||||
localStorage.removeItem('token');
|
||||
}
|
||||
}
|
||||
|
||||
function isLoggedIn() {
|
||||
return !!getToken();
|
||||
}
|
||||
|
||||
// ========== API 请求封装 ==========
|
||||
async function apiRequest(endpoint, options = {}) {
|
||||
const url = endpoint.startsWith('http') ? endpoint : `${window.API_BASE}${endpoint}`;
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers
|
||||
};
|
||||
|
||||
const token = getToken();
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers
|
||||
});
|
||||
|
||||
if (response.status === 401) {
|
||||
// token 失效,清除本地存储并跳转登录页
|
||||
setToken(null);
|
||||
if (!window.location.pathname.includes('/login.php')) {
|
||||
window.location.href = '/login.php';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (!response.ok) {
|
||||
throw new Error(data.detail || data.message || '请求失败');
|
||||
}
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('API 请求错误:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 热度上报 ==========
|
||||
async function recordUsage(toolName) {
|
||||
try {
|
||||
await apiRequest(`/tool/usage?tool_name=${toolName}`, { method: 'POST' });
|
||||
} catch (error) {
|
||||
console.warn('热度上报失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 获取用户信息 ==========
|
||||
async function loadUserInfo() {
|
||||
if (!isLoggedIn()) {
|
||||
updateUserUI(null);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await apiRequest('/user/profile');
|
||||
currentUser = user;
|
||||
updateUserUI(user);
|
||||
return user;
|
||||
} catch (error) {
|
||||
console.error('获取用户信息失败:', error);
|
||||
setToken(null);
|
||||
updateUserUI(null);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 更新侧边栏 UI ==========
|
||||
function updateUserUI(user) {
|
||||
const userInfoDiv = document.getElementById('userInfo');
|
||||
const profileLink = document.getElementById('profileLink');
|
||||
const logoutBtn = document.getElementById('logoutBtn');
|
||||
const loginLink = document.getElementById('loginLink');
|
||||
|
||||
if (user) {
|
||||
const displayName = user.username || user.phone || user.email || '用户';
|
||||
userInfoDiv.innerHTML = `
|
||||
<div class="px-6 py-3 bg-blue-800 rounded-lg mx-4 mb-2">
|
||||
<div class="text-sm text-blue-200">欢迎,</div>
|
||||
<div class="font-semibold">${escapeHtml(displayName)}</div>
|
||||
</div>
|
||||
`;
|
||||
if (profileLink) profileLink.style.display = 'flex';
|
||||
if (logoutBtn) logoutBtn.style.display = 'flex';
|
||||
if (loginLink) loginLink.style.display = 'none';
|
||||
} else {
|
||||
userInfoDiv.innerHTML = '';
|
||||
if (profileLink) profileLink.style.display = 'none';
|
||||
if (logoutBtn) logoutBtn.style.display = 'none';
|
||||
if (loginLink) loginLink.style.display = 'flex';
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 退出登录 ==========
|
||||
function logout() {
|
||||
setToken(null);
|
||||
currentUser = null;
|
||||
updateUserUI(null);
|
||||
window.location.href = '/';
|
||||
}
|
||||
|
||||
// ========== 侧边栏控制 ==========
|
||||
function initSidebar() {
|
||||
const menuBtn = document.getElementById('menuBtn');
|
||||
const closeBtn = document.getElementById('closeSidebar');
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const overlay = document.getElementById('overlay');
|
||||
|
||||
function openSidebar() {
|
||||
if (sidebar) sidebar.classList.add('open');
|
||||
if (overlay) overlay.classList.add('active');
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
function closeSidebar() {
|
||||
if (sidebar) sidebar.classList.remove('open');
|
||||
if (overlay) overlay.classList.remove('active');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
if (menuBtn) menuBtn.addEventListener('click', openSidebar);
|
||||
if (closeBtn) closeBtn.addEventListener('click', closeSidebar);
|
||||
if (overlay) overlay.addEventListener('click', closeSidebar);
|
||||
|
||||
// 点击侧边栏内的链接后自动关闭(移动端)
|
||||
if (sidebar) {
|
||||
sidebar.querySelectorAll('a').forEach(link => {
|
||||
link.addEventListener('click', () => {
|
||||
if (window.innerWidth < 768) {
|
||||
closeSidebar();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ========== HTML 转义 ==========
|
||||
function escapeHtml(text) {
|
||||
if (!text) return '';
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// ========== 显示 Toast 消息 ==========
|
||||
function showToast(message, type = 'success') {
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `fixed bottom-4 right-4 z-50 px-4 py-2 rounded-lg shadow-lg text-white ${
|
||||
type === 'success' ? 'bg-green-500' : 'bg-red-500'
|
||||
}`;
|
||||
toast.textContent = message;
|
||||
document.body.appendChild(toast);
|
||||
setTimeout(() => toast.remove(), 3000);
|
||||
}
|
||||
|
||||
// ========== 页面初始化 ==========
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initSidebar();
|
||||
loadUserInfo();
|
||||
|
||||
// 绑定退出按钮
|
||||
const logoutBtn = document.getElementById('logoutBtn');
|
||||
if (logoutBtn) {
|
||||
logoutBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
logout();
|
||||
});
|
||||
}
|
||||
});
|
||||
216
login.php
Normal file
216
login.php
Normal file
@@ -0,0 +1,216 @@
|
||||
<?php
|
||||
/**
|
||||
* PerToolBox Front - 登录/注册页面
|
||||
*
|
||||
* Copyright (C) 2024 Sea Network Technology Studio
|
||||
* Author: Canglan <admin@sea-studio.top>
|
||||
* License: AGPL v3
|
||||
*/
|
||||
|
||||
require_once 'config.php';
|
||||
include_once 'header.php';
|
||||
include_once 'sidebar.php';
|
||||
?>
|
||||
|
||||
<div class="main-content">
|
||||
<div class="max-w-md mx-auto">
|
||||
<div class="card">
|
||||
<div class="flex border-b mb-6">
|
||||
<button id="tabLogin" class="flex-1 py-2 text-center font-semibold text-blue-600 border-b-2 border-blue-600">登录</button>
|
||||
<button id="tabRegister" class="flex-1 py-2 text-center text-gray-500">注册</button>
|
||||
</div>
|
||||
|
||||
<!-- 登录表单 -->
|
||||
<div id="loginForm">
|
||||
<div class="mb-4">
|
||||
<label class="form-label">手机号 / 邮箱</label>
|
||||
<input type="text" id="loginAccount" class="form-input" placeholder="请输入手机号或邮箱">
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="form-label">密码 / 验证码</label>
|
||||
<div class="flex gap-2">
|
||||
<input type="text" id="loginPassword" class="form-input flex-1" placeholder="密码或验证码">
|
||||
<button id="loginCodeBtn" class="btn btn-secondary whitespace-nowrap">获取验证码</button>
|
||||
</div>
|
||||
</div>
|
||||
<button id="loginBtn" class="btn btn-primary w-full">登录</button>
|
||||
</div>
|
||||
|
||||
<!-- 注册表单 -->
|
||||
<div id="registerForm" style="display: none;">
|
||||
<div class="mb-4">
|
||||
<label class="form-label">手机号 / 邮箱</label>
|
||||
<input type="text" id="registerAccount" class="form-input" placeholder="请输入手机号或邮箱">
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="form-label">验证码</label>
|
||||
<div class="flex gap-2">
|
||||
<input type="text" id="registerCode" class="form-input flex-1" placeholder="验证码">
|
||||
<button id="registerCodeBtn" class="btn btn-secondary whitespace-nowrap">获取验证码</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="form-label">密码</label>
|
||||
<input type="password" id="registerPassword" class="form-input" placeholder="6-20位密码">
|
||||
</div>
|
||||
<button id="registerBtn" class="btn btn-primary w-full">注册</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let loginTimer = null;
|
||||
let registerTimer = null;
|
||||
|
||||
// 切换选项卡
|
||||
document.getElementById('tabLogin').addEventListener('click', () => {
|
||||
document.getElementById('loginForm').style.display = 'block';
|
||||
document.getElementById('registerForm').style.display = 'none';
|
||||
document.getElementById('tabLogin').className = 'flex-1 py-2 text-center font-semibold text-blue-600 border-b-2 border-blue-600';
|
||||
document.getElementById('tabRegister').className = 'flex-1 py-2 text-center text-gray-500';
|
||||
});
|
||||
|
||||
document.getElementById('tabRegister').addEventListener('click', () => {
|
||||
document.getElementById('loginForm').style.display = 'none';
|
||||
document.getElementById('registerForm').style.display = 'block';
|
||||
document.getElementById('tabRegister').className = 'flex-1 py-2 text-center font-semibold text-blue-600 border-b-2 border-blue-600';
|
||||
document.getElementById('tabLogin').className = 'flex-1 py-2 text-center text-gray-500';
|
||||
});
|
||||
|
||||
// 获取验证码
|
||||
function startCountdown(btn, timerVar, seconds = 60) {
|
||||
let count = seconds;
|
||||
btn.disabled = true;
|
||||
btn.textContent = `${count}秒后重试`;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
count--;
|
||||
if (count <= 0) {
|
||||
clearInterval(interval);
|
||||
btn.disabled = false;
|
||||
btn.textContent = '获取验证码';
|
||||
} else {
|
||||
btn.textContent = `${count}秒后重试`;
|
||||
}
|
||||
}, 1000);
|
||||
return interval;
|
||||
}
|
||||
|
||||
// 登录获取验证码
|
||||
document.getElementById('loginCodeBtn').addEventListener('click', async () => {
|
||||
const account = document.getElementById('loginAccount').value.trim();
|
||||
if (!account) {
|
||||
showToast('请输入手机号或邮箱', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const type = /^1[3-9]\d{9}$/.test(account) ? 'phone' : 'email';
|
||||
|
||||
try {
|
||||
await apiRequest('/auth/send-code', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ account, type })
|
||||
});
|
||||
showToast('验证码已发送');
|
||||
if (loginTimer) clearInterval(loginTimer);
|
||||
loginTimer = startCountdown(document.getElementById('loginCodeBtn'), loginTimer);
|
||||
} catch (error) {
|
||||
showToast(error.message, 'error');
|
||||
}
|
||||
});
|
||||
|
||||
// 注册获取验证码
|
||||
document.getElementById('registerCodeBtn').addEventListener('click', async () => {
|
||||
const account = document.getElementById('registerAccount').value.trim();
|
||||
if (!account) {
|
||||
showToast('请输入手机号或邮箱', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const type = /^1[3-9]\d{9}$/.test(account) ? 'phone' : 'email';
|
||||
|
||||
try {
|
||||
await apiRequest('/auth/send-code', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ account, type })
|
||||
});
|
||||
showToast('验证码已发送');
|
||||
if (registerTimer) clearInterval(registerTimer);
|
||||
registerTimer = startCountdown(document.getElementById('registerCodeBtn'), registerTimer);
|
||||
} catch (error) {
|
||||
showToast(error.message, 'error');
|
||||
}
|
||||
});
|
||||
|
||||
// 登录
|
||||
document.getElementById('loginBtn').addEventListener('click', async () => {
|
||||
const account = document.getElementById('loginAccount').value.trim();
|
||||
const password = document.getElementById('loginPassword').value.trim();
|
||||
|
||||
if (!account) {
|
||||
showToast('请输入账号', 'error');
|
||||
return;
|
||||
}
|
||||
if (!password) {
|
||||
showToast('请输入密码或验证码', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await apiRequest('/auth/login', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ account, password })
|
||||
});
|
||||
|
||||
if (result.token) {
|
||||
setToken(result.token);
|
||||
showToast('登录成功');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/';
|
||||
}, 1000);
|
||||
}
|
||||
} catch (error) {
|
||||
showToast(error.message, 'error');
|
||||
}
|
||||
});
|
||||
|
||||
// 注册
|
||||
document.getElementById('registerBtn').addEventListener('click', async () => {
|
||||
const account = document.getElementById('registerAccount').value.trim();
|
||||
const code = document.getElementById('registerCode').value.trim();
|
||||
const password = document.getElementById('registerPassword').value.trim();
|
||||
|
||||
if (!account) {
|
||||
showToast('请输入账号', 'error');
|
||||
return;
|
||||
}
|
||||
if (!code) {
|
||||
showToast('请输入验证码', 'error');
|
||||
return;
|
||||
}
|
||||
if (!password || password.length < 6) {
|
||||
showToast('密码长度至少6位', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await apiRequest('/auth/register', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ account, code, password })
|
||||
});
|
||||
|
||||
if (result.token) {
|
||||
setToken(result.token);
|
||||
showToast('注册成功');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/';
|
||||
}, 1000);
|
||||
}
|
||||
} catch (error) {
|
||||
showToast(error.message, 'error');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php include_once 'footer.php'; ?>
|
||||
172
pages/crypto.php
Normal file
172
pages/crypto.php
Normal file
@@ -0,0 +1,172 @@
|
||||
<?php
|
||||
/**
|
||||
* PerToolBox Front - 加密工具箱页面
|
||||
*
|
||||
* Copyright (C) 2024 Sea Network Technology Studio
|
||||
* Author: Canglan <admin@sea-studio.top>
|
||||
* License: AGPL v3
|
||||
*/
|
||||
|
||||
require_once '../config.php';
|
||||
include_once '../header.php';
|
||||
include_once '../sidebar.php';
|
||||
?>
|
||||
|
||||
<div class="main-content">
|
||||
<div class="card">
|
||||
<h1 class="text-2xl font-bold mb-6">🔒 加密工具箱</h1>
|
||||
|
||||
<!-- 哈希计算 -->
|
||||
<div class="mb-8 border-b pb-6">
|
||||
<h2 class="text-xl font-semibold mb-4">哈希计算</h2>
|
||||
<div class="flex gap-2 mb-3">
|
||||
<select id="hashAlgo" class="form-input w-32">
|
||||
<option value="md5">MD5</option>
|
||||
<option value="sha1">SHA1</option>
|
||||
<option value="sha256">SHA256</option>
|
||||
<option value="sha512">SHA512</option>
|
||||
</select>
|
||||
<input type="text" id="hashInput" placeholder="输入文本" class="form-input flex-1">
|
||||
<button id="hashBtn" class="btn btn-primary">计算</button>
|
||||
</div>
|
||||
<div class="bg-gray-50 p-3 rounded-lg">
|
||||
<div class="text-sm text-gray-500">结果:</div>
|
||||
<div id="hashResult" class="font-mono text-sm break-all"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Base64 编解码 -->
|
||||
<div class="mb-8 border-b pb-6">
|
||||
<h2 class="text-xl font-semibold mb-4">Base64 编解码</h2>
|
||||
<textarea id="base64Input" rows="3" placeholder="输入文本" class="form-input mb-3"></textarea>
|
||||
<div class="flex gap-2 mb-3">
|
||||
<button id="base64Encode" class="btn btn-primary">编码</button>
|
||||
<button id="base64Decode" class="btn btn-secondary">解码</button>
|
||||
</div>
|
||||
<div class="bg-gray-50 p-3 rounded-lg">
|
||||
<div class="text-sm text-gray-500">结果:</div>
|
||||
<div id="base64Result" class="font-mono text-sm break-all"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- URL 编解码 -->
|
||||
<div class="mb-8 border-b pb-6">
|
||||
<h2 class="text-xl font-semibold mb-4">URL 编解码</h2>
|
||||
<textarea id="urlInput" rows="3" placeholder="输入文本" class="form-input mb-3"></textarea>
|
||||
<div class="flex gap-2 mb-3">
|
||||
<button id="urlEncode" class="btn btn-primary">编码</button>
|
||||
<button id="urlDecode" class="btn btn-secondary">解码</button>
|
||||
</div>
|
||||
<div class="bg-gray-50 p-3 rounded-lg">
|
||||
<div class="text-sm text-gray-500">结果:</div>
|
||||
<div id="urlResult" class="font-mono text-sm break-all"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- AES 加解密 -->
|
||||
<div class="mb-8">
|
||||
<h2 class="text-xl font-semibold mb-4">AES 加解密</h2>
|
||||
<div class="grid md:grid-cols-2 gap-4 mb-3">
|
||||
<select id="aesMode" class="form-input">
|
||||
<option value="ECB">ECB</option>
|
||||
<option value="CBC">CBC</option>
|
||||
<option value="GCM">GCM</option>
|
||||
</select>
|
||||
<input type="text" id="aesKey" placeholder="密钥 (16/24/32字节)" class="form-input">
|
||||
</div>
|
||||
<input type="text" id="aesIv" placeholder="IV (CBC/GCM模式需要,16字节)" class="form-input mb-3">
|
||||
<textarea id="aesInput" rows="3" placeholder="输入文本" class="form-input mb-3"></textarea>
|
||||
<div class="flex gap-2 mb-3">
|
||||
<button id="aesEncrypt" class="btn btn-primary">加密</button>
|
||||
<button id="aesDecrypt" class="btn btn-secondary">解密</button>
|
||||
</div>
|
||||
<div class="bg-gray-50 p-3 rounded-lg">
|
||||
<div class="text-sm text-gray-500">结果:</div>
|
||||
<div id="aesResult" class="font-mono text-sm break-all"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 哈希计算
|
||||
document.getElementById('hashBtn').addEventListener('click', async () => {
|
||||
const algo = document.getElementById('hashAlgo').value;
|
||||
const text = document.getElementById('hashInput').value;
|
||||
if (!text) { showToast('请输入文本', 'error'); return; }
|
||||
try {
|
||||
const data = await apiRequest('/crypto/hash', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ algorithm: algo, text })
|
||||
});
|
||||
document.getElementById('hashResult').textContent = data.result;
|
||||
} catch (error) {
|
||||
showToast(error.message, 'error');
|
||||
}
|
||||
});
|
||||
|
||||
// Base64
|
||||
async function base64Process(action) {
|
||||
const text = document.getElementById('base64Input').value;
|
||||
if (!text) { showToast('请输入文本', 'error'); return; }
|
||||
try {
|
||||
const data = await apiRequest('/crypto/base64', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ action, text })
|
||||
});
|
||||
document.getElementById('base64Result').textContent = data.result;
|
||||
} catch (error) {
|
||||
showToast(error.message, 'error');
|
||||
}
|
||||
}
|
||||
document.getElementById('base64Encode').addEventListener('click', () => base64Process('encode'));
|
||||
document.getElementById('base64Decode').addEventListener('click', () => base64Process('decode'));
|
||||
|
||||
// URL
|
||||
async function urlProcess(action) {
|
||||
const text = document.getElementById('urlInput').value;
|
||||
if (!text) { showToast('请输入文本', 'error'); return; }
|
||||
try {
|
||||
const data = await apiRequest('/crypto/url', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ action, text })
|
||||
});
|
||||
document.getElementById('urlResult').textContent = data.result;
|
||||
} catch (error) {
|
||||
showToast(error.message, 'error');
|
||||
}
|
||||
}
|
||||
document.getElementById('urlEncode').addEventListener('click', () => urlProcess('encode'));
|
||||
document.getElementById('urlDecode').addEventListener('click', () => urlProcess('decode'));
|
||||
|
||||
// AES
|
||||
async function aesProcess(action) {
|
||||
const mode = document.getElementById('aesMode').value;
|
||||
const key = document.getElementById('aesKey').value;
|
||||
const iv = document.getElementById('aesIv').value;
|
||||
const text = document.getElementById('aesInput').value;
|
||||
|
||||
if (!key) { showToast('请输入密钥', 'error'); return; }
|
||||
if (!text) { showToast('请输入文本', 'error'); return; }
|
||||
if ((mode === 'CBC' || mode === 'GCM') && !iv) {
|
||||
showToast('CBC/GCM模式需要IV', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await apiRequest('/crypto/aes', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ mode, action, key, iv: iv || null, text })
|
||||
});
|
||||
document.getElementById('aesResult').textContent = data.result;
|
||||
} catch (error) {
|
||||
showToast(error.message, 'error');
|
||||
}
|
||||
}
|
||||
document.getElementById('aesEncrypt').addEventListener('click', () => aesProcess('encrypt'));
|
||||
document.getElementById('aesDecrypt').addEventListener('click', () => aesProcess('decrypt'));
|
||||
|
||||
recordUsage('crypto_hash');
|
||||
</script>
|
||||
|
||||
<?php include_once '../footer.php'; ?>
|
||||
82
pages/json.php
Normal file
82
pages/json.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
/**
|
||||
* PerToolBox Front - JSON 校验器页面
|
||||
*
|
||||
* Copyright (C) 2024 Sea Network Technology Studio
|
||||
* Author: Canglan <admin@sea-studio.top>
|
||||
* License: AGPL v3
|
||||
*/
|
||||
|
||||
require_once '../config.php';
|
||||
include_once '../header.php';
|
||||
include_once '../sidebar.php';
|
||||
?>
|
||||
|
||||
<div class="main-content">
|
||||
<div class="card">
|
||||
<h1 class="text-2xl font-bold mb-6">📋 JSON 校验器</h1>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="form-label">输入 JSON:</label>
|
||||
<textarea id="jsonInput" rows="10" placeholder='{"key": "value"}' class="form-input font-mono text-sm"></textarea>
|
||||
</div>
|
||||
|
||||
<button id="validateBtn" class="btn btn-primary mb-6">校验并格式化</button>
|
||||
|
||||
<div id="resultArea" class="hidden">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<h3 class="font-semibold">结果:</h3>
|
||||
<button id="copyBtn" class="text-sm text-blue-500 hover:text-blue-700">复制</button>
|
||||
</div>
|
||||
<pre id="jsonResult" class="bg-gray-50 p-4 rounded-lg overflow-x-auto font-mono text-sm"></pre>
|
||||
</div>
|
||||
|
||||
<div id="errorArea" class="hidden">
|
||||
<div class="bg-red-50 border border-red-200 rounded-lg p-4">
|
||||
<div class="text-red-600 font-semibold mb-2">❌ JSON 无效</div>
|
||||
<div id="errorMsg" class="text-red-500 text-sm"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('validateBtn').addEventListener('click', async () => {
|
||||
const jsonString = document.getElementById('jsonInput').value;
|
||||
if (!jsonString) {
|
||||
showToast('请输入 JSON', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await apiRequest('/json/validate', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ json_string: jsonString })
|
||||
});
|
||||
|
||||
if (data.valid) {
|
||||
document.getElementById('resultArea').classList.remove('hidden');
|
||||
document.getElementById('errorArea').classList.add('hidden');
|
||||
document.getElementById('jsonResult').textContent = data.formatted;
|
||||
} else {
|
||||
document.getElementById('resultArea').classList.add('hidden');
|
||||
document.getElementById('errorArea').classList.remove('hidden');
|
||||
document.getElementById('errorMsg').textContent = `第 ${data.error.line} 行,第 ${data.error.column} 列:${data.error.message}`;
|
||||
}
|
||||
} catch (error) {
|
||||
showToast(error.message, 'error');
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('copyBtn').addEventListener('click', () => {
|
||||
const result = document.getElementById('jsonResult').textContent;
|
||||
if (result) {
|
||||
navigator.clipboard.writeText(result);
|
||||
showToast('已复制到剪贴板');
|
||||
}
|
||||
});
|
||||
|
||||
recordUsage('json');
|
||||
</script>
|
||||
|
||||
<?php include_once '../footer.php'; ?>
|
||||
157
pages/notes.php
Normal file
157
pages/notes.php
Normal file
@@ -0,0 +1,157 @@
|
||||
<?php
|
||||
/**
|
||||
* PerToolBox Front - 便签本页面
|
||||
*
|
||||
* Copyright (C) 2024 Sea Network Technology Studio
|
||||
* Author: Canglan <admin@sea-studio.top>
|
||||
* License: AGPL v3
|
||||
*/
|
||||
|
||||
require_once '../config.php';
|
||||
include_once '../header.php';
|
||||
include_once '../sidebar.php';
|
||||
?>
|
||||
|
||||
<div class="main-content">
|
||||
<div class="card">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold">📝 便签本</h1>
|
||||
<button id="addBtn" class="btn btn-primary">+ 新建便签</button>
|
||||
</div>
|
||||
|
||||
<!-- 便签网格 -->
|
||||
<div id="noteGrid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div class="text-center text-gray-400 py-8 col-span-full">加载中...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 添加/编辑模态框 -->
|
||||
<div id="modal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50">
|
||||
<div class="bg-white rounded-lg w-full max-w-lg p-6">
|
||||
<h3 id="modalTitle" class="text-xl font-bold mb-4">新建便签</h3>
|
||||
<input type="text" id="noteTitle" placeholder="标题" class="form-input mb-3">
|
||||
<textarea id="noteContent" rows="6" placeholder="内容" class="form-input mb-3"></textarea>
|
||||
<input type="text" id="noteTags" placeholder="标签(用逗号分隔)" class="form-input mb-4">
|
||||
<div class="flex gap-2">
|
||||
<button id="modalConfirm" class="btn btn-primary flex-1">保存</button>
|
||||
<button id="modalCancel" class="btn btn-secondary flex-1">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentEditId = null;
|
||||
|
||||
async function loadNotes() {
|
||||
try {
|
||||
const notes = await apiRequest('/notes');
|
||||
renderNotes(notes);
|
||||
} catch (error) {
|
||||
document.getElementById('noteGrid').innerHTML = `<div class="text-center text-red-500 py-8 col-span-full">${error.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderNotes(notes) {
|
||||
if (!notes.length) {
|
||||
document.getElementById('noteGrid').innerHTML = '<div class="text-center text-gray-400 py-8 col-span-full">暂无便签,新建一个吧~</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const html = notes.map(note => `
|
||||
<div class="border rounded-lg p-4 hover:shadow-md transition bg-white">
|
||||
<div class="flex justify-between items-start mb-2">
|
||||
<h3 class="font-bold text-lg">${escapeHtml(note.title)}</h3>
|
||||
<div class="flex gap-1">
|
||||
<button onclick="editNote(${note.id})" class="text-blue-500 hover:text-blue-700">✏️</button>
|
||||
<button onclick="deleteNote(${note.id})" class="text-red-500 hover:text-red-700">🗑️</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-gray-600 whitespace-pre-wrap text-sm">${escapeHtml(note.content || '').substring(0, 200)}${(note.content || '').length > 200 ? '...' : ''}</p>
|
||||
${note.tags && note.tags.length ? `<div class="mt-2 flex gap-1 flex-wrap">${note.tags.map(t => `<span class="text-xs bg-gray-100 px-2 py-0.5 rounded">${escapeHtml(t)}</span>`).join('')}</div>` : ''}
|
||||
<div class="mt-2 text-xs text-gray-400">${new Date(note.created_at).toLocaleString()}</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
document.getElementById('noteGrid').innerHTML = html;
|
||||
}
|
||||
|
||||
function openModal(editId = null) {
|
||||
currentEditId = editId;
|
||||
const modal = document.getElementById('modal');
|
||||
const title = document.getElementById('modalTitle');
|
||||
|
||||
if (editId) {
|
||||
title.textContent = '编辑便签';
|
||||
apiRequest(`/notes/${editId}`).then(note => {
|
||||
document.getElementById('noteTitle').value = note.title;
|
||||
document.getElementById('noteContent').value = note.content || '';
|
||||
document.getElementById('noteTags').value = (note.tags || []).join(',');
|
||||
});
|
||||
} else {
|
||||
title.textContent = '新建便签';
|
||||
document.getElementById('noteTitle').value = '';
|
||||
document.getElementById('noteContent').value = '';
|
||||
document.getElementById('noteTags').value = '';
|
||||
}
|
||||
modal.classList.remove('hidden');
|
||||
modal.classList.add('flex');
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
const modal = document.getElementById('modal');
|
||||
modal.classList.add('hidden');
|
||||
modal.classList.remove('flex');
|
||||
currentEditId = null;
|
||||
}
|
||||
|
||||
async function saveNote() {
|
||||
const data = {
|
||||
title: document.getElementById('noteTitle').value.trim(),
|
||||
content: document.getElementById('noteContent').value,
|
||||
tags: document.getElementById('noteTags').value.split(',').map(t => t.trim()).filter(t => t)
|
||||
};
|
||||
|
||||
if (!data.title) {
|
||||
showToast('请输入标题', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (currentEditId) {
|
||||
await apiRequest(`/notes/${currentEditId}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
} else {
|
||||
await apiRequest('/notes', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
}
|
||||
closeModal();
|
||||
loadNotes();
|
||||
} catch (error) {
|
||||
showToast(error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteNote(id) {
|
||||
if (!confirm('确定删除吗?')) return;
|
||||
try {
|
||||
await apiRequest(`/notes/${id}`, { method: 'DELETE' });
|
||||
loadNotes();
|
||||
} catch (error) {
|
||||
showToast(error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('addBtn').addEventListener('click', () => openModal());
|
||||
document.getElementById('modalConfirm').addEventListener('click', saveNote);
|
||||
document.getElementById('modalCancel').addEventListener('click', closeModal);
|
||||
|
||||
recordUsage('notes');
|
||||
loadNotes();
|
||||
</script>
|
||||
|
||||
<?php include_once '../footer.php'; ?>
|
||||
108
pages/password.php
Normal file
108
pages/password.php
Normal file
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
/**
|
||||
* PerToolBox Front - 密码生成器页面
|
||||
*
|
||||
* Copyright (C) 2024 Sea Network Technology Studio
|
||||
* Author: Canglan <admin@sea-studio.top>
|
||||
* License: AGPL v3
|
||||
*/
|
||||
|
||||
require_once '../config.php';
|
||||
include_once '../header.php';
|
||||
include_once '../sidebar.php';
|
||||
?>
|
||||
|
||||
<div class="main-content">
|
||||
<div class="card">
|
||||
<h1 class="text-2xl font-bold mb-6">🔑 密码生成器</h1>
|
||||
|
||||
<div class="mb-6">
|
||||
<div class="flex gap-2">
|
||||
<input type="text" id="password" readonly class="flex-1 font-mono text-xl border rounded-lg px-4 py-3 bg-gray-50">
|
||||
<button id="copyBtn" class="btn btn-secondary">复制</button>
|
||||
<button id="generateBtn" class="btn btn-primary">生成</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
||||
<label class="flex items-center gap-2"><input type="checkbox" id="useUpper" checked> 大写字母</label>
|
||||
<label class="flex items-center gap-2"><input type="checkbox" id="useLower" checked> 小写字母</label>
|
||||
<label class="flex items-center gap-2"><input type="checkbox" id="useDigits" checked> 数字</label>
|
||||
<label class="flex items-center gap-2"><input type="checkbox" id="useSymbols" checked> 符号</label>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label>密码长度: <span id="lengthValue">12</span></label>
|
||||
<input type="range" id="length" min="4" max="64" value="12" class="w-full">
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label>批量生成数量:</label>
|
||||
<div class="flex gap-2 mt-1">
|
||||
<input type="number" id="count" min="1" max="10" value="1" class="form-input w-24">
|
||||
<button id="batchBtn" class="btn btn-secondary">批量生成</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="batchResults" class="hidden mt-4 p-4 bg-gray-50 rounded-lg">
|
||||
<h3 class="font-bold mb-2">生成的密码:</h3>
|
||||
<div id="passwordList" class="space-y-1 font-mono text-sm"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function generatePassword() {
|
||||
const length = document.getElementById('length').value;
|
||||
const upper = document.getElementById('useUpper').checked;
|
||||
const lower = document.getElementById('useLower').checked;
|
||||
const digits = document.getElementById('useDigits').checked;
|
||||
const symbols = document.getElementById('useSymbols').checked;
|
||||
|
||||
try {
|
||||
const data = await apiRequest(`/password/generate?length=${length}&upper=${upper}&lower=${lower}&digits=${digits}&symbols=${symbols}`);
|
||||
document.getElementById('password').value = data.passwords;
|
||||
} catch (error) {
|
||||
showToast(error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function batchGenerate() {
|
||||
const length = document.getElementById('length').value;
|
||||
const count = document.getElementById('count').value;
|
||||
const upper = document.getElementById('useUpper').checked;
|
||||
const lower = document.getElementById('useLower').checked;
|
||||
const digits = document.getElementById('useDigits').checked;
|
||||
const symbols = document.getElementById('useSymbols').checked;
|
||||
|
||||
try {
|
||||
const data = await apiRequest(`/password/generate?length=${length}&upper=${upper}&lower=${lower}&digits=${digits}&symbols=${symbols}&count=${count}`);
|
||||
const passwords = Array.isArray(data.passwords) ? data.passwords : [data.passwords];
|
||||
const listHtml = passwords.map(p => `<div>• ${escapeHtml(p)}</div>`).join('');
|
||||
document.getElementById('passwordList').innerHTML = listHtml;
|
||||
document.getElementById('batchResults').classList.remove('hidden');
|
||||
} catch (error) {
|
||||
showToast(error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function copyPassword() {
|
||||
const pwd = document.getElementById('password').value;
|
||||
if (pwd) {
|
||||
navigator.clipboard.writeText(pwd);
|
||||
showToast('已复制到剪贴板');
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('length').addEventListener('input', (e) => {
|
||||
document.getElementById('lengthValue').textContent = e.target.value;
|
||||
});
|
||||
document.getElementById('generateBtn').addEventListener('click', generatePassword);
|
||||
document.getElementById('copyBtn').addEventListener('click', copyPassword);
|
||||
document.getElementById('batchBtn').addEventListener('click', batchGenerate);
|
||||
|
||||
recordUsage('password');
|
||||
generatePassword();
|
||||
</script>
|
||||
|
||||
<?php include_once '../footer.php'; ?>
|
||||
84
pages/qrcode.php
Normal file
84
pages/qrcode.php
Normal file
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
/**
|
||||
* PerToolBox Front - 二维码生成器页面
|
||||
*
|
||||
* Copyright (C) 2024 Sea Network Technology Studio
|
||||
* Author: Canglan <admin@sea-studio.top>
|
||||
* License: AGPL v3
|
||||
*/
|
||||
|
||||
require_once '../config.php';
|
||||
include_once '../header.php';
|
||||
include_once '../sidebar.php';
|
||||
?>
|
||||
|
||||
<div class="main-content">
|
||||
<div class="card">
|
||||
<h1 class="text-2xl font-bold mb-6">📱 二维码生成器</h1>
|
||||
|
||||
<div class="grid md:grid-cols-2 gap-8">
|
||||
<div>
|
||||
<label class="form-label">内容/链接:</label>
|
||||
<textarea id="content" rows="4" placeholder="输入文本或URL..." class="form-input mb-4"></textarea>
|
||||
|
||||
<label class="form-label">尺寸:<span id="sizeValue">10</span></label>
|
||||
<input type="range" id="size" min="5" max="20" value="10" class="w-full mb-4">
|
||||
|
||||
<button id="generateBtn" class="btn btn-primary w-full">生成二维码</button>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<div id="qrContainer" class="bg-gray-50 rounded-lg p-4 min-h-[300px] flex items-center justify-center">
|
||||
<div class="text-gray-400">点击生成二维码</div>
|
||||
</div>
|
||||
<button id="downloadBtn" class="btn btn-secondary mt-4 hidden">下载二维码</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentQRCode = null;
|
||||
|
||||
async function generateQR() {
|
||||
const content = document.getElementById('content').value.trim();
|
||||
if (!content) {
|
||||
showToast('请输入内容', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const size = document.getElementById('size').value;
|
||||
|
||||
try {
|
||||
const data = await apiRequest('/qrcode/generate', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ content, size: parseInt(size) })
|
||||
});
|
||||
|
||||
currentQRCode = data.qr_code;
|
||||
document.getElementById('qrContainer').innerHTML = `<img src="${data.qr_code}" alt="二维码" class="max-w-full mx-auto">`;
|
||||
document.getElementById('downloadBtn').classList.remove('hidden');
|
||||
} catch (error) {
|
||||
showToast(error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function downloadQR() {
|
||||
if (currentQRCode) {
|
||||
const link = document.createElement('a');
|
||||
link.download = 'qrcode.png';
|
||||
link.href = currentQRCode;
|
||||
link.click();
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('size').addEventListener('input', (e) => {
|
||||
document.getElementById('sizeValue').textContent = e.target.value;
|
||||
});
|
||||
document.getElementById('generateBtn').addEventListener('click', generateQR);
|
||||
document.getElementById('downloadBtn').addEventListener('click', downloadQR);
|
||||
|
||||
recordUsage('qrcode');
|
||||
</script>
|
||||
|
||||
<?php include_once '../footer.php'; ?>
|
||||
262
pages/todos.php
Normal file
262
pages/todos.php
Normal file
@@ -0,0 +1,262 @@
|
||||
<?php
|
||||
/**
|
||||
* PerToolBox Front - 待办事项页面
|
||||
*
|
||||
* Copyright (C) 2024 Sea Network Technology Studio
|
||||
* Author: Canglan <admin@sea-studio.top>
|
||||
* License: AGPL v3
|
||||
*/
|
||||
|
||||
require_once '../config.php';
|
||||
include_once '../header.php';
|
||||
include_once '../sidebar.php';
|
||||
?>
|
||||
|
||||
<div class="main-content">
|
||||
<div class="card">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold">✅ 待办事项</h1>
|
||||
<button id="addBtn" class="btn btn-primary">+ 添加</button>
|
||||
</div>
|
||||
|
||||
<!-- 筛选栏 -->
|
||||
<div class="flex gap-2 mb-4 flex-wrap">
|
||||
<button class="filter-btn active" data-filter="all">全部</button>
|
||||
<button class="filter-btn" data-filter="active">未完成</button>
|
||||
<button class="filter-btn" data-filter="completed">已完成</button>
|
||||
<select id="categoryFilter" class="border rounded px-2 py-1 text-sm">
|
||||
<option value="">全部分类</option>
|
||||
<option value="学习">学习</option>
|
||||
<option value="工作">工作</option>
|
||||
<option value="生活">生活</option>
|
||||
<option value="其他">其他</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 待办列表 -->
|
||||
<div id="todoList" class="space-y-2">
|
||||
<div class="text-center text-gray-400 py-8">加载中...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 添加/编辑模态框 -->
|
||||
<div id="modal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50">
|
||||
<div class="bg-white rounded-lg w-full max-w-md p-6">
|
||||
<h3 id="modalTitle" class="text-xl font-bold mb-4">添加待办</h3>
|
||||
<input type="text" id="todoTitle" placeholder="标题" class="form-input mb-3">
|
||||
<textarea id="todoDesc" rows="3" placeholder="描述(可选)" class="form-input mb-3"></textarea>
|
||||
<select id="todoPriority" class="form-input mb-3">
|
||||
<option value="1">低优先级</option>
|
||||
<option value="2" selected>中优先级</option>
|
||||
<option value="3">高优先级</option>
|
||||
</select>
|
||||
<select id="todoCategory" class="form-input mb-3">
|
||||
<option value="学习">学习</option>
|
||||
<option value="工作">工作</option>
|
||||
<option value="生活">生活</option>
|
||||
<option value="其他">其他</option>
|
||||
</select>
|
||||
<input type="datetime-local" id="todoDueDate" class="form-input mb-4">
|
||||
<div class="flex gap-2">
|
||||
<button id="modalConfirm" class="btn btn-primary flex-1">确认</button>
|
||||
<button id="modalCancel" class="btn btn-secondary flex-1">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentEditId = null;
|
||||
let currentFilter = 'all';
|
||||
let currentCategory = '';
|
||||
|
||||
// 加载待办列表
|
||||
async function loadTodos() {
|
||||
let url = '/todos';
|
||||
const params = [];
|
||||
if (currentFilter === 'active') params.push('completed=false');
|
||||
if (currentFilter === 'completed') params.push('completed=true');
|
||||
if (currentCategory) params.push(`category=${encodeURIComponent(currentCategory)}`);
|
||||
if (params.length) url += '?' + params.join('&');
|
||||
|
||||
try {
|
||||
const todos = await apiRequest(url);
|
||||
renderTodos(todos);
|
||||
} catch (error) {
|
||||
document.getElementById('todoList').innerHTML = `<div class="text-center text-red-500 py-8">${error.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderTodos(todos) {
|
||||
if (!todos.length) {
|
||||
document.getElementById('todoList').innerHTML = '<div class="text-center text-gray-400 py-8">暂无待办,添加一个吧~</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const priorityMap = {1: '低', 2: '中', 3: '高'};
|
||||
const priorityColor = {1: 'gray', 2: 'yellow', 3: 'red'};
|
||||
|
||||
const html = todos.map(todo => `
|
||||
<div class="flex items-center justify-between p-3 border rounded-lg hover:bg-gray-50">
|
||||
<div class="flex items-center gap-3 flex-1">
|
||||
<input type="checkbox"
|
||||
${todo.completed ? 'checked' : ''}
|
||||
onchange="toggleTodo(${todo.id})"
|
||||
class="w-5 h-5">
|
||||
<div class="flex-1">
|
||||
<span class="${todo.completed ? 'line-through text-gray-400' : 'text-gray-700'}">
|
||||
${escapeHtml(todo.title)}
|
||||
</span>
|
||||
${todo.description ? `<p class="text-xs text-gray-400 mt-1">${escapeHtml(todo.description)}</p>` : ''}
|
||||
</div>
|
||||
<span class="text-xs px-2 py-1 rounded bg-${priorityColor[todo.priority]}-100 text-${priorityColor[todo.priority]}-800">
|
||||
${priorityMap[todo.priority]}
|
||||
</span>
|
||||
<span class="text-xs text-gray-400">${escapeHtml(todo.category)}</span>
|
||||
${todo.due_date ? `<span class="text-xs text-gray-400">📅 ${new Date(todo.due_date).toLocaleDateString()}</span>` : ''}
|
||||
</div>
|
||||
<div class="flex gap-1">
|
||||
<button onclick="editTodo(${todo.id})" class="text-blue-500 hover:text-blue-700">✏️</button>
|
||||
<button onclick="deleteTodo(${todo.id})" class="text-red-500 hover:text-red-700">🗑️</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
document.getElementById('todoList').innerHTML = html;
|
||||
}
|
||||
|
||||
// 切换完成状态
|
||||
async function toggleTodo(id) {
|
||||
try {
|
||||
const todo = await apiRequest(`/todos/${id}`);
|
||||
await apiRequest(`/todos/${id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ completed: !todo.completed })
|
||||
});
|
||||
loadTodos();
|
||||
} catch (error) {
|
||||
showToast(error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 删除待办
|
||||
async function deleteTodo(id) {
|
||||
if (!confirm('确定删除吗?')) return;
|
||||
try {
|
||||
await apiRequest(`/todos/${id}`, { method: 'DELETE' });
|
||||
loadTodos();
|
||||
} catch (error) {
|
||||
showToast(error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 打开添加模态框
|
||||
function openModal(editId = null) {
|
||||
currentEditId = editId;
|
||||
const modal = document.getElementById('modal');
|
||||
const title = document.getElementById('modalTitle');
|
||||
|
||||
if (editId) {
|
||||
title.textContent = '编辑待办';
|
||||
// 加载数据填充表单
|
||||
apiRequest(`/todos/${editId}`).then(todo => {
|
||||
document.getElementById('todoTitle').value = todo.title;
|
||||
document.getElementById('todoDesc').value = todo.description || '';
|
||||
document.getElementById('todoPriority').value = todo.priority;
|
||||
document.getElementById('todoCategory').value = todo.category;
|
||||
if (todo.due_date) {
|
||||
document.getElementById('todoDueDate').value = todo.due_date.slice(0, 16);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
title.textContent = '添加待办';
|
||||
document.getElementById('todoTitle').value = '';
|
||||
document.getElementById('todoDesc').value = '';
|
||||
document.getElementById('todoPriority').value = '2';
|
||||
document.getElementById('todoCategory').value = '学习';
|
||||
document.getElementById('todoDueDate').value = '';
|
||||
}
|
||||
modal.classList.remove('hidden');
|
||||
modal.classList.add('flex');
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
const modal = document.getElementById('modal');
|
||||
modal.classList.add('hidden');
|
||||
modal.classList.remove('flex');
|
||||
currentEditId = null;
|
||||
}
|
||||
|
||||
// 保存待办
|
||||
async function saveTodo() {
|
||||
const data = {
|
||||
title: document.getElementById('todoTitle').value.trim(),
|
||||
description: document.getElementById('todoDesc').value,
|
||||
priority: parseInt(document.getElementById('todoPriority').value),
|
||||
category: document.getElementById('todoCategory').value,
|
||||
due_date: document.getElementById('todoDueDate').value || null
|
||||
};
|
||||
|
||||
if (!data.title) {
|
||||
showToast('请输入标题', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (currentEditId) {
|
||||
await apiRequest(`/todos/${currentEditId}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
} else {
|
||||
await apiRequest('/todos', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
}
|
||||
closeModal();
|
||||
loadTodos();
|
||||
} catch (error) {
|
||||
showToast(error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 筛选器
|
||||
document.querySelectorAll('.filter-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active', 'bg-blue-500', 'text-white'));
|
||||
btn.classList.add('active', 'bg-blue-500', 'text-white');
|
||||
currentFilter = btn.dataset.filter;
|
||||
loadTodos();
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('categoryFilter').addEventListener('change', (e) => {
|
||||
currentCategory = e.target.value;
|
||||
loadTodos();
|
||||
});
|
||||
|
||||
document.getElementById('addBtn').addEventListener('click', () => openModal());
|
||||
document.getElementById('modalConfirm').addEventListener('click', saveTodo);
|
||||
document.getElementById('modalCancel').addEventListener('click', closeModal);
|
||||
|
||||
// 页面加载时上报热度
|
||||
recordUsage('todos');
|
||||
|
||||
loadTodos();
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.filter-btn {
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.filter-btn.active {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
|
||||
<?php include_once '../footer.php'; ?>
|
||||
123
profile.php
Normal file
123
profile.php
Normal file
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
/**
|
||||
* PerToolBox Front - 个人中心
|
||||
*
|
||||
* Copyright (C) 2024 Sea Network Technology Studio
|
||||
* Author: Canglan <admin@sea-studio.top>
|
||||
* License: AGPL v3
|
||||
*/
|
||||
|
||||
require_once 'config.php';
|
||||
include_once 'header.php';
|
||||
include_once 'sidebar.php';
|
||||
|
||||
// 检查登录状态(前端 JS 会处理跳转)
|
||||
?>
|
||||
|
||||
<div class="main-content">
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<div class="card">
|
||||
<h1 class="text-2xl font-bold mb-6">👤 个人中心</h1>
|
||||
|
||||
<div id="userProfile">
|
||||
<div class="text-center py-8">
|
||||
<div class="loading mx-auto"></div>
|
||||
<p class="mt-2 text-gray-500">加载中...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-4">
|
||||
<h2 class="text-xl font-bold mb-4">修改密码</h2>
|
||||
<div class="mb-4">
|
||||
<label class="form-label">原密码</label>
|
||||
<input type="password" id="oldPassword" class="form-input">
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="form-label">新密码</label>
|
||||
<input type="password" id="newPassword" class="form-input">
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="form-label">确认新密码</label>
|
||||
<input type="password" id="confirmPassword" class="form-input">
|
||||
</div>
|
||||
<button id="changePasswordBtn" class="btn btn-primary">修改密码</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 加载用户信息
|
||||
async function loadProfile() {
|
||||
try {
|
||||
const user = await apiRequest('/user/profile');
|
||||
const profileHtml = `
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="text-4xl">${user.avatar ? `<img src="${user.avatar}" class="w-16 h-16 rounded-full">` : '👤'}</div>
|
||||
<div>
|
||||
<div class="text-sm text-gray-500">用户名</div>
|
||||
<div class="font-medium">${escapeHtml(user.username || '未设置')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm text-gray-500">手机号</div>
|
||||
<div class="font-medium">${escapeHtml(user.phone || '未绑定')}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm text-gray-500">邮箱</div>
|
||||
<div class="font-medium">${escapeHtml(user.email || '未绑定')}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm text-gray-500">注册时间</div>
|
||||
<div class="font-medium">${new Date(user.created_at).toLocaleString()}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.getElementById('userProfile').innerHTML = profileHtml;
|
||||
} catch (error) {
|
||||
if (error.message.includes('401')) {
|
||||
window.location.href = '/login.php';
|
||||
} else {
|
||||
document.getElementById('userProfile').innerHTML = `<div class="text-center text-red-500">加载失败: ${error.message}</div>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 修改密码
|
||||
document.getElementById('changePasswordBtn').addEventListener('click', async () => {
|
||||
const oldPassword = document.getElementById('oldPassword').value;
|
||||
const newPassword = document.getElementById('newPassword').value;
|
||||
const confirmPassword = document.getElementById('confirmPassword').value;
|
||||
|
||||
if (!oldPassword || !newPassword) {
|
||||
showToast('请填写完整信息', 'error');
|
||||
return;
|
||||
}
|
||||
if (newPassword !== confirmPassword) {
|
||||
showToast('两次输入的密码不一致', 'error');
|
||||
return;
|
||||
}
|
||||
if (newPassword.length < 6) {
|
||||
showToast('新密码长度至少6位', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await apiRequest('/user/change-password', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ old_password: oldPassword, new_password: newPassword })
|
||||
});
|
||||
showToast('密码修改成功');
|
||||
document.getElementById('oldPassword').value = '';
|
||||
document.getElementById('newPassword').value = '';
|
||||
document.getElementById('confirmPassword').value = '';
|
||||
} catch (error) {
|
||||
showToast(error.message, 'error');
|
||||
}
|
||||
});
|
||||
|
||||
loadProfile();
|
||||
</script>
|
||||
|
||||
<?php include_once 'footer.php'; ?>
|
||||
68
sidebar.php
Normal file
68
sidebar.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
/**
|
||||
* PerToolBox Front - 左侧侧边栏
|
||||
*
|
||||
* Copyright (C) 2024 Sea Network Technology Studio
|
||||
* Author: Canglan <admin@sea-studio.top>
|
||||
* License: AGPL v3
|
||||
*/
|
||||
?>
|
||||
<!-- 汉堡按钮(移动端显示) -->
|
||||
<button id="menuBtn" class="fixed top-4 left-4 z-30 md:hidden bg-blue-600 text-white p-2 rounded-lg shadow-lg">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- 侧边栏 -->
|
||||
<aside id="sidebar" class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-2xl font-bold text-white"><?php echo SITE_NAME; ?></span>
|
||||
<button id="closeSidebar" class="md:hidden text-white text-2xl">×</button>
|
||||
</div>
|
||||
<p class="text-gray-300 text-sm mt-2">个人工具箱</p>
|
||||
</div>
|
||||
|
||||
<nav class="sidebar-nav">
|
||||
<a href="/" class="sidebar-link">
|
||||
<span class="text-xl">🏠</span> 首页
|
||||
</a>
|
||||
<a href="/pages/todos.php" class="sidebar-link">
|
||||
<span class="text-xl">✅</span> 待办事项
|
||||
</a>
|
||||
<a href="/pages/notes.php" class="sidebar-link">
|
||||
<span class="text-xl">📝</span> 便签本
|
||||
</a>
|
||||
<a href="/pages/password.php" class="sidebar-link">
|
||||
<span class="text-xl">🔑</span> 密码生成
|
||||
</a>
|
||||
<a href="/pages/qrcode.php" class="sidebar-link">
|
||||
<span class="text-xl">📱</span> 二维码
|
||||
</a>
|
||||
<a href="/pages/crypto.php" class="sidebar-link">
|
||||
<span class="text-xl">🔒</span> 加密工具
|
||||
</a>
|
||||
<a href="/pages/json.php" class="sidebar-link">
|
||||
<span class="text-xl">📋</span> JSON校验
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
<div id="userInfo" class="mb-4">
|
||||
<!-- 动态显示用户信息 -->
|
||||
</div>
|
||||
<a href="/profile.php" id="profileLink" class="sidebar-link" style="display: none;">
|
||||
<span class="text-xl">👤</span> 个人中心
|
||||
</a>
|
||||
<a href="#" id="logoutBtn" class="sidebar-link text-red-300 hover:text-red-200" style="display: none;">
|
||||
<span class="text-xl">🚪</span> 退出登录
|
||||
</a>
|
||||
<a href="/login.php" id="loginLink" class="sidebar-link">
|
||||
<span class="text-xl">🔐</span> 登录/注册
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- 遮罩层 -->
|
||||
<div id="overlay" class="overlay"></div>
|
||||
Reference in New Issue
Block a user