first commit
This commit is contained in:
commit
d75163b2b4
83
.htaccess
Normal file
83
.htaccess
Normal 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
304
DEPLOY.md
Normal 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
21
LICENSE
Normal 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
261
README.md
Normal 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
BIN
admin/arial.ttf
Normal file
Binary file not shown.
60
admin/captcha.php
Normal file
60
admin/captcha.php
Normal 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
750
admin/index.php
Normal 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
280
admin/login.php
Normal 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
22
admin/logout.php
Normal 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;
|
||||
?>
|
35
api/fetch_website_info.php
Normal file
35
api/fetch_website_info.php
Normal 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
524
assets/css/style.css
Normal 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
612
assets/js/main.js
Normal 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
231
cleanup.php
Normal 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
147
config/database.php
Normal 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
266
includes/utils.php
Normal 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
580
index.php
Normal 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
478
install.php
Normal 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
31
需求说明.md
Normal 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和开源协议。
|
Loading…
x
Reference in New Issue
Block a user