first commit

This commit is contained in:
Snowz 2025-05-26 15:23:18 +08:00
commit d75163b2b4
18 changed files with 4685 additions and 0 deletions

83
.htaccess Normal file
View File

@ -0,0 +1,83 @@
# Apache配置文件
# 用于URL重写和安全设置
# 启用重写引擎
RewriteEngine On
# URL重写规则
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php [QSA,L]
# 安全设置 - 禁止访问敏感目录
<Files "config/*">
Order Allow,Deny
Deny from all
</Files>
<Files "includes/*">
Order Allow,Deny
Deny from all
</Files>
<Files "data/*">
Order Allow,Deny
Deny from all
</Files>
# 禁止访问敏感文件
<FilesMatch "\.(sql|log|md|txt|conf)$">
Order Allow,Deny
Deny from all
</FilesMatch>
# 禁止访问隐藏文件
<FilesMatch "^\.*">
Order Allow,Deny
Deny from all
</FilesMatch>
# 禁止访问安装文件(安装完成后)
<Files "install.php">
Order Allow,Deny
Deny from all
</Files>
# 设置默认字符集
AddDefaultCharset UTF-8
# 启用GZIP压缩
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/plain
AddOutputFilterByType DEFLATE text/html
AddOutputFilterByType DEFLATE text/xml
AddOutputFilterByType DEFLATE text/css
AddOutputFilterByType DEFLATE application/xml
AddOutputFilterByType DEFLATE application/xhtml+xml
AddOutputFilterByType DEFLATE application/rss+xml
AddOutputFilterByType DEFLATE application/javascript
AddOutputFilterByType DEFLATE application/x-javascript
</IfModule>
# 设置缓存策略
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType text/css "access plus 1 month"
ExpiresByType application/javascript "access plus 1 month"
ExpiresByType image/png "access plus 1 month"
ExpiresByType image/jpg "access plus 1 month"
ExpiresByType image/jpeg "access plus 1 month"
ExpiresByType image/gif "access plus 1 month"
ExpiresByType image/ico "access plus 1 month"
ExpiresByType image/icon "access plus 1 month"
ExpiresByType text/plain "access plus 1 month"
ExpiresByType application/pdf "access plus 1 month"
</IfModule>
# 安全头设置
<IfModule mod_headers.c>
Header always set X-Content-Type-Options nosniff
Header always set X-Frame-Options DENY
Header always set X-XSS-Protection "1; mode=block"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
</IfModule>

304
DEPLOY.md Normal file
View File

@ -0,0 +1,304 @@
# 宝塔面板部署指南
本文档详细介绍如何在宝塔面板中部署内容投稿系统。
## 📋 部署前准备
### 服务器要求
- 操作系统Linux推荐CentOS 7+/Ubuntu 18+
- 内存至少512MB推荐1GB+
- 硬盘至少1GB可用空间
- 网络:稳定的互联网连接
### 宝塔面板要求
- 宝塔面板版本7.0+
- PHP版本7.4+
- Web服务器Apache或Nginx
- 数据库MySQL 5.7+可选也可使用SQLite
## 🚀 详细部署步骤
### 第一步:安装宝塔面板
如果还未安装宝塔面板,请先安装:
```bash
# CentOS安装命令
yum install -y wget && wget -O install.sh http://download.bt.cn/install/install_6.0.sh && sh install.sh
# Ubuntu安装命令
wget -O install.sh http://download.bt.cn/install/install-ubuntu_6.0.sh && sudo bash install.sh
```
### 第二步:配置服务器环境
1. **登录宝塔面板**
- 访问 `http://服务器IP:8888`
- 使用安装时显示的用户名和密码登录
2. **安装LNMP/LAMP环境**
- 选择"软件商店" → "一键部署"
- 推荐安装Nginx 1.20+ + MySQL 5.7+ + PHP 7.4+
- 等待安装完成约10-30分钟
3. **安装PHP扩展**
- 进入"软件商店" → "已安装"
- 找到PHP点击"设置"
- 在"安装扩展"中安装以下扩展:
- `pdo_mysql`MySQL支持
- `pdo_sqlite`SQLite支持
- `gd`(图像处理)
- `curl`(网络请求)
- `fileinfo`(文件信息)
### 第三步:创建网站
1. **添加站点**
- 点击"网站" → "添加站点"
- 域名填入你的域名example.com
- 根目录:默认即可
- PHP版本选择7.4或更高版本
- 数据库选择MySQL可选
- 点击"提交"
2. **配置域名解析**
- 在域名服务商处添加A记录
- 将域名指向服务器IP地址
### 第四步:上传项目文件
1. **下载项目**
- 方式一:直接上传压缩包
- 将项目打包为zip文件
- 在宝塔面板"文件"中上传到网站根目录
- 解压文件
- 方式二使用Git推荐
- 在"终端"中执行:
```bash
cd /www/wwwroot/your-domain.com
git clone https://github.com/your-repo/submission-system.git .
```
2. **设置文件权限**
- 在"文件"管理中,选择网站根目录
- 右键选择"权限"设置为755
- 特别设置以下目录权限为777
- `config/`
- `data/`
### 第五步配置数据库MySQL方式
1. **创建数据库**
- 点击"数据库" → "添加数据库"
- 数据库名:`submission_system`
- 用户名:自定义
- 密码:自动生成或自定义
- 记录数据库信息
2. **配置数据库连接**
- 编辑 `config/database.php`
- 填入正确的数据库信息
### 第六步:运行安装向导
1. **访问安装页面**
- 浏览器访问:`http://your-domain.com/install.php`
2. **环境检查**
- 系统会自动检查服务器环境
- 确保所有检查项都通过
3. **数据库配置**
- 选择数据库类型MySQL或SQLite
- 填入数据库连接信息
- 测试连接
4. **初始化数据库**
- 点击"初始化数据库"
- 等待数据表创建完成
5. **完成安装**
- 记录默认管理员账户信息
- 删除或重命名 `install.php` 文件
### 第七步:安全配置
1. **SSL证书配置**
- 在"网站"中找到你的站点
- 点击"设置" → "SSL"
- 申请Let's Encrypt免费证书
- 开启"强制HTTPS"
2. **防火墙设置**
- 在"安全"中配置防火墙
- 开放80、443端口
- 关闭不必要的端口
3. **文件安全**
- 删除 `install.php`
- 检查敏感文件权限
- 定期备份数据
## 🔧 高级配置
### Nginx配置优化
在网站设置中添加以下Nginx配置
```nginx
# 安全设置
location ~ ^/(config|includes|data)/ {
deny all;
}
# 禁止访问敏感文件
location ~* \.(sql|log|md|txt|conf)$ {
deny all;
}
# 隐藏文件
location ~ /\. {
deny all;
}
# PHP配置
location ~ \.php$ {
fastcgi_pass unix:/tmp/php-cgi-74.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
# 静态文件缓存
location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
```
### PHP配置优化
在PHP设置中调整以下参数
```ini
; 上传限制
upload_max_filesize = 10M
post_max_size = 10M
; 执行时间
max_execution_time = 60
; 内存限制
memory_limit = 256M
; 错误报告
display_errors = Off
log_errors = On
; 会话配置
session.cookie_httponly = On
session.cookie_secure = On
```
### 定时任务配置
可以设置定时任务来清理过期数据:
```bash
# 每天凌晨2点清理7天前的IP记录
0 2 * * * /usr/bin/php /www/wwwroot/your-domain.com/cleanup.php
```
## 📊 性能优化
### 数据库优化
1. **索引优化**
```sql
-- 为常用查询字段添加索引
ALTER TABLE website_submissions ADD INDEX idx_status (status);
ALTER TABLE website_submissions ADD INDEX idx_created (created_at);
ALTER TABLE app_submissions ADD INDEX idx_status (status);
ALTER TABLE app_submissions ADD INDEX idx_created (created_at);
```
2. **定期清理**
- 定期清理过期的IP限制记录
- 归档或删除过旧的投稿记录
### 缓存配置
1. **开启OPcache**
- 在PHP设置中开启OPcache扩展
- 提高PHP执行效率
2. **静态文件CDN**
- 将CSS、JS等静态文件上传到CDN
- 加速页面加载速度
## 🔍 故障排除
### 常见问题解决
1. **500错误**
- 检查PHP错误日志
- 确认文件权限设置
- 检查.htaccess配置
2. **数据库连接失败**
- 验证数据库配置信息
- 检查数据库服务状态
- 确认防火墙设置
3. **验证码不显示**
- 检查GD扩展是否安装
- 确认PHP图像处理功能
4. **无法获取网站信息**
- 检查cURL扩展
- 确认服务器网络连接
- 检查目标网站可访问性
### 日志查看
- **PHP错误日志**`/www/wwwroot/your-domain.com/php_errors.log`
- **Nginx访问日志**`/www/wwwroot/your-domain.com/log/access.log`
- **Nginx错误日志**`/www/wwwroot/your-domain.com/log/error.log`
## 📈 监控与维护
### 定期维护任务
1. **系统更新**
- 定期更新宝塔面板
- 更新PHP、MySQL版本
- 更新系统安全补丁
2. **数据备份**
- 设置自动数据库备份
- 定期下载备份文件
- 测试备份恢复流程
3. **安全检查**
- 检查异常访问日志
- 更新管理员密码
- 检查文件完整性
### 性能监控
- 使用宝塔面板的监控功能
- 关注CPU、内存、磁盘使用率
- 监控网站访问速度
## 📞 技术支持
如果在部署过程中遇到问题:
1. 查看本文档的故障排除部分
2. 检查项目的GitHub Issues
3. 联系技术支持
---
**祝您部署顺利!如有问题,欢迎反馈。**

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Content Submission System
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

261
README.md Normal file
View File

@ -0,0 +1,261 @@
# 内容投稿系统
一个轻量级、易部署的内容投稿管理系统支持网址和APP/软件投稿,具有完善的后台审核功能。
## ✨ 功能特性
### 前端功能
- 🌐 **网址投稿**自动获取网站TDK信息标题、描述、关键词
- 📱 **APP/软件投稿**:支持多平台应用投稿
- 🎯 **多平台收录**支持自媒体维基、zTab、SOSO平台选择
- 🔒 **IP限制**每IP每日最多提交3次
- 🚫 **重复检测**:智能检测重复内容
- 📱 **响应式设计**:完美适配移动端和桌面端
### 后端功能
- 👨‍💼 **管理后台**:完善的内容审核管理
- 🔐 **安全登录**:验证码保护,防暴力破解
- 📊 **数据统计**:实时查看投稿统计数据
- ✅ **状态管理**:待处理、已通过、已拒绝状态管理
- 👤 **账户管理**:支持修改管理员用户名和密码
- 🗂️ **分类管理**网址和APP投稿分开管理
### 技术特性
- 🗄️ **双数据库支持**MySQL和SQLite可选
- 🚀 **轻量化设计**纯PHP开发无复杂依赖
- 🎨 **现代化UI**:参考大厂设计风格
- 📦 **易于部署**:支持宝塔面板一键部署
- 🔧 **安装向导**:图形化安装配置
## 🛠️ 环境要求
- PHP >= 7.4
- PDO扩展
- PDO MySQL扩展必须使用MySQL或MariaDB
- GD扩展验证码功能
- cURL扩展网站信息抓取
## 📦 安装部署
### 方式一:宝塔面板部署(推荐)
1. **下载源码**
```bash
# 在宝塔面板文件管理中,进入网站根目录
# 上传项目文件或使用Git克隆
git clone https://github.com/your-repo/submission-system.git
```
2. **设置网站**
- 在宝塔面板中创建新网站
- 设置运行目录为项目根目录
- PHP版本选择7.4或以上
3. **配置数据库**
- 在宝塔面板中创建MySQL数据库
- 记录数据库名、用户名、密码
4. **设置文件权限**
```bash
chmod 755 -R /www/wwwroot/your-domain/
chmod 777 /www/wwwroot/your-domain/config/
chmod 777 /www/wwwroot/your-domain/data/
```
5. **运行安装向导**
- 访问 `http://your-domain/install.php`
- 按照向导完成安装配置
6. **安全设置**
- 安装完成后删除或重命名 `install.php`
- 登录后台修改默认密码
### 方式二:手动部署
1. **上传文件**
- 将所有文件上传到Web服务器根目录
2. **配置Web服务器**
**Apache配置**.htaccess
```apache
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php [QSA,L]
# 安全设置
<Files "config/*">
Order Allow,Deny
Deny from all
</Files>
```
**Nginx配置**
```nginx
server {
listen 80;
server_name your-domain.com;
root /path/to/submission-system;
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
# 安全设置
location ~ ^/config/ {
deny all;
}
}
```
3. **设置权限**
```bash
chmod 755 -R /path/to/submission-system/
chmod 777 /path/to/submission-system/config/
chmod 777 /path/to/submission-system/data/
```
4. **运行安装**
- 访问安装向导完成配置
## 🎯 使用说明
### 前台投稿
1. **网址投稿**
- 输入网址URL支持http/https
- 点击"获取信息"自动填充网站信息
- 选择收录平台
- 填写联系方式(可选)
- 提交投稿
2. **APP投稿**
- 切换到"APP/软件投稿"标签
- 填写应用名称、平台、版本等信息
- 提供图标地址、下载链接、官网地址
- 选择收录平台并提交
### 后台管理
1. **登录后台**
- 访问 `/admin/login.php`
- 默认账户admin/admin
- 输入验证码登录
2. **内容审核**
- 查看待处理的投稿内容
- 切换查看网址投稿和APP投稿
- 批量或单个审核通过/拒绝
- 添加审核备注
3. **账户管理**
- 点击"账户设置"修改用户名和密码
- 建议首次登录后立即修改默认密码
## 🔧 配置说明
### 数据库配置
编辑 `config/database.php`
```php
// MySQL配置
private $host = 'localhost';
private $db_name = 'submission_system';
private $username = 'root';
private $password = '';
```
### 功能配置
- **IP限制**:在 `includes/utils.php` 中修改每日提交次数限制
- **验证码**:可在 `admin/captcha.php` 中自定义验证码样式
- **平台选项**:在前端页面中修改收录平台选项
## 📁 目录结构
```
submission-system/
├── admin/ # 后台管理
│ ├── index.php # 管理主页
│ ├── login.php # 登录页面
│ ├── logout.php # 退出登录
│ └── captcha.php # 验证码生成
├── api/ # API接口
│ └── fetch_website_info.php # 获取网站信息
├── config/ # 配置文件
│ └── database.php # 数据库配置
├── includes/ # 核心文件
│ └── utils.php # 工具类
├── data/ # 数据目录SQLite
├── index.php # 前台主页
├── install.php # 安装向导
├── README.md # 说明文档
└── LICENSE # 开源协议
```
## 🔒 安全建议
1. **修改默认密码**首次登录后立即修改admin账户密码
2. **删除安装文件**:安装完成后删除 `install.php`
3. **设置文件权限**确保配置文件不可通过Web访问
4. **定期备份**:定期备份数据库和配置文件
5. **更新维护**及时更新PHP版本和扩展
## 🐛 常见问题
### Q: 无法获取网站信息?
A: 检查服务器是否支持cURL扩展确保目标网站可访问。
### Q: 验证码不显示?
A: 检查GD扩展是否安装确保PHP支持图像处理。
### Q: 数据库连接失败?
A: 检查数据库配置信息,确保数据库服务正常运行。
### Q: 文件上传权限错误?
A: 检查目录权限设置确保Web服务器有写入权限。
### Q: 页面样式异常?
A: 检查CDN资源是否正常加载可考虑本地化CSS/JS文件。
## 🤝 贡献指南
欢迎提交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
## 📄 开源协议
本项目采用 MIT 协议开源,详见 [LICENSE](LICENSE) 文件。
## 🙏 致谢
- 感谢所有为开源社区做出贡献的开发者
- 特别感谢提供设计灵感的各大互联网公司
- 感谢使用本系统的每一位用户
## 📞 联系方式
如有问题或建议,欢迎通过以下方式联系:
- 提交 Issue
- 发送邮件
- 加入讨论群
---
**⭐ 如果这个项目对你有帮助请给个Star支持一下**

BIN
admin/arial.ttf Normal file

Binary file not shown.

60
admin/captcha.php Normal file
View File

@ -0,0 +1,60 @@
<?php
session_start();
// 生成验证码
$code = '';
$chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
for ($i = 0; $i < 4; $i++) {
$code .= $chars[rand(0, strlen($chars) - 1)];
}
$_SESSION['captcha'] = $code;
// 创建图片
$width = 100;
$height = 40;
$image = imagecreate($width, $height);
// 颜色定义
$bg_color = imagecolorallocate($image, 245, 245, 245);
$text_color = imagecolorallocate($image, 50, 50, 50);
$line_color = imagecolorallocate($image, 200, 200, 200);
$noise_color = imagecolorallocate($image, 180, 180, 180);
// 填充背景
imagefill($image, 0, 0, $bg_color);
// 添加干扰线
for ($i = 0; $i < 5; $i++) {
imageline($image, rand(0, $width), rand(0, $height), rand(0, $width), rand(0, $height), $line_color);
}
// 添加噪点
for ($i = 0; $i < 50; $i++) {
imagesetpixel($image, rand(0, $width), rand(0, $height), $noise_color);
}
// 添加验证码文字
for ($i = 0; $i < 4; $i++) {
$x = 15 + $i * 18;
$y = rand(8, 15);
$angle = rand(-15, 15);
if (function_exists('imagettftext')) {
// 如果支持TTF字体
imagettftext($image, 16, $angle, $x, 25, $text_color, __DIR__ . '/arial.ttf', $code[$i]);
} else {
// 使用内置字体
imagestring($image, 5, $x, $y, $code[$i], $text_color);
}
}
// 输出图片
header('Content-Type: image/png');
header('Cache-Control: no-cache, no-store, must-revalidate');
header('Pragma: no-cache');
header('Expires: 0');
imagepng($image);
imagedestroy($image);
?>

750
admin/index.php Normal file
View File

@ -0,0 +1,750 @@
<?php
session_start();
// 检查登录状态
if (!isset($_SESSION['admin_logged_in']) || !$_SESSION['admin_logged_in']) {
header('Location: login.php');
exit;
}
require_once '../config/database.php';
require_once '../includes/utils.php';
$database = new Database();
$db = $database->getConnection();
$message = '';
$message_type = '';
// 处理操作
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $_POST['action'] ?? '';
if ($action === 'update_status') {
$type = $_POST['type'] ?? '';
$id = $_POST['id'] ?? '';
$status = $_POST['status'] ?? '';
$note = $_POST['note'] ?? '';
if ($type === 'website') {
$stmt = $db->prepare("UPDATE website_submissions SET status = ?, admin_note = ? WHERE id = ?");
} else {
$stmt = $db->prepare("UPDATE app_submissions SET status = ?, admin_note = ? WHERE id = ?");
}
if ($stmt->execute([$status, $note, $id])) {
$message = '状态更新成功';
$message_type = 'success';
} else {
$message = '状态更新失败';
$message_type = 'error';
}
} elseif ($action === 'update_account') {
$new_username = trim($_POST['new_username'] ?? '');
$new_password = $_POST['new_password'] ?? '';
$confirm_password = $_POST['confirm_password'] ?? '';
if (empty($new_username)) {
$message = '用户名不能为空';
$message_type = 'error';
} elseif (!empty($new_password) && $new_password !== $confirm_password) {
$message = '两次输入的密码不一致';
$message_type = 'error';
} else {
$admin_id = $_SESSION['admin_id'];
if (!empty($new_password)) {
$hashed_password = password_hash($new_password, PASSWORD_DEFAULT);
$stmt = $db->prepare("UPDATE admins SET username = ?, password = ? WHERE id = ?");
$stmt->execute([$new_username, $hashed_password, $admin_id]);
} else {
$stmt = $db->prepare("UPDATE admins SET username = ? WHERE id = ?");
$stmt->execute([$new_username, $admin_id]);
}
$_SESSION['admin_username'] = $new_username;
$message = '账户信息更新成功';
$message_type = 'success';
}
}
}
// 获取统计数据
$stats = [
'website_pending' => 0,
'website_approved' => 0,
'website_rejected' => 0,
'app_pending' => 0,
'app_approved' => 0,
'app_rejected' => 0
];
$stmt = $db->prepare("SELECT status, COUNT(*) as count FROM website_submissions GROUP BY status");
$stmt->execute();
while ($row = $stmt->fetch()) {
$stats['website_' . $row['status']] = $row['count'];
}
$stmt = $db->prepare("SELECT status, COUNT(*) as count FROM app_submissions GROUP BY status");
$stmt->execute();
while ($row = $stmt->fetch()) {
$stats['app_' . $row['status']] = $row['count'];
}
// 获取列表数据
$filter = $_GET['filter'] ?? 'pending';
$type = $_GET['type'] ?? 'website';
$page = max(1, intval($_GET['page'] ?? 1));
$limit = 10;
$offset = ($page - 1) * $limit;
// 强制转换为整数
$limit = (int)$limit;
$offset = (int)$offset;
if ($type === 'website') {
$count_stmt = $db->prepare("SELECT COUNT(*) FROM website_submissions WHERE status = ?");
$count_stmt->execute([$filter]);
$total = $count_stmt->fetchColumn();
$stmt = $db->prepare("
SELECT id, url, title, description, platforms, contact, status, admin_note, created_at
FROM website_submissions
WHERE status = ?
ORDER BY created_at DESC
LIMIT ?, ?
");
$stmt->bindValue(1, $filter, PDO::PARAM_STR);
$stmt->bindValue(2, $offset, PDO::PARAM_INT);
$stmt->bindValue(3, $limit, PDO::PARAM_INT);
$stmt->execute();
} else {
$count_stmt = $db->prepare("SELECT COUNT(*) FROM app_submissions WHERE status = ?");
$count_stmt->execute([$filter]);
$total = $count_stmt->fetchColumn();
$stmt = $db->prepare("
SELECT id, name, platform, version, icon_url, download_url, website_url, description, platforms, contact, status, admin_note, created_at
FROM app_submissions
WHERE status = ?
ORDER BY created_at DESC
LIMIT ?, ?
");
$stmt->bindValue(1, $filter, PDO::PARAM_STR);
$stmt->bindValue(2, $offset, PDO::PARAM_INT);
$stmt->bindValue(3, $limit, PDO::PARAM_INT);
$stmt->execute();
}
$submissions = $stmt->fetchAll();
$total_pages = ceil($total / $limit);
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>管理后台 - 内容投稿系统</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: #f8fafc;
color: #1a202c;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px 0;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.header-content {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.header h1 {
font-size: 1.5rem;
font-weight: 700;
}
.header-actions {
display: flex;
gap: 15px;
align-items: center;
}
.btn {
padding: 8px 16px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 6px;
}
.btn-primary {
background: rgba(255, 255, 255, 0.2);
color: white;
border: 1px solid rgba(255, 255, 255, 0.3);
}
.btn-primary:hover {
background: rgba(255, 255, 255, 0.3);
}
.btn-danger {
background: #ef4444;
color: white;
}
.btn-danger:hover {
background: #dc2626;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 30px 20px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: white;
padding: 25px;
border-radius: 12px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
border-left: 4px solid #667eea;
}
.stat-card h3 {
font-size: 2rem;
font-weight: 700;
color: #667eea;
margin-bottom: 5px;
}
.stat-card p {
color: #64748b;
font-size: 14px;
}
.controls {
background: white;
padding: 20px;
border-radius: 12px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
margin-bottom: 20px;
display: flex;
gap: 15px;
align-items: center;
flex-wrap: wrap;
}
.controls select {
padding: 8px 12px;
border: 2px solid #e2e8f0;
border-radius: 6px;
font-size: 14px;
}
.submissions-table {
background: white;
border-radius: 12px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
overflow: hidden;
}
.table {
width: 100%;
border-collapse: collapse;
}
.table th,
.table td {
padding: 15px;
text-align: left;
border-bottom: 1px solid #e2e8f0;
}
.table th {
background: #f8fafc;
font-weight: 600;
color: #374151;
}
.table tr:hover {
background: #f8fafc;
}
.status-badge {
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.status-pending {
background: #fef3c7;
color: #92400e;
}
.status-approved {
background: #d1fae5;
color: #065f46;
}
.status-rejected {
background: #fee2e2;
color: #991b1b;
}
.action-buttons {
display: flex;
gap: 5px;
}
.btn-sm {
padding: 4px 8px;
font-size: 12px;
}
.btn-success {
background: #10b981;
color: white;
}
.btn-warning {
background: #f59e0b;
color: white;
}
.pagination {
display: flex;
justify-content: center;
gap: 10px;
margin-top: 20px;
}
.pagination a {
padding: 8px 12px;
border: 1px solid #e2e8f0;
border-radius: 6px;
text-decoration: none;
color: #374151;
}
.pagination a.active {
background: #667eea;
color: white;
border-color: #667eea;
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
}
.modal-content {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 30px;
border-radius: 12px;
width: 90%;
max-width: 500px;
}
.modal h3 {
margin-bottom: 20px;
color: #374151;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: 500;
color: #374151;
}
.form-group select,
.form-group textarea,
.form-group input {
width: 100%;
padding: 8px 12px;
border: 2px solid #e2e8f0;
border-radius: 6px;
font-size: 14px;
}
.form-group textarea {
resize: vertical;
min-height: 80px;
}
.modal-actions {
display: flex;
gap: 10px;
justify-content: flex-end;
margin-top: 20px;
}
.message {
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
font-weight: 500;
}
.message.success {
background: #d1fae5;
color: #065f46;
border: 1px solid #10b981;
}
.message.error {
background: #fee2e2;
color: #991b1b;
border: 1px solid #ef4444;
}
.account-settings {
background: white;
padding: 20px;
border-radius: 12px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
margin-bottom: 20px;
}
.account-settings.collapsed {
display: none;
}
@media (max-width: 768px) {
.header-content {
flex-direction: column;
gap: 15px;
}
.controls {
flex-direction: column;
align-items: stretch;
}
.table {
font-size: 14px;
}
.table th,
.table td {
padding: 10px 8px;
}
}
</style>
</head>
<body>
<div class="header">
<div class="header-content">
<h1><i class="fas fa-tachometer-alt"></i> 管理后台</h1>
<div class="header-actions">
<span>欢迎,<?php echo htmlspecialchars($_SESSION['admin_username']); ?></span>
<button class="btn btn-primary" onclick="toggleAccountSettings()">
<i class="fas fa-cog"></i> 账户设置
</button>
<a href="../index.php" class="btn btn-primary" target="_blank">
<i class="fas fa-external-link-alt"></i> 前台
</a>
<a href="logout.php" class="btn btn-danger">
<i class="fas fa-sign-out-alt"></i> 退出
</a>
</div>
</div>
</div>
<div class="container">
<?php if ($message): ?>
<div class="message <?php echo $message_type; ?>">
<?php echo htmlspecialchars($message); ?>
</div>
<?php endif; ?>
<!-- 账户设置 -->
<div class="account-settings" id="accountSettings">
<h3><i class="fas fa-user-cog"></i> 账户设置</h3>
<form method="POST">
<input type="hidden" name="action" value="update_account">
<div class="form-group">
<label for="new_username">新用户名</label>
<input type="text" id="new_username" name="new_username"
value="<?php echo htmlspecialchars($_SESSION['admin_username']); ?>" required>
</div>
<div class="form-group">
<label for="new_password">新密码(留空则不修改)</label>
<input type="password" id="new_password" name="new_password">
</div>
<div class="form-group">
<label for="confirm_password">确认新密码</label>
<input type="password" id="confirm_password" name="confirm_password">
</div>
<div class="modal-actions">
<button type="submit" class="btn btn-success">
<i class="fas fa-save"></i> 保存
</button>
<button type="button" class="btn btn-warning" onclick="toggleAccountSettings()">
<i class="fas fa-times"></i> 取消
</button>
</div>
</form>
</div>
<!-- 统计数据 -->
<div class="stats-grid">
<div class="stat-card">
<h3><?php echo $stats['website_pending']; ?></h3>
<p>网址待审核</p>
</div>
<div class="stat-card">
<h3><?php echo $stats['website_approved']; ?></h3>
<p>网址已通过</p>
</div>
<div class="stat-card">
<h3><?php echo $stats['app_pending']; ?></h3>
<p>应用待审核</p>
</div>
<div class="stat-card">
<h3><?php echo $stats['app_approved']; ?></h3>
<p>应用已通过</p>
</div>
</div>
<!-- 筛选控制 -->
<div class="controls">
<label>类型:</label>
<select onchange="changeType(this.value)">
<option value="website" <?php echo $type === 'website' ? 'selected' : ''; ?>>网址投稿</option>
<option value="app" <?php echo $type === 'app' ? 'selected' : ''; ?>>APP投稿</option>
</select>
<label>状态:</label>
<select onchange="changeFilter(this.value)">
<option value="pending" <?php echo $filter === 'pending' ? 'selected' : ''; ?>>待处理</option>
<option value="approved" <?php echo $filter === 'approved' ? 'selected' : ''; ?>>已通过</option>
<option value="rejected" <?php echo $filter === 'rejected' ? 'selected' : ''; ?>>已拒绝</option>
</select>
</div>
<!-- 投稿列表 -->
<div class="submissions-table">
<table class="table">
<thead>
<tr>
<th>ID</th>
<?php if ($type === 'website'): ?>
<th>网址</th>
<th>标题</th>
<?php else: ?>
<th>应用名称</th>
<th>平台</th>
<th>版本</th>
<?php endif; ?>
<th>收录平台</th>
<th>联系方式</th>
<th>状态</th>
<th>提交时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<?php foreach ($submissions as $submission): ?>
<tr>
<td><?php echo $submission['id']; ?></td>
<?php if ($type === 'website'): ?>
<td>
<a href="<?php echo htmlspecialchars($submission['url']); ?>" target="_blank">
<?php echo htmlspecialchars(substr($submission['url'], 0, 30)) . (strlen($submission['url']) > 30 ? '...' : ''); ?>
</a>
</td>
<td><?php echo htmlspecialchars($submission['title'] ?: '未获取'); ?></td>
<?php else: ?>
<td><?php echo htmlspecialchars($submission['name']); ?></td>
<td><?php echo htmlspecialchars($submission['platform']); ?></td>
<td><?php echo htmlspecialchars($submission['version'] ?: '-'); ?></td>
<?php endif; ?>
<td><?php echo htmlspecialchars($submission['platforms'] ?: '-'); ?></td>
<td><?php echo htmlspecialchars($submission['contact'] ?: '-'); ?></td>
<td>
<span class="status-badge status-<?php echo $submission['status']; ?>">
<?php
$status_text = [
'pending' => '待处理',
'approved' => '已通过',
'rejected' => '已拒绝'
];
echo $status_text[$submission['status']];
?>
</span>
</td>
<td><?php echo date('Y-m-d H:i', strtotime($submission['created_at'])); ?></td>
<td>
<div class="action-buttons">
<button class="btn btn-sm btn-primary"
onclick="showStatusModal(<?php echo $submission['id']; ?>, '<?php echo $type; ?>', '<?php echo $submission['status']; ?>', '<?php echo htmlspecialchars($submission['admin_note'] ?? '', ENT_QUOTES); ?>')">
<i class="fas fa-edit"></i>
</button>
<?php if ($submission['status'] === 'pending'): ?>
<button class="btn btn-sm btn-success"
onclick="quickUpdate(<?php echo $submission['id']; ?>, '<?php echo $type; ?>', 'approved')">
<i class="fas fa-check"></i>
</button>
<button class="btn btn-sm btn-danger"
onclick="quickUpdate(<?php echo $submission['id']; ?>, '<?php echo $type; ?>', 'rejected')">
<i class="fas fa-times"></i>
</button>
<?php endif; ?>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<!-- 分页 -->
<?php if ($total_pages > 1): ?>
<div class="pagination">
<?php for ($i = 1; $i <= $total_pages; $i++): ?>
<a href="?type=<?php echo $type; ?>&filter=<?php echo $filter; ?>&page=<?php echo $i; ?>"
class="<?php echo $i === $page ? 'active' : ''; ?>">
<?php echo $i; ?>
</a>
<?php endfor; ?>
</div>
<?php endif; ?>
</div>
<!-- 状态更新模态框 -->
<div class="modal" id="statusModal">
<div class="modal-content">
<h3>更新状态</h3>
<form method="POST">
<input type="hidden" name="action" value="update_status">
<input type="hidden" name="type" id="modal_type">
<input type="hidden" name="id" id="modal_id">
<div class="form-group">
<label for="modal_status">状态</label>
<select name="status" id="modal_status">
<option value="pending">待处理</option>
<option value="approved">通过</option>
<option value="rejected">拒绝</option>
</select>
</div>
<div class="form-group">
<label for="modal_note">备注</label>
<textarea name="note" id="modal_note" placeholder="审核备注(可选)"></textarea>
</div>
<div class="modal-actions">
<button type="submit" class="btn btn-success">
<i class="fas fa-save"></i> 保存
</button>
<button type="button" class="btn btn-warning" onclick="closeModal()">
<i class="fas fa-times"></i> 取消
</button>
</div>
</form>
</div>
</div>
<script>
function changeType(type) {
window.location.href = `?type=${type}&filter=<?php echo $filter; ?>`;
}
function changeFilter(filter) {
window.location.href = `?type=<?php echo $type; ?>&filter=${filter}`;
}
function showStatusModal(id, type, status, note) {
document.getElementById('modal_id').value = id;
document.getElementById('modal_type').value = type;
document.getElementById('modal_status').value = status;
document.getElementById('modal_note').value = note;
document.getElementById('statusModal').style.display = 'block';
}
function closeModal() {
document.getElementById('statusModal').style.display = 'none';
}
function quickUpdate(id, type, status) {
if (confirm(`确定要${status === 'approved' ? '通过' : '拒绝'}这个投稿吗?`)) {
const form = document.createElement('form');
form.method = 'POST';
form.innerHTML = `
<input type="hidden" name="action" value="update_status">
<input type="hidden" name="type" value="${type}">
<input type="hidden" name="id" value="${id}">
<input type="hidden" name="status" value="${status}">
<input type="hidden" name="note" value="">
`;
document.body.appendChild(form);
form.submit();
}
}
function toggleAccountSettings() {
const settings = document.getElementById('accountSettings');
settings.classList.toggle('collapsed');
}
// 点击模态框外部关闭
window.onclick = function(event) {
const modal = document.getElementById('statusModal');
if (event.target === modal) {
closeModal();
}
}
// 初始化时折叠账户设置
document.getElementById('accountSettings').classList.add('collapsed');
</script>
</body>
</html>

280
admin/login.php Normal file
View File

@ -0,0 +1,280 @@
<?php
session_start();
require_once '../config/database.php';
require_once '../includes/utils.php';
// 如果已登录,跳转到管理页面
if (isset($_SESSION['admin_logged_in']) && $_SESSION['admin_logged_in']) {
header('Location: index.php');
exit;
}
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = trim($_POST['username'] ?? '');
$password = $_POST['password'] ?? '';
$captcha = $_POST['captcha'] ?? '';
if (empty($username) || empty($password) || empty($captcha)) {
$error = '请填写完整信息';
} elseif (!Utils::verifyCaptcha($captcha)) {
$error = '验证码错误';
} else {
$database = new Database();
$db = $database->getConnection();
$stmt = $db->prepare("SELECT id, username, password FROM admins WHERE username = ?");
$stmt->execute([$username]);
$admin = $stmt->fetch();
if ($admin && password_verify($password, $admin['password'])) {
$_SESSION['admin_logged_in'] = true;
$_SESSION['admin_id'] = $admin['id'];
$_SESSION['admin_username'] = $admin['username'];
header('Location: index.php');
exit;
} else {
$error = '用户名或密码错误';
}
}
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>管理后台登录</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.login-container {
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
overflow: hidden;
width: 100%;
max-width: 400px;
backdrop-filter: blur(10px);
}
.login-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 40px 30px;
text-align: center;
}
.login-header h1 {
font-size: 1.8rem;
font-weight: 700;
margin-bottom: 8px;
}
.login-header p {
opacity: 0.9;
font-size: 0.9rem;
}
.login-form {
padding: 40px 30px;
}
.form-group {
margin-bottom: 25px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #374151;
font-size: 14px;
}
.form-group input {
width: 100%;
padding: 12px 16px;
border: 2px solid #e5e7eb;
border-radius: 10px;
font-size: 16px;
transition: all 0.3s ease;
background: #fafafa;
}
.form-group input:focus {
outline: none;
border-color: #667eea;
background: white;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.captcha-group {
display: flex;
gap: 10px;
align-items: end;
}
.captcha-group input {
flex: 1;
}
.captcha-image {
border: 2px solid #e5e7eb;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
}
.captcha-image:hover {
border-color: #667eea;
}
.error-message {
background: #fee2e2;
color: #991b1b;
padding: 12px 16px;
border-radius: 8px;
margin-bottom: 20px;
font-size: 14px;
border: 1px solid #fecaca;
}
.login-btn {
width: 100%;
padding: 14px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 10px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.login-btn:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
}
.login-btn:active {
transform: translateY(0);
}
.default-account {
margin-top: 20px;
padding: 15px;
background: #f0f9ff;
border: 1px solid #bae6fd;
border-radius: 8px;
font-size: 13px;
color: #0369a1;
}
.default-account strong {
display: block;
margin-bottom: 5px;
}
@media (max-width: 480px) {
.login-container {
margin: 10px;
border-radius: 15px;
}
.login-header {
padding: 30px 20px;
}
.login-form {
padding: 30px 20px;
}
}
</style>
</head>
<body>
<div class="login-container">
<div class="login-header">
<h1><i class="fas fa-shield-alt"></i> 管理后台</h1>
<p>内容投稿系统管理中心</p>
</div>
<div class="login-form">
<?php if ($error): ?>
<div class="error-message">
<i class="fas fa-exclamation-circle"></i> <?php echo htmlspecialchars($error); ?>
</div>
<?php endif; ?>
<form method="POST">
<div class="form-group">
<label for="username">用户名</label>
<input type="text" id="username" name="username" required
value="<?php echo htmlspecialchars($_POST['username'] ?? ''); ?>">
</div>
<div class="form-group">
<label for="password">密码</label>
<input type="password" id="password" name="password" required>
</div>
<div class="form-group">
<label for="captcha">验证码</label>
<div class="captcha-group">
<input type="text" id="captcha" name="captcha" required
placeholder="请输入验证码" maxlength="4">
<img src="captcha.php" alt="验证码" class="captcha-image"
onclick="this.src='captcha.php?'+Math.random()"
title="点击刷新验证码">
</div>
</div>
<button type="submit" class="login-btn">
<i class="fas fa-sign-in-alt"></i> 登录
</button>
</form>
<div class="default-account">
<strong><i class="fas fa-info-circle"></i> 默认账户信息:</strong>
用户名admin<br>
密码admin
</div>
</div>
</div>
<script>
// 自动聚焦到用户名输入框
document.getElementById('username').focus();
// 回车键提交表单
document.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
document.querySelector('form').submit();
}
});
</script>
</body>
</html>

22
admin/logout.php Normal file
View File

@ -0,0 +1,22 @@
<?php
session_start();
// 清除所有会话数据
$_SESSION = array();
// 如果使用了cookie也删除它
if (ini_get("session.use_cookies")) {
$params = session_get_cookie_params();
setcookie(session_name(), '', time() - 42000,
$params["path"], $params["domain"],
$params["secure"], $params["httponly"]
);
}
// 销毁会话
session_destroy();
// 重定向到登录页面
header('Location: login.php');
exit;
?>

View File

@ -0,0 +1,35 @@
<?php
/**
* 获取网站信息API接口
*/
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST');
header('Access-Control-Allow-Headers: Content-Type');
require_once '../config/database.php';
require_once '../includes/utils.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Method not allowed']);
exit;
}
$input = json_decode(file_get_contents('php://input'), true);
$url = $input['url'] ?? '';
if (empty($url)) {
echo json_encode(['success' => false, 'message' => 'URL is required']);
exit;
}
$database = new Database();
$db = $database->getConnection();
$utils = new Utils($db);
$result = $utils->getWebsiteInfo($url);
echo json_encode($result);
?>

524
assets/css/style.css Normal file
View File

@ -0,0 +1,524 @@
/* 投稿系统样式文件 */
/* 现代化设计,参考互联网大厂设计思路 */
/* 全局样式重置 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* CSS变量定义 - 统一配色方案 */
:root {
/* 主色调 - 现代蓝紫渐变 */
--primary-color: #667eea;
--primary-dark: #5a67d8;
--primary-light: #7c3aed;
--primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
/* 辅助色 */
--secondary-color: #f093fb;
--accent-color: #4facfe;
/* 状态色 */
--success-color: #10b981;
--warning-color: #f59e0b;
--error-color: #ef4444;
--info-color: #3b82f6;
/* 中性色 */
--text-primary: #1f2937;
--text-secondary: #6b7280;
--text-light: #9ca3af;
--border-color: #e5e7eb;
--bg-primary: #ffffff;
--bg-secondary: #f9fafb;
--bg-dark: #111827;
/* 阴影 */
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
/* 圆角 */
--radius-sm: 0.375rem;
--radius-md: 0.5rem;
--radius-lg: 0.75rem;
--radius-xl: 1rem;
/* 间距 */
--spacing-xs: 0.25rem;
--spacing-sm: 0.5rem;
--spacing-md: 1rem;
--spacing-lg: 1.5rem;
--spacing-xl: 2rem;
--spacing-2xl: 3rem;
}
/* 基础样式 */
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
line-height: 1.6;
color: var(--text-primary);
background: var(--bg-secondary);
font-size: 16px;
}
/* 容器样式 */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 var(--spacing-md);
}
.container-sm {
max-width: 600px;
margin: 0 auto;
padding: 0 var(--spacing-md);
}
/* 卡片样式 */
.card {
background: var(--bg-primary);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-md);
padding: var(--spacing-xl);
margin-bottom: var(--spacing-lg);
border: 1px solid var(--border-color);
transition: all 0.3s ease;
}
.card:hover {
box-shadow: var(--shadow-lg);
transform: translateY(-2px);
}
.card-header {
margin-bottom: var(--spacing-lg);
padding-bottom: var(--spacing-md);
border-bottom: 2px solid var(--border-color);
}
.card-title {
font-size: 1.5rem;
font-weight: 600;
color: var(--text-primary);
margin-bottom: var(--spacing-sm);
}
.card-subtitle {
color: var(--text-secondary);
font-size: 0.875rem;
}
/* 按钮样式 */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.75rem 1.5rem;
font-size: 0.875rem;
font-weight: 500;
border-radius: var(--radius-md);
border: none;
cursor: pointer;
text-decoration: none;
transition: all 0.2s ease;
min-height: 44px;
gap: var(--spacing-sm);
}
.btn-primary {
background: var(--primary-gradient);
color: white;
box-shadow: var(--shadow-sm);
}
.btn-primary:hover {
transform: translateY(-1px);
box-shadow: var(--shadow-md);
filter: brightness(1.05);
}
.btn-secondary {
background: var(--bg-primary);
color: var(--text-primary);
border: 1px solid var(--border-color);
}
.btn-secondary:hover {
background: var(--bg-secondary);
border-color: var(--primary-color);
}
.btn-success {
background: var(--success-color);
color: white;
}
.btn-warning {
background: var(--warning-color);
color: white;
}
.btn-danger {
background: var(--error-color);
color: white;
}
.btn-sm {
padding: 0.5rem 1rem;
font-size: 0.75rem;
min-height: 36px;
}
.btn-lg {
padding: 1rem 2rem;
font-size: 1rem;
min-height: 52px;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
/* 表单样式 */
.form-group {
margin-bottom: var(--spacing-lg);
}
.form-label {
display: block;
font-weight: 500;
color: var(--text-primary);
margin-bottom: var(--spacing-sm);
font-size: 0.875rem;
}
.form-label.required::after {
content: ' *';
color: var(--error-color);
}
.form-control {
width: 100%;
padding: 0.75rem;
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
font-size: 0.875rem;
transition: all 0.2s ease;
background: var(--bg-primary);
min-height: 44px;
}
.form-control:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.form-control:invalid {
border-color: var(--error-color);
}
textarea.form-control {
resize: vertical;
min-height: 100px;
}
select.form-control {
cursor: pointer;
}
/* 复选框样式 */
.checkbox-group {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
}
.checkbox-item {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-sm);
border-radius: var(--radius-sm);
transition: background-color 0.2s ease;
}
.checkbox-item:hover {
background: var(--bg-secondary);
}
.checkbox-item input[type="checkbox"] {
width: 18px;
height: 18px;
accent-color: var(--primary-color);
}
.checkbox-item label {
font-size: 0.875rem;
cursor: pointer;
flex: 1;
}
/* 警告提示样式 */
.platform-warning {
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
border: 1px solid #f59e0b;
border-radius: var(--radius-md);
padding: var(--spacing-md);
margin-top: var(--spacing-sm);
font-size: 0.75rem;
color: #92400e;
display: none;
}
.platform-warning.show {
display: block;
animation: slideDown 0.3s ease;
}
/* 切换按钮样式 */
.tab-container {
display: flex;
background: var(--bg-secondary);
border-radius: var(--radius-lg);
padding: var(--spacing-xs);
margin-bottom: var(--spacing-xl);
border: 1px solid var(--border-color);
}
.tab-btn {
flex: 1;
padding: var(--spacing-md);
background: transparent;
border: none;
border-radius: var(--radius-md);
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
color: var(--text-secondary);
}
.tab-btn.active {
background: var(--bg-primary);
color: var(--primary-color);
box-shadow: var(--shadow-sm);
}
/* 表单内容区域 */
.form-content {
display: none;
}
.form-content.active {
display: block;
animation: fadeIn 0.3s ease;
}
/* 消息提示样式 */
.alert {
padding: var(--spacing-md);
border-radius: var(--radius-md);
margin-bottom: var(--spacing-lg);
border: 1px solid transparent;
font-size: 0.875rem;
}
.alert-success {
background: #ecfdf5;
border-color: #10b981;
color: #065f46;
}
.alert-error {
background: #fef2f2;
border-color: #ef4444;
color: #991b1b;
}
.alert-warning {
background: #fffbeb;
border-color: #f59e0b;
color: #92400e;
}
.alert-info {
background: #eff6ff;
border-color: #3b82f6;
color: #1e40af;
}
/* 加载状态 */
.loading {
display: inline-flex;
align-items: center;
gap: var(--spacing-sm);
}
.spinner {
width: 16px;
height: 16px;
border: 2px solid transparent;
border-top: 2px solid currentColor;
border-radius: 50%;
animation: spin 1s linear infinite;
}
/* 响应式设计 */
@media (max-width: 768px) {
.container {
padding: 0 var(--spacing-sm);
}
.card {
padding: var(--spacing-lg);
margin-bottom: var(--spacing-md);
}
.btn {
width: 100%;
justify-content: center;
}
.tab-container {
flex-direction: column;
}
.tab-btn {
text-align: center;
}
}
/* 动画效果 */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideDown {
from {
opacity: 0;
max-height: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
max-height: 100px;
transform: translateY(0);
}
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* 页面头部样式 */
.page-header {
background: var(--primary-gradient);
color: white;
padding: var(--spacing-2xl) 0;
margin-bottom: var(--spacing-xl);
text-align: center;
}
.page-title {
font-size: 2.5rem;
font-weight: 700;
margin-bottom: var(--spacing-sm);
}
.page-subtitle {
font-size: 1.125rem;
opacity: 0.9;
font-weight: 400;
}
/* 页脚样式 */
.page-footer {
background: var(--bg-dark);
color: white;
padding: var(--spacing-xl) 0;
margin-top: var(--spacing-2xl);
text-align: center;
}
.page-footer p {
margin-bottom: var(--spacing-sm);
opacity: 0.8;
}
/* 工具类 */
.text-center { text-align: center; }
.text-left { text-align: left; }
.text-right { text-align: right; }
.mt-0 { margin-top: 0; }
.mt-1 { margin-top: var(--spacing-xs); }
.mt-2 { margin-top: var(--spacing-sm); }
.mt-3 { margin-top: var(--spacing-md); }
.mt-4 { margin-top: var(--spacing-lg); }
.mt-5 { margin-top: var(--spacing-xl); }
.mb-0 { margin-bottom: 0; }
.mb-1 { margin-bottom: var(--spacing-xs); }
.mb-2 { margin-bottom: var(--spacing-sm); }
.mb-3 { margin-bottom: var(--spacing-md); }
.mb-4 { margin-bottom: var(--spacing-lg); }
.mb-5 { margin-bottom: var(--spacing-xl); }
.hidden { display: none; }
.block { display: block; }
.inline-block { display: inline-block; }
.flex { display: flex; }
.inline-flex { display: inline-flex; }
.justify-center { justify-content: center; }
.justify-between { justify-content: space-between; }
.items-center { align-items: center; }
.w-full { width: 100%; }
.h-full { height: 100%; }
.opacity-50 { opacity: 0.5; }
.opacity-75 { opacity: 0.75; }
/* 深色模式支持 */
@media (prefers-color-scheme: dark) {
:root {
--text-primary: #f9fafb;
--text-secondary: #d1d5db;
--text-light: #9ca3af;
--border-color: #374151;
--bg-primary: #1f2937;
--bg-secondary: #111827;
}
.card {
border-color: var(--border-color);
}
.form-control {
background: var(--bg-primary);
border-color: var(--border-color);
color: var(--text-primary);
}
.btn-secondary {
background: var(--bg-primary);
color: var(--text-primary);
border-color: var(--border-color);
}
}

612
assets/js/main.js Normal file
View File

@ -0,0 +1,612 @@
/**
* 投稿系统前端交互脚本
* 提供表单切换验证提交等功能
*/
// 全局变量
let currentFormType = 'website';
let isSubmitting = false;
// DOM加载完成后初始化
document.addEventListener('DOMContentLoaded', function() {
initializeApp();
});
/**
* 初始化应用
*/
function initializeApp() {
initializeFormSwitching();
initializeFormValidation();
initializeWebsiteInfoFetching();
initializePlatformWarnings();
initializeFormSubmission();
}
/**
* 初始化表单切换功能
*/
function initializeFormSwitching() {
const tabButtons = document.querySelectorAll('.tab-btn');
const formContents = document.querySelectorAll('.form-content');
tabButtons.forEach(button => {
button.addEventListener('click', function() {
const targetForm = this.dataset.form;
switchForm(targetForm);
});
});
}
/**
* 切换表单
* @param {string} formType - 表单类型 ('website' 'app')
*/
function switchForm(formType) {
currentFormType = formType;
// 更新标签按钮状态
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.classList.remove('active');
});
document.querySelector(`[data-form="${formType}"]`).classList.add('active');
// 切换表单内容
document.querySelectorAll('.form-content').forEach(content => {
content.classList.remove('active');
});
document.getElementById(`${formType}-form`).classList.add('active');
// 更新必填字段
updateRequiredFields(formType);
// 清除之前的错误信息
clearFormErrors();
}
/**
* 更新必填字段
* @param {string} formType - 表单类型
*/
function updateRequiredFields(formType) {
// 清除所有必填标记
document.querySelectorAll('input, textarea, select').forEach(field => {
field.removeAttribute('required');
});
if (formType === 'website') {
// 网址投稿必填字段
const requiredFields = ['url', 'platforms'];
requiredFields.forEach(fieldName => {
const field = document.querySelector(`[name="${fieldName}"]`);
if (field) {
if (fieldName === 'platforms') {
// 平台选择至少选一个
const checkboxes = document.querySelectorAll('input[name="platforms[]"]');
checkboxes.forEach(cb => cb.setAttribute('required', 'required'));
} else {
field.setAttribute('required', 'required');
}
}
});
} else if (formType === 'app') {
// APP投稿必填字段
const requiredFields = ['app_name', 'platform', 'version', 'download_url'];
requiredFields.forEach(fieldName => {
const field = document.querySelector(`[name="${fieldName}"]`);
if (field) {
field.setAttribute('required', 'required');
}
});
}
}
/**
* 初始化表单验证
*/
function initializeFormValidation() {
// URL格式验证
const urlInput = document.querySelector('input[name="url"]');
if (urlInput) {
urlInput.addEventListener('blur', validateURL);
urlInput.addEventListener('input', debounce(validateURL, 500));
}
// 平台选择验证
const platformCheckboxes = document.querySelectorAll('input[name="platforms[]"]');
platformCheckboxes.forEach(checkbox => {
checkbox.addEventListener('change', validatePlatformSelection);
});
// APP表单验证
const appNameInput = document.querySelector('input[name="app_name"]');
if (appNameInput) {
appNameInput.addEventListener('blur', validateAppName);
}
const downloadUrlInput = document.querySelector('input[name="download_url"]');
if (downloadUrlInput) {
downloadUrlInput.addEventListener('blur', validateDownloadURL);
}
}
/**
* 验证URL格式
*/
function validateURL() {
const urlInput = document.querySelector('input[name="url"]');
const url = urlInput.value.trim();
if (!url) return;
// 自动添加协议
if (url && !url.match(/^https?:\/\//)) {
urlInput.value = 'https://' + url;
}
// 验证URL格式
const urlPattern = /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$/;
if (!urlPattern.test(urlInput.value)) {
showFieldError(urlInput, '请输入有效的网址格式');
return false;
} else {
clearFieldError(urlInput);
return true;
}
}
/**
* 验证平台选择
*/
function validatePlatformSelection() {
const checkboxes = document.querySelectorAll('input[name="platforms[]"]');
const checked = Array.from(checkboxes).some(cb => cb.checked);
if (!checked) {
showFieldError(checkboxes[0].closest('.checkbox-group'), '请至少选择一个收录平台');
return false;
} else {
clearFieldError(checkboxes[0].closest('.checkbox-group'));
return true;
}
}
/**
* 验证APP名称
*/
function validateAppName() {
const appNameInput = document.querySelector('input[name="app_name"]');
const appName = appNameInput.value.trim();
if (appName.length < 2) {
showFieldError(appNameInput, 'APP名称至少需要2个字符');
return false;
} else {
clearFieldError(appNameInput);
return true;
}
}
/**
* 验证下载链接
*/
function validateDownloadURL() {
const downloadUrlInput = document.querySelector('input[name="download_url"]');
const url = downloadUrlInput.value.trim();
if (!url) return;
const urlPattern = /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$/;
if (!urlPattern.test(url)) {
showFieldError(downloadUrlInput, '请输入有效的下载链接');
return false;
} else {
clearFieldError(downloadUrlInput);
return true;
}
}
/**
* 显示字段错误
* @param {Element} field - 字段元素
* @param {string} message - 错误信息
*/
function showFieldError(field, message) {
clearFieldError(field);
const errorDiv = document.createElement('div');
errorDiv.className = 'field-error';
errorDiv.style.cssText = `
color: var(--error-color);
font-size: 0.75rem;
margin-top: 0.25rem;
display: flex;
align-items: center;
gap: 0.25rem;
`;
errorDiv.innerHTML = `
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
</svg>
${message}
`;
field.style.borderColor = 'var(--error-color)';
field.parentNode.appendChild(errorDiv);
}
/**
* 清除字段错误
* @param {Element} field - 字段元素
*/
function clearFieldError(field) {
const existingError = field.parentNode.querySelector('.field-error');
if (existingError) {
existingError.remove();
}
field.style.borderColor = '';
}
/**
* 清除所有表单错误
*/
function clearFormErrors() {
document.querySelectorAll('.field-error').forEach(error => error.remove());
document.querySelectorAll('.form-control').forEach(field => {
field.style.borderColor = '';
});
}
/**
* 初始化网站信息获取功能
*/
function initializeWebsiteInfoFetching() {
const urlInput = document.querySelector('input[name="url"]');
const fetchBtn = document.querySelector('#fetch-info-btn');
if (fetchBtn) {
fetchBtn.addEventListener('click', fetchWebsiteInfo);
}
if (urlInput) {
urlInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
fetchWebsiteInfo();
}
});
}
}
/**
* 获取网站信息
*/
async function fetchWebsiteInfo() {
const urlInput = document.querySelector('input[name="url"]');
const fetchBtn = document.querySelector('#fetch-info-btn');
const url = urlInput.value.trim();
if (!url) {
showAlert('请先输入网址', 'warning');
return;
}
if (!validateURL()) {
return;
}
// 显示加载状态
const originalText = fetchBtn.innerHTML;
fetchBtn.innerHTML = '<span class="spinner"></span> 获取中...';
fetchBtn.disabled = true;
try {
const response = await fetch('api/fetch_website_info.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ url: url })
});
const data = await response.json();
if (data.success) {
// 填充表单字段
fillWebsiteInfo(data.data);
showAlert('网站信息获取成功!', 'success');
} else {
showAlert(data.message || '获取网站信息失败', 'error');
}
} catch (error) {
console.error('获取网站信息失败:', error);
showAlert('网络错误,请稍后重试', 'error');
} finally {
// 恢复按钮状态
fetchBtn.innerHTML = originalText;
fetchBtn.disabled = false;
}
}
/**
* 填充网站信息到表单
* @param {Object} info - 网站信息
*/
function fillWebsiteInfo(info) {
const fields = {
'site_name': info.title || '',
'site_description': info.description || '',
'site_keywords': info.keywords || ''
};
Object.entries(fields).forEach(([fieldName, value]) => {
const field = document.querySelector(`[name="${fieldName}"]`);
if (field && value) {
field.value = value;
// 触发输入事件以更新UI
field.dispatchEvent(new Event('input', { bubbles: true }));
}
});
}
/**
* 初始化平台警告提示
*/
function initializePlatformWarnings() {
const platformCheckboxes = document.querySelectorAll('input[name="platforms[]"]');
platformCheckboxes.forEach(checkbox => {
checkbox.addEventListener('change', updatePlatformWarnings);
});
}
/**
* 更新平台警告提示
*/
function updatePlatformWarnings() {
const warnings = {
'zmtwiki': '该平台需要合法合规的内容',
'ztab': '该平台需要合法合规的内容',
'soso': '该平台内容审查相当宽松'
};
// 清除现有警告
document.querySelectorAll('.platform-warning').forEach(warning => {
warning.classList.remove('show');
});
// 显示相关警告
Object.entries(warnings).forEach(([platform, message]) => {
const checkbox = document.querySelector(`input[value="${platform}"]`);
if (checkbox && checkbox.checked) {
showPlatformWarning(platform, message);
}
});
}
/**
* 显示平台警告
* @param {string} platform - 平台名称
* @param {string} message - 警告信息
*/
function showPlatformWarning(platform, message) {
let warningDiv = document.querySelector(`#warning-${platform}`);
if (!warningDiv) {
warningDiv = document.createElement('div');
warningDiv.id = `warning-${platform}`;
warningDiv.className = 'platform-warning';
const checkbox = document.querySelector(`input[value="${platform}"]`);
checkbox.closest('.checkbox-item').appendChild(warningDiv);
}
warningDiv.innerHTML = `
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="flex-shrink: 0;">
<path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/>
</svg>
<span>${message}</span>
`;
warningDiv.classList.add('show');
}
/**
* 初始化表单提交
*/
function initializeFormSubmission() {
const form = document.querySelector('#submission-form');
if (form) {
form.addEventListener('submit', handleFormSubmission);
}
}
/**
* 处理表单提交
* @param {Event} e - 提交事件
*/
async function handleFormSubmission(e) {
e.preventDefault();
if (isSubmitting) return;
// 验证表单
if (!validateForm()) {
return;
}
isSubmitting = true;
const submitBtn = document.querySelector('button[type="submit"]');
const originalText = submitBtn.innerHTML;
// 显示提交状态
submitBtn.innerHTML = '<span class="spinner"></span> 提交中...';
submitBtn.disabled = true;
try {
const formData = new FormData(e.target);
formData.append('form_type', currentFormType);
const response = await fetch('', {
method: 'POST',
body: formData
});
const result = await response.text();
// 检查响应中是否包含成功信息
if (result.includes('提交成功') || result.includes('success')) {
showAlert('投稿提交成功!我们会尽快审核您的内容。', 'success');
resetForm();
} else if (result.includes('已存在') || result.includes('重复')) {
showAlert('该内容已存在,请勿重复提交。', 'warning');
} else if (result.includes('超出限制') || result.includes('限制')) {
showAlert('今日提交次数已达上限,请明天再试。', 'warning');
} else {
showAlert('提交失败,请稍后重试。', 'error');
}
} catch (error) {
console.error('提交失败:', error);
showAlert('网络错误,请稍后重试。', 'error');
} finally {
// 恢复按钮状态
isSubmitting = false;
submitBtn.innerHTML = originalText;
submitBtn.disabled = false;
}
}
/**
* 验证整个表单
* @returns {boolean} 验证结果
*/
function validateForm() {
let isValid = true;
if (currentFormType === 'website') {
if (!validateURL()) isValid = false;
if (!validatePlatformSelection()) isValid = false;
} else if (currentFormType === 'app') {
if (!validateAppName()) isValid = false;
if (!validateDownloadURL()) isValid = false;
}
return isValid;
}
/**
* 重置表单
*/
function resetForm() {
const form = document.querySelector('#submission-form');
if (form) {
form.reset();
clearFormErrors();
// 清除平台警告
document.querySelectorAll('.platform-warning').forEach(warning => {
warning.classList.remove('show');
});
}
}
/**
* 显示提示信息
* @param {string} message - 提示信息
* @param {string} type - 提示类型 (success, error, warning, info)
*/
function showAlert(message, type = 'info') {
// 移除现有提示
const existingAlert = document.querySelector('.alert');
if (existingAlert) {
existingAlert.remove();
}
const alertDiv = document.createElement('div');
alertDiv.className = `alert alert-${type}`;
alertDiv.innerHTML = `
<div style="display: flex; align-items: center; gap: 0.5rem;">
${getAlertIcon(type)}
<span>${message}</span>
</div>
`;
// 插入到表单顶部
const form = document.querySelector('#submission-form');
if (form) {
form.insertBefore(alertDiv, form.firstChild);
}
// 自动隐藏
setTimeout(() => {
if (alertDiv.parentNode) {
alertDiv.style.opacity = '0';
alertDiv.style.transform = 'translateY(-10px)';
setTimeout(() => alertDiv.remove(), 300);
}
}, 5000);
}
/**
* 获取提示图标
* @param {string} type - 提示类型
* @returns {string} SVG图标
*/
function getAlertIcon(type) {
const icons = {
success: '<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>',
error: '<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2zm5 13.59L15.59 17 12 13.41 8.41 17 7 15.59 10.59 12 7 8.41 8.41 7 12 10.59 15.59 7 17 8.41 13.41 12 17 15.59z"/></svg>',
warning: '<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/></svg>',
info: '<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/></svg>'
};
return icons[type] || icons.info;
}
/**
* 防抖函数
* @param {Function} func - 要防抖的函数
* @param {number} wait - 等待时间
* @returns {Function} 防抖后的函数
*/
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
/**
* 节流函数
* @param {Function} func - 要节流的函数
* @param {number} limit - 限制时间
* @returns {Function} 节流后的函数
*/
function throttle(func, limit) {
let inThrottle;
return function() {
const args = arguments;
const context = this;
if (!inThrottle) {
func.apply(context, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
// 导出函数供其他脚本使用
window.SubmissionSystem = {
switchForm,
validateForm,
fetchWebsiteInfo,
showAlert,
resetForm
};

231
cleanup.php Normal file
View File

@ -0,0 +1,231 @@
<?php
/**
* 数据清理脚本
* 用于定期清理过期的IP限制记录和旧的投稿数据
* 建议通过定时任务每天运行一次
*/
require_once 'config/database.php';
class DataCleanup {
private $db;
public function __construct() {
$database = new Database();
$this->db = $database->getConnection();
}
/**
* 清理过期的IP限制记录
* 删除7天前的记录
*/
public function cleanupIPLimits() {
try {
$sql = "DELETE FROM ip_limits WHERE DATE(last_submit) < DATE_SUB(NOW(), INTERVAL 7 DAY)";
$stmt = $this->db->prepare($sql);
$result = $stmt->execute();
$deletedRows = $stmt->rowCount();
$this->log("清理IP限制记录: 删除了 {$deletedRows} 条过期记录");
return $deletedRows;
} catch (Exception $e) {
$this->log("清理IP限制记录失败: " . $e->getMessage(), 'ERROR');
return false;
}
}
/**
* 清理旧的投稿记录
* 删除6个月前已处理的记录
*/
public function cleanupOldSubmissions() {
$deletedWebsite = 0;
$deletedApp = 0;
try {
// 清理网址投稿记录
$sql = "DELETE FROM website_submissions WHERE status != 'pending' AND DATE(created_at) < DATE_SUB(NOW(), INTERVAL 6 MONTH)";
$stmt = $this->db->prepare($sql);
$stmt->execute();
$deletedWebsite = $stmt->rowCount();
// 清理APP投稿记录
$sql = "DELETE FROM app_submissions WHERE status != 'pending' AND DATE(created_at) < DATE_SUB(NOW(), INTERVAL 6 MONTH)";
$stmt = $this->db->prepare($sql);
$stmt->execute();
$deletedApp = $stmt->rowCount();
$this->log("清理旧投稿记录: 网址投稿 {$deletedWebsite} 条, APP投稿 {$deletedApp}");
return ['website' => $deletedWebsite, 'app' => $deletedApp];
} catch (Exception $e) {
$this->log("清理旧投稿记录失败: " . $e->getMessage(), 'ERROR');
return false;
}
}
/**
* 优化数据库表
*/
public function optimizeTables() {
try {
$tables = ['admins', 'website_submissions', 'app_submissions', 'ip_limits'];
foreach ($tables as $table) {
$sql = "OPTIMIZE TABLE {$table}";
$stmt = $this->db->prepare($sql);
$stmt->execute();
}
$this->log("数据库表优化完成");
return true;
} catch (Exception $e) {
$this->log("数据库表优化失败: " . $e->getMessage(), 'ERROR');
return false;
}
}
/**
* 获取数据库统计信息
*/
public function getStatistics() {
try {
$stats = [];
// 统计各表记录数
$tables = [
'website_submissions' => '网址投稿',
'app_submissions' => 'APP投稿',
'ip_limits' => 'IP限制记录',
'admins' => '管理员账户'
];
foreach ($tables as $table => $name) {
$sql = "SELECT COUNT(*) as count FROM {$table}";
$stmt = $this->db->prepare($sql);
$stmt->execute();
$result = $stmt->fetch(PDO::FETCH_ASSOC);
$stats[$name] = $result['count'];
}
// 统计各状态的投稿数量
$statusStats = [];
$statuses = ['pending' => '待处理', 'approved' => '已通过', 'rejected' => '已拒绝'];
foreach ($statuses as $status => $name) {
// 网址投稿
$sql = "SELECT COUNT(*) as count FROM website_submissions WHERE status = ?";
$stmt = $this->db->prepare($sql);
$stmt->execute([$status]);
$result = $stmt->fetch(PDO::FETCH_ASSOC);
$statusStats["网址投稿-{$name}"] = $result['count'];
// APP投稿
$sql = "SELECT COUNT(*) as count FROM app_submissions WHERE status = ?";
$stmt = $this->db->prepare($sql);
$stmt->execute([$status]);
$result = $stmt->fetch(PDO::FETCH_ASSOC);
$statusStats["APP投稿-{$name}"] = $result['count'];
}
return array_merge($stats, $statusStats);
} catch (Exception $e) {
$this->log("获取统计信息失败: " . $e->getMessage(), 'ERROR');
return false;
}
}
/**
* 记录日志
*/
private function log($message, $level = 'INFO') {
$timestamp = date('Y-m-d H:i:s');
$logMessage = "[{$timestamp}] [{$level}] {$message}" . PHP_EOL;
// 输出到控制台
echo $logMessage;
// 写入日志文件
$logFile = 'data/cleanup.log';
if (!file_exists('data')) {
mkdir('data', 0755, true);
}
file_put_contents($logFile, $logMessage, FILE_APPEND | LOCK_EX);
}
/**
* 执行完整的清理流程
*/
public function runFullCleanup() {
$this->log("开始执行数据清理任务");
// 显示清理前的统计信息
$beforeStats = $this->getStatistics();
if ($beforeStats) {
$this->log("清理前统计信息:");
foreach ($beforeStats as $key => $value) {
$this->log(" {$key}: {$value}");
}
}
// 执行清理任务
$ipCleanup = $this->cleanupIPLimits();
$submissionCleanup = $this->cleanupOldSubmissions();
$optimize = $this->optimizeTables();
// 显示清理后的统计信息
$afterStats = $this->getStatistics();
if ($afterStats) {
$this->log("清理后统计信息:");
foreach ($afterStats as $key => $value) {
$this->log(" {$key}: {$value}");
}
}
$this->log("数据清理任务完成");
return [
'ip_cleanup' => $ipCleanup,
'submission_cleanup' => $submissionCleanup,
'optimize' => $optimize,
'before_stats' => $beforeStats,
'after_stats' => $afterStats
];
}
}
// 如果直接运行此脚本
if (php_sapi_name() === 'cli' || !isset($_SERVER['HTTP_HOST'])) {
$cleanup = new DataCleanup();
$result = $cleanup->runFullCleanup();
echo "\n清理任务执行完成!\n";
if ($result['ip_cleanup'] !== false) {
echo "IP限制记录清理: {$result['ip_cleanup']}\n";
}
if ($result['submission_cleanup'] !== false) {
echo "投稿记录清理: 网址 {$result['submission_cleanup']['website']} 条, APP {$result['submission_cleanup']['app']}\n";
}
echo "数据库优化: " . ($result['optimize'] ? '成功' : '失败') . "\n";
} else {
// 如果通过Web访问返回JSON格式结果
header('Content-Type: application/json; charset=utf-8');
// 简单的安全检查
if (!isset($_GET['token']) || $_GET['token'] !== 'cleanup_token_2024') {
http_response_code(403);
echo json_encode(['error' => '访问被拒绝']);
exit;
}
$cleanup = new DataCleanup();
$result = $cleanup->runFullCleanup();
echo json_encode([
'success' => true,
'message' => '清理任务执行完成',
'data' => $result
], JSON_UNESCAPED_UNICODE);
}
?>

147
config/database.php Normal file
View File

@ -0,0 +1,147 @@
<?php
/**
* 数据库配置文件
* 支持MySQL和SQLite数据库
*/
class Database {
private $host = 'localhost';
private $db_name = 'submission_system';
private $username = 'root';
private $password = '';
private $conn;
private $use_sqlite = false; // 设置为true使用SQLite
public function getConnection() {
$this->conn = null;
try {
if ($this->use_sqlite) {
// SQLite配置
$this->conn = new PDO('sqlite:' . __DIR__ . '/../data/database.sqlite');
} else {
// MySQL配置
$this->conn = new PDO(
"mysql:host=" . $this->host . ";dbname=" . $this->db_name . ";charset=utf8mb4",
$this->username,
$this->password
);
}
$this->conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$this->conn->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
} catch(PDOException $exception) {
echo "连接失败: " . $exception->getMessage();
}
return $this->conn;
}
public function initDatabase() {
$conn = $this->getConnection();
// 创建管理员表
$admin_table = "
CREATE TABLE IF NOT EXISTS admins (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
";
// 创建网址投稿表
$website_table = "
CREATE TABLE IF NOT EXISTS website_submissions (
id INT AUTO_INCREMENT PRIMARY KEY,
url VARCHAR(500) NOT NULL,
title VARCHAR(200),
description TEXT,
keywords VARCHAR(500),
platforms TEXT,
contact VARCHAR(100),
ip_address VARCHAR(45) NOT NULL,
status ENUM('pending', 'approved', 'rejected') DEFAULT 'pending',
admin_note TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
)
";
// 创建APP/软件投稿表
$app_table = "
CREATE TABLE IF NOT EXISTS app_submissions (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(200) NOT NULL,
platform VARCHAR(100) NOT NULL,
version VARCHAR(50),
icon_url VARCHAR(500),
download_url VARCHAR(500),
website_url VARCHAR(500),
description TEXT,
platforms TEXT,
contact VARCHAR(100),
ip_address VARCHAR(45) NOT NULL,
status ENUM('pending', 'approved', 'rejected') DEFAULT 'pending',
admin_note TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
)
";
// 创建IP限制表
$ip_limit_table = "
CREATE TABLE IF NOT EXISTS ip_submissions (
id INT AUTO_INCREMENT PRIMARY KEY,
ip_address VARCHAR(45) NOT NULL,
submission_date DATE NOT NULL,
count INT DEFAULT 1,
UNIQUE KEY unique_ip_date (ip_address, submission_date)
)
";
if ($this->use_sqlite) {
// SQLite语法调整
$admin_table = str_replace('AUTO_INCREMENT', '', $admin_table);
$admin_table = str_replace('INT AUTO_INCREMENT PRIMARY KEY', 'INTEGER PRIMARY KEY AUTOINCREMENT', $admin_table);
$admin_table = str_replace('TIMESTAMP DEFAULT CURRENT_TIMESTAMP', 'DATETIME DEFAULT CURRENT_TIMESTAMP', $admin_table);
$website_table = str_replace('AUTO_INCREMENT', '', $website_table);
$website_table = str_replace('INT AUTO_INCREMENT PRIMARY KEY', 'INTEGER PRIMARY KEY AUTOINCREMENT', $website_table);
$website_table = str_replace('ENUM(\'pending\', \'approved\', \'rejected\')', 'TEXT CHECK(status IN (\'pending\', \'approved\', \'rejected\'))', $website_table);
$website_table = str_replace('TIMESTAMP DEFAULT CURRENT_TIMESTAMP', 'DATETIME DEFAULT CURRENT_TIMESTAMP', $website_table);
$website_table = str_replace('ON UPDATE CURRENT_TIMESTAMP', '', $website_table);
$app_table = str_replace('AUTO_INCREMENT', '', $app_table);
$app_table = str_replace('INT AUTO_INCREMENT PRIMARY KEY', 'INTEGER PRIMARY KEY AUTOINCREMENT', $app_table);
$app_table = str_replace('ENUM(\'pending\', \'approved\', \'rejected\')', 'TEXT CHECK(status IN (\'pending\', \'approved\', \'rejected\'))', $app_table);
$app_table = str_replace('TIMESTAMP DEFAULT CURRENT_TIMESTAMP', 'DATETIME DEFAULT CURRENT_TIMESTAMP', $app_table);
$app_table = str_replace('ON UPDATE CURRENT_TIMESTAMP', '', $app_table);
$ip_limit_table = str_replace('AUTO_INCREMENT', '', $ip_limit_table);
$ip_limit_table = str_replace('INT AUTO_INCREMENT PRIMARY KEY', 'INTEGER PRIMARY KEY AUTOINCREMENT', $ip_limit_table);
}
try {
$conn->exec($admin_table);
$conn->exec($website_table);
$conn->exec($app_table);
$conn->exec($ip_limit_table);
// 插入默认管理员账户
$check_admin = $conn->prepare("SELECT COUNT(*) FROM admins WHERE username = 'admin'");
$check_admin->execute();
if ($check_admin->fetchColumn() == 0) {
$default_password = password_hash('admin', PASSWORD_DEFAULT);
$insert_admin = $conn->prepare("INSERT INTO admins (username, password) VALUES ('admin', ?)");
$insert_admin->execute([$default_password]);
}
return true;
} catch(PDOException $e) {
echo "数据库初始化失败: " . $e->getMessage();
return false;
}
}
}
?>

266
includes/utils.php Normal file
View File

@ -0,0 +1,266 @@
<?php
/**
* 工具类文件
* 包含URL抓取、重复检测、IP限制等功能
*/
class Utils {
private $db;
public function __construct($database) {
$this->db = $database;
}
/**
* 获取网站TDK信息
*/
public function getWebsiteInfo($url) {
// 确保URL包含协议
if (!preg_match('/^https?:\/\//i', $url)) {
$url = 'http://' . $url;
}
$result = [
'title' => '',
'description' => '',
'keywords' => '',
'url' => $url,
'success' => false
];
try {
$context = stream_context_create([
'http' => [
'timeout' => 10,
'user_agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
]
]);
$html = @file_get_contents($url, false, $context);
if ($html === false) {
return $result;
}
// 转换编码
$html = mb_convert_encoding($html, 'UTF-8', 'auto');
// 解析HTML
$dom = new DOMDocument();
@$dom->loadHTML($html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
// 获取title
$titles = $dom->getElementsByTagName('title');
if ($titles->length > 0) {
$result['title'] = trim($titles->item(0)->textContent);
}
// 获取meta标签
$metas = $dom->getElementsByTagName('meta');
foreach ($metas as $meta) {
$name = $meta->getAttribute('name');
$property = $meta->getAttribute('property');
$content = $meta->getAttribute('content');
if (strtolower($name) === 'description' || strtolower($property) === 'og:description') {
if (empty($result['description'])) {
$result['description'] = trim($content);
}
}
if (strtolower($name) === 'keywords') {
$result['keywords'] = trim($content);
}
}
$result['success'] = true;
} catch (Exception $e) {
// 静默处理错误
}
return $result;
}
/**
* 检查IP提交限制
*/
public function checkIPLimit($ip) {
$today = date('Y-m-d');
$stmt = $this->db->prepare("
SELECT count FROM ip_submissions
WHERE ip_address = ? AND submission_date = ?
");
$stmt->execute([$ip, $today]);
$result = $stmt->fetch();
if ($result) {
return $result['count'] < 3;
}
return true;
}
/**
* 记录IP提交
*/
public function recordIPSubmission($ip) {
$today = date('Y-m-d');
$stmt = $this->db->prepare("
INSERT INTO ip_submissions (ip_address, submission_date, count)
VALUES (?, ?, 1)
ON DUPLICATE KEY UPDATE count = count + 1
");
try {
$stmt->execute([$ip, $today]);
} catch (PDOException $e) {
// SQLite兼容处理
$check = $this->db->prepare("
SELECT count FROM ip_submissions
WHERE ip_address = ? AND submission_date = ?
");
$check->execute([$ip, $today]);
$existing = $check->fetch();
if ($existing) {
$update = $this->db->prepare("
UPDATE ip_submissions
SET count = count + 1
WHERE ip_address = ? AND submission_date = ?
");
$update->execute([$ip, $today]);
} else {
$insert = $this->db->prepare("
INSERT INTO ip_submissions (ip_address, submission_date, count)
VALUES (?, ?, 1)
");
$insert->execute([$ip, $today]);
}
}
}
/**
* 检查网址重复
*/
public function checkWebsiteDuplicate($url) {
// 标准化URL
$normalized_url = $this->normalizeUrl($url);
$domain = $this->extractDomain($url);
$stmt = $this->db->prepare("
SELECT id FROM website_submissions
WHERE url = ? OR url LIKE ?
");
$stmt->execute([$normalized_url, '%' . $domain . '%']);
return $stmt->fetch() !== false;
}
/**
* 检查APP重复
*/
public function checkAppDuplicate($name, $platform) {
$stmt = $this->db->prepare("
SELECT id FROM app_submissions
WHERE name = ? AND platform = ?
");
$stmt->execute([$name, $platform]);
return $stmt->fetch() !== false;
}
/**
* 标准化URL
*/
private function normalizeUrl($url) {
// 移除协议
$url = preg_replace('/^https?:\/\//i', '', $url);
// 移除www
$url = preg_replace('/^www\./i', '', $url);
// 移除尾部斜杠
$url = rtrim($url, '/');
// 转换为小写
$url = strtolower($url);
return $url;
}
/**
* 提取域名
*/
private function extractDomain($url) {
$parsed = parse_url($url);
$host = isset($parsed['host']) ? $parsed['host'] : $url;
// 移除www
$host = preg_replace('/^www\./i', '', $host);
return strtolower($host);
}
/**
* 获取客户端IP
*/
public static function getClientIP() {
$ip_keys = ['HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP', 'HTTP_CLIENT_IP', 'REMOTE_ADDR'];
foreach ($ip_keys as $key) {
if (!empty($_SERVER[$key])) {
$ip = $_SERVER[$key];
if (strpos($ip, ',') !== false) {
$ip = trim(explode(',', $ip)[0]);
}
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
return $ip;
}
}
}
return $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
}
/**
* 生成验证码
*/
public static function generateCaptcha() {
session_start();
$code = '';
$chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
for ($i = 0; $i < 4; $i++) {
$code .= $chars[rand(0, strlen($chars) - 1)];
}
$_SESSION['captcha'] = $code;
// 创建图片
$image = imagecreate(100, 40);
$bg_color = imagecolorallocate($image, 255, 255, 255);
$text_color = imagecolorallocate($image, 0, 0, 0);
$line_color = imagecolorallocate($image, 200, 200, 200);
// 添加干扰线
for ($i = 0; $i < 5; $i++) {
imageline($image, rand(0, 100), rand(0, 40), rand(0, 100), rand(0, 40), $line_color);
}
// 添加文字
imagestring($image, 5, 25, 12, $code, $text_color);
header('Content-Type: image/png');
imagepng($image);
imagedestroy($image);
}
/**
* 验证验证码
*/
public static function verifyCaptcha($input) {
session_start();
return isset($_SESSION['captcha']) && strtoupper($input) === $_SESSION['captcha'];
}
}
?>

580
index.php Normal file
View File

@ -0,0 +1,580 @@
<?php
require_once 'config/database.php';
require_once 'includes/utils.php';
$database = new Database();
$db = $database->getConnection();
$utils = new Utils($db);
// 初始化数据库
$database->initDatabase();
$message = '';
$message_type = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$ip = Utils::getClientIP();
// 检查IP限制
if (!$utils->checkIPLimit($ip)) {
$message = '您今天的提交次数已达上限3次请明天再试。';
$message_type = 'error';
} else {
$submission_type = $_POST['submission_type'] ?? 'website';
if ($submission_type === 'website') {
// 网址投稿处理
$url = trim($_POST['url'] ?? '');
$title = trim($_POST['title'] ?? '');
$description = trim($_POST['description'] ?? '');
$keywords = trim($_POST['keywords'] ?? '');
$platforms = isset($_POST['platforms']) ? implode(',', $_POST['platforms']) : '';
$contact = trim($_POST['contact'] ?? '');
if (empty($url)) {
$message = '请输入网址URL。';
$message_type = 'error';
} elseif ($utils->checkWebsiteDuplicate($url)) {
$message = '该网址已存在,请勿重复提交。';
$message_type = 'error';
} else {
try {
$stmt = $db->prepare("
INSERT INTO website_submissions (url, title, description, keywords, platforms, contact, ip_address)
VALUES (?, ?, ?, ?, ?, ?, ?)
");
$stmt->execute([$url, $title, $description, $keywords, $platforms, $contact, $ip]);
$utils->recordIPSubmission($ip);
$message = '提交成功!我们会尽快审核您的内容。';
$message_type = 'success';
} catch (PDOException $e) {
$message = '提交失败,请稍后重试。';
$message_type = 'error';
}
}
} else {
// APP投稿处理
$name = trim($_POST['app_name'] ?? '');
$platform = trim($_POST['app_platform'] ?? '');
$version = trim($_POST['app_version'] ?? '');
$icon_url = trim($_POST['icon_url'] ?? '');
$download_url = trim($_POST['download_url'] ?? '');
$website_url = trim($_POST['website_url'] ?? '');
$description = trim($_POST['app_description'] ?? '');
$platforms = isset($_POST['platforms']) ? implode(',', $_POST['platforms']) : '';
$contact = trim($_POST['contact'] ?? '');
if (empty($name) || empty($platform)) {
$message = '请填写应用名称和系统平台。';
$message_type = 'error';
} elseif ($utils->checkAppDuplicate($name, $platform)) {
$message = '该应用已存在,请勿重复提交。';
$message_type = 'error';
} else {
try {
$stmt = $db->prepare("
INSERT INTO app_submissions (name, platform, version, icon_url, download_url, website_url, description, platforms, contact, ip_address)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
");
$stmt->execute([$name, $platform, $version, $icon_url, $download_url, $website_url, $description, $platforms, $contact, $ip]);
$utils->recordIPSubmission($ip);
$message = '提交成功!我们会尽快审核您的内容。';
$message_type = 'success';
} catch (PDOException $e) {
$message = '提交失败,请稍后重试。';
$message_type = 'error';
}
}
}
}
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>内容投稿系统</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 800px;
margin: 0 auto;
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
overflow: hidden;
backdrop-filter: blur(10px);
}
.header {
background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
color: white;
padding: 40px;
text-align: center;
}
.header h1 {
font-size: 2.5rem;
font-weight: 700;
margin-bottom: 10px;
}
.header p {
font-size: 1.1rem;
opacity: 0.9;
}
.tab-container {
display: flex;
background: #f8fafc;
border-bottom: 1px solid #e2e8f0;
}
.tab {
flex: 1;
padding: 20px;
text-align: center;
cursor: pointer;
font-weight: 600;
color: #64748b;
transition: all 0.3s ease;
border-bottom: 3px solid transparent;
}
.tab.active {
color: #4f46e5;
background: white;
border-bottom-color: #4f46e5;
}
.tab:hover {
background: #f1f5f9;
}
.form-container {
padding: 40px;
}
.form-section {
display: none;
}
.form-section.active {
display: block;
}
.form-group {
margin-bottom: 25px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #374151;
}
.form-group input,
.form-group textarea,
.form-group select {
width: 100%;
padding: 12px 16px;
border: 2px solid #e5e7eb;
border-radius: 10px;
font-size: 16px;
transition: all 0.3s ease;
background: #fafafa;
}
.form-group input:focus,
.form-group textarea:focus,
.form-group select:focus {
outline: none;
border-color: #4f46e5;
background: white;
box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1);
}
.form-group textarea {
resize: vertical;
min-height: 100px;
}
.checkbox-group {
display: flex;
flex-wrap: wrap;
gap: 15px;
margin-top: 10px;
}
.checkbox-item {
display: flex;
align-items: center;
gap: 8px;
}
.checkbox-item input[type="checkbox"] {
width: auto;
margin: 0;
}
.platform-warning {
margin-top: 10px;
padding: 12px;
border-radius: 8px;
font-size: 14px;
display: none;
}
.platform-warning.compliance {
background: #fef3c7;
color: #92400e;
border: 1px solid #fbbf24;
}
.platform-warning.loose {
background: #dbeafe;
color: #1e40af;
border: 1px solid #60a5fa;
}
.btn-group {
display: flex;
gap: 15px;
margin-top: 30px;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 10px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 8px;
}
.btn-primary {
background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
color: white;
flex: 1;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(79, 70, 229, 0.3);
}
.btn-secondary {
background: #f1f5f9;
color: #64748b;
border: 2px solid #e2e8f0;
}
.btn-secondary:hover {
background: #e2e8f0;
}
.message {
padding: 15px;
border-radius: 10px;
margin-bottom: 20px;
font-weight: 500;
}
.message.success {
background: #d1fae5;
color: #065f46;
border: 1px solid #10b981;
}
.message.error {
background: #fee2e2;
color: #991b1b;
border: 1px solid #ef4444;
}
.url-fetch {
display: flex;
gap: 10px;
align-items: end;
}
.url-fetch input {
flex: 1;
}
.url-fetch button {
white-space: nowrap;
}
@media (max-width: 768px) {
.container {
margin: 10px;
border-radius: 15px;
}
.header {
padding: 30px 20px;
}
.header h1 {
font-size: 2rem;
}
.form-container {
padding: 30px 20px;
}
.btn-group {
flex-direction: column;
}
.url-fetch {
flex-direction: column;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1><i class="fas fa-paper-plane"></i> 内容投稿系统</h1>
<p>分享优质内容,共建互联网生态</p>
</div>
<div class="tab-container">
<div class="tab active" onclick="switchTab('website')">
<i class="fas fa-globe"></i> 网址投稿
</div>
<div class="tab" onclick="switchTab('app')">
<i class="fas fa-mobile-alt"></i> APP/软件投稿
</div>
</div>
<div class="form-container">
<?php if ($message): ?>
<div class="message <?php echo $message_type; ?>">
<?php echo htmlspecialchars($message); ?>
</div>
<?php endif; ?>
<form method="POST" id="submissionForm">
<input type="hidden" name="submission_type" id="submission_type" value="website">
<!-- 网址投稿表单 -->
<div class="form-section active" id="website-form">
<div class="form-group">
<label for="url">网址URL *</label>
<div class="url-fetch">
<input type="url" id="url" name="url" placeholder="https://example.com" required>
<button type="button" class="btn btn-secondary" onclick="fetchWebsiteInfo()">
<i class="fas fa-download"></i> 获取信息
</button>
</div>
</div>
<div class="form-group">
<label for="title">网站名称</label>
<input type="text" id="title" name="title" placeholder="网站标题">
</div>
<div class="form-group">
<label for="description">网站描述</label>
<textarea id="description" name="description" placeholder="网站描述信息"></textarea>
</div>
<div class="form-group">
<label for="keywords">关键词</label>
<input type="text" id="keywords" name="keywords" placeholder="关键词,用逗号分隔">
</div>
</div>
<!-- APP投稿表单 -->
<div class="form-section" id="app-form">
<div class="form-group">
<label for="app_name">应用名称 *</label>
<input type="text" id="app_name" name="app_name" placeholder="应用名称">
</div>
<div class="form-group">
<label for="app_platform">系统平台 *</label>
<select id="app_platform" name="app_platform">
<option value="">请选择平台</option>
<option value="Windows">Windows</option>
<option value="macOS">macOS</option>
<option value="Linux">Linux</option>
<option value="Android">Android</option>
<option value="iOS">iOS</option>
<option value="Web">Web应用</option>
<option value="跨平台">跨平台</option>
</select>
</div>
<div class="form-group">
<label for="app_version">版本号</label>
<input type="text" id="app_version" name="app_version" placeholder="v1.0.0">
</div>
<div class="form-group">
<label for="icon_url">图标地址</label>
<input type="url" id="icon_url" name="icon_url" placeholder="https://example.com/icon.png">
</div>
<div class="form-group">
<label for="download_url">下载链接</label>
<input type="url" id="download_url" name="download_url" placeholder="https://example.com/download">
</div>
<div class="form-group">
<label for="website_url">官网/落地页</label>
<input type="url" id="website_url" name="website_url" placeholder="https://example.com">
</div>
<div class="form-group">
<label for="app_description">应用描述</label>
<textarea id="app_description" name="app_description" placeholder="应用功能描述"></textarea>
</div>
</div>
<!-- 公共字段 -->
<div class="form-group">
<label>收录平台</label>
<div class="checkbox-group">
<div class="checkbox-item">
<input type="checkbox" id="platform1" name="platforms[]" value="自媒体维基" onchange="checkPlatforms()">
<label for="platform1">自媒体维基</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="platform2" name="platforms[]" value="zTab" onchange="checkPlatforms()">
<label for="platform2">zTab</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="platform3" name="platforms[]" value="SOSO" onchange="checkPlatforms()">
<label for="platform3">SOSO</label>
</div>
</div>
<div class="platform-warning compliance" id="compliance-warning">
<i class="fas fa-exclamation-triangle"></i> 提醒自媒体维基和zTab平台需要合法合规的内容请确保您提交的内容符合相关法律法规。
</div>
<div class="platform-warning loose" id="loose-warning">
<i class="fas fa-info-circle"></i> 提醒SOSO平台内容审查相对宽松但仍需遵守基本的网络道德规范。
</div>
</div>
<div class="form-group">
<label for="contact">联系方式(可选)</label>
<input type="text" id="contact" name="contact" placeholder="邮箱或其他联系方式">
</div>
<div class="btn-group">
<button type="submit" class="btn btn-primary">
<i class="fas fa-paper-plane"></i> 提交投稿
</button>
</div>
</form>
</div>
</div>
<script>
function switchTab(type) {
// 切换标签
document.querySelectorAll('.tab').forEach(tab => tab.classList.remove('active'));
event.target.closest('.tab').classList.add('active');
// 切换表单
document.querySelectorAll('.form-section').forEach(section => section.classList.remove('active'));
document.getElementById(type + '-form').classList.add('active');
// 更新提交类型
document.getElementById('submission_type').value = type;
// 更新必填字段
updateRequiredFields(type);
}
function updateRequiredFields(type) {
// 清除所有必填
document.querySelectorAll('input[required], select[required]').forEach(field => {
field.removeAttribute('required');
});
if (type === 'website') {
document.getElementById('url').setAttribute('required', 'required');
} else {
document.getElementById('app_name').setAttribute('required', 'required');
document.getElementById('app_platform').setAttribute('required', 'required');
}
}
function checkPlatforms() {
const compliance = document.querySelectorAll('#platform1:checked, #platform2:checked').length > 0;
const loose = document.querySelector('#platform3:checked');
document.getElementById('compliance-warning').style.display = compliance ? 'block' : 'none';
document.getElementById('loose-warning').style.display = loose ? 'block' : 'none';
}
async function fetchWebsiteInfo() {
const url = document.getElementById('url').value;
if (!url) {
alert('请先输入网址');
return;
}
const button = event.target;
const originalText = button.innerHTML;
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 获取中...';
button.disabled = true;
try {
const response = await fetch('api/fetch_website_info.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ url: url })
});
const data = await response.json();
if (data.success) {
document.getElementById('title').value = data.title || '';
document.getElementById('description').value = data.description || '';
document.getElementById('keywords').value = data.keywords || '';
} else {
alert('获取网站信息失败,请手动填写');
}
} catch (error) {
alert('获取网站信息失败,请手动填写');
} finally {
button.innerHTML = originalText;
button.disabled = false;
}
}
// 初始化
updateRequiredFields('website');
</script>
</body>
</html>

478
install.php Normal file
View File

@ -0,0 +1,478 @@
<?php
/**
* 安装脚本
* 用于初始化数据库和检查环境
*/
$step = $_GET['step'] ?? 1;
$message = '';
$message_type = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($step == 2) {
// 数据库配置测试
$db_type = $_POST['db_type'] ?? 'mysql';
$host = $_POST['host'] ?? 'localhost';
$dbname = $_POST['dbname'] ?? 'submission_system';
$username = $_POST['username'] ?? 'root';
$password = $_POST['password'] ?? '';
try {
if ($db_type === 'sqlite') {
$pdo = new PDO('sqlite:' . __DIR__ . '/data/database.sqlite');
$message = 'SQLite数据库连接成功';
$message_type = 'success';
// 更新配置文件
$config_content = file_get_contents('config/database.php');
$config_content = str_replace('private $use_sqlite = false;', 'private $use_sqlite = true;', $config_content);
file_put_contents('config/database.php', $config_content);
} else {
$pdo = new PDO("mysql:host=$host;dbname=$dbname;charset=utf8mb4", $username, $password);
$message = 'MySQL数据库连接成功';
$message_type = 'success';
// 更新配置文件
$config_content = file_get_contents('config/database.php');
$config_content = str_replace("private \$host = 'localhost';", "private \$host = '$host';", $config_content);
$config_content = str_replace("private \$db_name = 'submission_system';", "private \$db_name = '$dbname';", $config_content);
$config_content = str_replace("private \$username = 'root';", "private \$username = '$username';", $config_content);
$config_content = str_replace("private \$password = '';", "private \$password = '$password';", $config_content);
file_put_contents('config/database.php', $config_content);
}
$step = 3;
} catch (PDOException $e) {
$message = '数据库连接失败: ' . $e->getMessage();
$message_type = 'error';
}
} elseif ($step == 3) {
// 初始化数据库
require_once 'config/database.php';
$database = new Database();
if ($database->initDatabase()) {
$message = '数据库初始化成功!';
$message_type = 'success';
$step = 4;
} else {
$message = '数据库初始化失败!';
$message_type = 'error';
}
}
}
// 环境检查
function checkEnvironment() {
$checks = [
'PHP版本 >= 7.4' => version_compare(PHP_VERSION, '7.4.0', '>='),
'PDO扩展' => extension_loaded('pdo'),
'PDO MySQL扩展' => extension_loaded('pdo_mysql'),
'PDO SQLite扩展' => extension_loaded('pdo_sqlite'),
'GD扩展' => extension_loaded('gd'),
'cURL扩展' => extension_loaded('curl'),
'config目录可写' => is_writable(__DIR__ . '/config'),
'data目录可写' => is_writable(__DIR__ . '/data') || mkdir(__DIR__ . '/data', 0755, true)
];
return $checks;
}
$env_checks = checkEnvironment();
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>安装向导 - 内容投稿系统</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 600px;
margin: 0 auto;
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
overflow: hidden;
backdrop-filter: blur(10px);
}
.header {
background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
color: white;
padding: 40px;
text-align: center;
}
.header h1 {
font-size: 2rem;
font-weight: 700;
margin-bottom: 10px;
}
.step-indicator {
display: flex;
justify-content: center;
gap: 10px;
margin-top: 20px;
}
.step {
width: 30px;
height: 30px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.3);
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
}
.step.active {
background: white;
color: #4f46e5;
}
.step.completed {
background: #10b981;
color: white;
}
.content {
padding: 40px;
}
.message {
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
font-weight: 500;
}
.message.success {
background: #d1fae5;
color: #065f46;
border: 1px solid #10b981;
}
.message.error {
background: #fee2e2;
color: #991b1b;
border: 1px solid #ef4444;
}
.check-list {
list-style: none;
margin-bottom: 20px;
}
.check-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 0;
border-bottom: 1px solid #e5e7eb;
}
.check-item:last-child {
border-bottom: none;
}
.check-icon {
width: 20px;
text-align: center;
}
.check-icon.success {
color: #10b981;
}
.check-icon.error {
color: #ef4444;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #374151;
}
.form-group input,
.form-group select {
width: 100%;
padding: 12px 16px;
border: 2px solid #e5e7eb;
border-radius: 8px;
font-size: 16px;
}
.form-group input:focus,
.form-group select:focus {
outline: none;
border-color: #4f46e5;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
gap: 8px;
}
.btn-primary {
background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(79, 70, 229, 0.3);
}
.btn-success {
background: #10b981;
color: white;
}
.btn-success:hover {
background: #059669;
}
.actions {
display: flex;
gap: 15px;
justify-content: center;
margin-top: 30px;
}
.db-option {
padding: 20px;
border: 2px solid #e5e7eb;
border-radius: 8px;
margin-bottom: 15px;
cursor: pointer;
transition: all 0.3s ease;
}
.db-option:hover {
border-color: #4f46e5;
}
.db-option.selected {
border-color: #4f46e5;
background: #f0f9ff;
}
.db-config {
display: none;
margin-top: 20px;
padding: 20px;
background: #f8fafc;
border-radius: 8px;
}
.db-config.active {
display: block;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1><i class="fas fa-cogs"></i> 安装向导</h1>
<p>内容投稿系统安装配置</p>
<div class="step-indicator">
<div class="step <?php echo $step >= 1 ? ($step > 1 ? 'completed' : 'active') : ''; ?>">1</div>
<div class="step <?php echo $step >= 2 ? ($step > 2 ? 'completed' : 'active') : ''; ?>">2</div>
<div class="step <?php echo $step >= 3 ? ($step > 3 ? 'completed' : 'active') : ''; ?>">3</div>
<div class="step <?php echo $step >= 4 ? 'active' : ''; ?>">4</div>
</div>
</div>
<div class="content">
<?php if ($message): ?>
<div class="message <?php echo $message_type; ?>">
<?php echo htmlspecialchars($message); ?>
</div>
<?php endif; ?>
<?php if ($step == 1): ?>
<!-- 步骤1: 环境检查 -->
<h2>环境检查</h2>
<p>正在检查服务器环境是否满足运行要求...</p>
<ul class="check-list">
<?php foreach ($env_checks as $name => $status): ?>
<li class="check-item">
<div class="check-icon <?php echo $status ? 'success' : 'error'; ?>">
<i class="fas <?php echo $status ? 'fa-check' : 'fa-times'; ?>"></i>
</div>
<span><?php echo $name; ?></span>
</li>
<?php endforeach; ?>
</ul>
<?php if (array_product($env_checks)): ?>
<div class="actions">
<a href="?step=2" class="btn btn-primary">
<i class="fas fa-arrow-right"></i> 下一步
</a>
</div>
<?php else: ?>
<div class="message error">
<i class="fas fa-exclamation-triangle"></i>
环境检查未通过,请先解决上述问题后再继续安装。
</div>
<?php endif; ?>
<?php elseif ($step == 2): ?>
<!-- 步骤2: 数据库配置 -->
<h2>数据库配置</h2>
<p>请选择数据库类型并配置连接信息。</p>
<form method="POST">
<div class="db-option" onclick="selectDatabase('mysql')">
<h3><i class="fas fa-database"></i> MySQL数据库</h3>
<p>适合生产环境,性能更好,支持并发访问</p>
</div>
<div class="db-option" onclick="selectDatabase('sqlite')">
<h3><i class="fas fa-file-alt"></i> SQLite数据库</h3>
<p>适合小型站点,无需额外配置,开箱即用</p>
</div>
<input type="hidden" name="db_type" id="db_type" value="mysql">
<div class="db-config" id="mysql-config">
<h4>MySQL配置</h4>
<div class="form-group">
<label for="host">数据库主机</label>
<input type="text" name="host" id="host" value="localhost" required>
</div>
<div class="form-group">
<label for="dbname">数据库名称</label>
<input type="text" name="dbname" id="dbname" value="submission_system" required>
</div>
<div class="form-group">
<label for="username">用户名</label>
<input type="text" name="username" id="username" value="root" required>
</div>
<div class="form-group">
<label for="password">密码</label>
<input type="password" name="password" id="password">
</div>
</div>
<div class="db-config" id="sqlite-config">
<h4>SQLite配置</h4>
<p>SQLite数据库将自动创建在 data/database.sqlite 文件中,无需额外配置。</p>
</div>
<div class="actions">
<button type="submit" class="btn btn-primary">
<i class="fas fa-check"></i> 测试连接
</button>
</div>
</form>
<?php elseif ($step == 3): ?>
<!-- 步骤3: 初始化数据库 -->
<h2>初始化数据库</h2>
<p>数据库连接成功!现在将创建必要的数据表。</p>
<form method="POST">
<div class="actions">
<button type="submit" class="btn btn-primary">
<i class="fas fa-database"></i> 初始化数据库
</button>
</div>
</form>
<?php elseif ($step == 4): ?>
<!-- 步骤4: 安装完成 -->
<h2>安装完成</h2>
<div class="message success">
<i class="fas fa-check-circle"></i>
恭喜!内容投稿系统安装成功!
</div>
<h3>默认管理员账户</h3>
<p><strong>用户名:</strong>admin</p>
<p><strong>密码:</strong>admin</p>
<p style="color: #ef4444; margin-top: 10px;">⚠️ 请登录后台后立即修改默认密码!</p>
<h3>下一步操作</h3>
<ul style="margin: 20px 0; padding-left: 20px;">
<li>删除或重命名 install.php 文件以确保安全</li>
<li>配置Web服务器如Apache、Nginx</li>
<li>设置适当的文件权限</li>
<li>登录管理后台修改默认密码</li>
</ul>
<div class="actions">
<a href="index.php" class="btn btn-primary">
<i class="fas fa-home"></i> 访问前台
</a>
<a href="admin/login.php" class="btn btn-success">
<i class="fas fa-shield-alt"></i> 管理后台
</a>
</div>
<?php endif; ?>
</div>
</div>
<script>
function selectDatabase(type) {
// 移除所有选中状态
document.querySelectorAll('.db-option').forEach(option => {
option.classList.remove('selected');
});
// 隐藏所有配置
document.querySelectorAll('.db-config').forEach(config => {
config.classList.remove('active');
});
// 选中当前选项
event.currentTarget.classList.add('selected');
// 显示对应配置
document.getElementById(type + '-config').classList.add('active');
// 设置表单值
document.getElementById('db_type').value = type;
}
// 默认选择MySQL
document.addEventListener('DOMContentLoaded', function() {
document.querySelector('.db-option').click();
});
</script>
</body>
</html>

31
需求说明.md Normal file
View File

@ -0,0 +1,31 @@
创建一个投稿系统,需求:
网址投稿系统能够通过URL快速获取网站的TDK内容用户在填入网址URL的时候可以兼容http和https的链接。
获取的内容可以快速填入网站名称、网站描述、关键词、网址另外为了细化到具体投稿至哪个平台可以增加三个可以多选的选项分别是自媒体维基、zTab、SOSO在用户勾选自媒体维基、zTab时提醒用户该平台需要合法合规的内容在勾选SOSO时提醒用户该平台内容审查相当宽松。用户可以选择是否留有联系方式以便我们后续沟通。方便用户提交无需注册登录就可以提交但为了安全起见每天每个IP最多只能提交三次且数据库里有相同的内容则提示内容已存在重复检测可以检测域名二级域名网址参数有更好的方案你可以使用
所以用户表单部分的内容应该是:
需要获取信息的URL输入框、网站名称、网站描述、网站关键词、收录平台、联系方式、提交。如果你有好的思路可以补充完善
后台可以审核内容,前台提交完成后,可以告诉用户,我们会尽快审核内容。用户提交的内容只会显示在后台和数据库里,通过审核/拒绝的内容不会显示在列表里,默认显示的内容都是待处理的,当然也可以通过选单切换到已经审核的内容,比如审核通过的拒绝的内容。提交页面不会有显示。
后台需要账号密码登录,安全起见,需要增加一个验证码的功能;另外为了方便使用,首次登录默认账号/密码为admin/admin当admin成功登陆到后台后提示可以修改账户名称和密码修改完的信息将写入数据库然后折叠修改区域。
整体的样式UI需要参考互联网大厂的设计思路简洁大气现代配色方面需要统一性但需要比较前沿的方向设计。
补充部分因为只有网址信息过于单一可以支持APP、软件的投稿但投递系统不存储图片和软件包用户可以在投稿区域切换APP/软件投稿界面。
APP/软件的投稿表单需要用户手动填写软件名称、系统平台、版本号、图标地址、下载链接、落地页/官网。(关于这个部分你可以思考后补充。)
关于APP/软件的投递后台部分也要分开来显示网址的部分和APP/软件的不能在同一个列表里显示,因为会导致后台审核区域的信息数据混乱。
APP/软件应该是单独的数据表。
数据库默认使用MySQL如果部署的环境配置低可以支持兼容使用SQLite3。
使用开源易部署的方案,我希望整体保持轻量化,我是中学生,所以需要你完成全部的工作。
部署方面,我使用的是宝塔面板部署,所以你要考虑到部署的过程和配置教程等。
最后写一份完整的readme和开源协议。