release: v2.1.0 - Prompt管理系统、页脚优化、图标修复
This commit is contained in:
463
DEPLOYMENT.md
Normal file
463
DEPLOYMENT.md
Normal file
@@ -0,0 +1,463 @@
|
|||||||
|
# ZJPB - 焦提示词 部署文档
|
||||||
|
|
||||||
|
## 1Panel部署指南
|
||||||
|
|
||||||
|
### 前置要求
|
||||||
|
- 1Panel管理面板已安装
|
||||||
|
- MySQL 5.7+ 或 MariaDB
|
||||||
|
- Python 3.8+
|
||||||
|
- Nginx(1Panel自带)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、数据库准备
|
||||||
|
|
||||||
|
### 1.1 在1Panel中创建MySQL数据库
|
||||||
|
|
||||||
|
1. 登录1Panel管理面板
|
||||||
|
2. 进入「数据库」菜单
|
||||||
|
3. 点击「创建数据库」
|
||||||
|
4. 填写信息:
|
||||||
|
- 数据库名:`ai_nav`
|
||||||
|
- 用户名:`ai_nav_user`
|
||||||
|
- 密码:自动生成或自定义(记录下来)
|
||||||
|
- 权限:本地访问
|
||||||
|
5. 点击创建
|
||||||
|
|
||||||
|
### 1.2 导入数据库结构
|
||||||
|
|
||||||
|
使用1Panel的phpMyAdmin或命令行导入:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 如果你有数据库备份文件,可以直接导入
|
||||||
|
-- 否则在部署后通过init_db.py初始化
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、上传项目文件
|
||||||
|
|
||||||
|
### 2.1 压缩项目
|
||||||
|
|
||||||
|
在本地Windows环境压缩项目文件夹为 `zjpb.zip`,**排除以下文件/文件夹**:
|
||||||
|
- `__pycache__/`
|
||||||
|
- `*.pyc`
|
||||||
|
- `.git/`
|
||||||
|
- `.env`(生产环境重新配置)
|
||||||
|
- `venv/` 或 `env/`
|
||||||
|
- `test_*.py`(测试文件)
|
||||||
|
|
||||||
|
### 2.2 上传到服务器
|
||||||
|
|
||||||
|
1. 在1Panel中进入「文件」菜单
|
||||||
|
2. 导航到 `/opt/1panel/apps/` 或你的网站目录(如 `/www/wwwroot/`)
|
||||||
|
3. 创建项目目录:`zjpb`
|
||||||
|
4. 上传 `zjpb.zip` 并解压
|
||||||
|
5. 最终路径示例:`/www/wwwroot/zjpb/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、环境配置
|
||||||
|
|
||||||
|
### 3.1 SSH连接到服务器
|
||||||
|
|
||||||
|
使用1Panel的终端或SSH工具连接服务器
|
||||||
|
|
||||||
|
### 3.2 创建Python虚拟环境
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /www/wwwroot/zjpb
|
||||||
|
|
||||||
|
# 创建虚拟环境
|
||||||
|
python3 -m venv venv
|
||||||
|
|
||||||
|
# 激活虚拟环境
|
||||||
|
source venv/bin/activate
|
||||||
|
|
||||||
|
# 升级pip
|
||||||
|
pip install --upgrade pip
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 安装Python依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4 配置环境变量
|
||||||
|
|
||||||
|
创建生产环境的 `.env` 文件:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nano .env
|
||||||
|
```
|
||||||
|
|
||||||
|
填写以下内容(**修改为实际值**):
|
||||||
|
|
||||||
|
```env
|
||||||
|
# 数据库配置
|
||||||
|
DB_HOST=localhost
|
||||||
|
DB_PORT=3306
|
||||||
|
DB_USER=ai_nav_user
|
||||||
|
DB_PASSWORD=你的数据库密码
|
||||||
|
DB_NAME=ai_nav
|
||||||
|
|
||||||
|
# 安全配置(生成随机密钥)
|
||||||
|
SECRET_KEY=your-production-secret-key-change-this
|
||||||
|
|
||||||
|
# 运行环境
|
||||||
|
FLASK_ENV=production
|
||||||
|
|
||||||
|
# DeepSeek API配置(可选)
|
||||||
|
DEEPSEEK_API_KEY=你的DeepSeek_API密钥
|
||||||
|
DEEPSEEK_BASE_URL=https://api.deepseek.com
|
||||||
|
```
|
||||||
|
|
||||||
|
**生成安全的SECRET_KEY**:
|
||||||
|
```bash
|
||||||
|
python3 -c "import secrets; print(secrets.token_hex(32))"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.5 创建必要的目录
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 创建日志目录
|
||||||
|
mkdir -p logs
|
||||||
|
|
||||||
|
# 创建静态文件上传目录
|
||||||
|
mkdir -p static/uploads
|
||||||
|
|
||||||
|
# 设置权限
|
||||||
|
chmod 755 logs static/uploads
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、初始化数据库
|
||||||
|
|
||||||
|
### 4.1 运行初始化脚本
|
||||||
|
|
||||||
|
```bash
|
||||||
|
source venv/bin/activate
|
||||||
|
python init_db.py
|
||||||
|
```
|
||||||
|
|
||||||
|
这会创建所有表并创建默认管理员账号:
|
||||||
|
- 用户名:`admin`
|
||||||
|
- 密码:`admin123`
|
||||||
|
|
||||||
|
**⚠️ 重要:登录后立即修改默认密码!**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、配置Nginx反向代理
|
||||||
|
|
||||||
|
### 5.1 在1Panel中创建网站
|
||||||
|
|
||||||
|
1. 进入1Panel「网站」菜单
|
||||||
|
2. 点击「创建网站」
|
||||||
|
3. 填写信息:
|
||||||
|
- 网站类型:反向代理
|
||||||
|
- 域名:`your-domain.com`(或IP)
|
||||||
|
- 代理地址:`http://127.0.0.1:5000`
|
||||||
|
- 启用SSL:推荐(自动申请Let's Encrypt证书)
|
||||||
|
|
||||||
|
### 5.2 自定义Nginx配置(可选)
|
||||||
|
|
||||||
|
如果需要自定义,编辑Nginx配置:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name your-domain.com;
|
||||||
|
|
||||||
|
# 如果启用SSL,这里会自动重定向到443
|
||||||
|
|
||||||
|
client_max_body_size 10M; # 允许上传文件大小
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:5000;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
# WebSocket支持(如果需要)
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
}
|
||||||
|
|
||||||
|
# 静态文件直接由Nginx处理
|
||||||
|
location /static/ {
|
||||||
|
alias /www/wwwroot/zjpb/static/;
|
||||||
|
expires 30d;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、配置Supervisor守护进程
|
||||||
|
|
||||||
|
### 6.1 在1Panel中使用Supervisor
|
||||||
|
|
||||||
|
1. 进入1Panel「容器」→「应用编排」或「进程管理」
|
||||||
|
2. 创建新的守护进程配置
|
||||||
|
|
||||||
|
### 6.2 创建Supervisor配置文件
|
||||||
|
|
||||||
|
如果1Panel没有内置,手动创建:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nano /etc/supervisor/conf.d/zjpb.conf
|
||||||
|
```
|
||||||
|
|
||||||
|
配置内容:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[program:zjpb]
|
||||||
|
command=/www/wwwroot/zjpb/venv/bin/gunicorn -c gunicorn_config.py app:app
|
||||||
|
directory=/www/wwwroot/zjpb
|
||||||
|
user=www
|
||||||
|
autostart=true
|
||||||
|
autorestart=true
|
||||||
|
redirect_stderr=true
|
||||||
|
stdout_logfile=/www/wwwroot/zjpb/logs/supervisor.log
|
||||||
|
environment=FLASK_ENV="production"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.3 启动服务
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 重新加载Supervisor配置
|
||||||
|
supervisorctl reread
|
||||||
|
supervisorctl update
|
||||||
|
|
||||||
|
# 启动应用
|
||||||
|
supervisorctl start zjpb
|
||||||
|
|
||||||
|
# 查看状态
|
||||||
|
supervisorctl status zjpb
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、使用systemd守护进程(替代方案)
|
||||||
|
|
||||||
|
如果不使用Supervisor,可以用systemd:
|
||||||
|
|
||||||
|
### 7.1 创建systemd服务文件
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo nano /etc/systemd/system/zjpb.service
|
||||||
|
```
|
||||||
|
|
||||||
|
内容:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[Unit]
|
||||||
|
Description=ZJPB AI Navigation Flask Application
|
||||||
|
After=network.target mysql.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=notify
|
||||||
|
User=www
|
||||||
|
Group=www
|
||||||
|
WorkingDirectory=/www/wwwroot/zjpb
|
||||||
|
Environment="PATH=/www/wwwroot/zjpb/venv/bin"
|
||||||
|
Environment="FLASK_ENV=production"
|
||||||
|
ExecStart=/www/wwwroot/zjpb/venv/bin/gunicorn -c gunicorn_config.py app:app
|
||||||
|
Restart=always
|
||||||
|
RestartSec=10
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 启动服务
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 重新加载systemd
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
|
||||||
|
# 启动服务
|
||||||
|
sudo systemctl start zjpb
|
||||||
|
|
||||||
|
# 设置开机自启
|
||||||
|
sudo systemctl enable zjpb
|
||||||
|
|
||||||
|
# 查看状态
|
||||||
|
sudo systemctl status zjpb
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、验证部署
|
||||||
|
|
||||||
|
### 8.1 检查服务状态
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查看gunicorn进程
|
||||||
|
ps aux | grep gunicorn
|
||||||
|
|
||||||
|
# 查看日志
|
||||||
|
tail -f logs/error.log
|
||||||
|
tail -f logs/access.log
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.2 访问网站
|
||||||
|
|
||||||
|
1. 前台访问:`http://your-domain.com`
|
||||||
|
2. 后台访问:`http://your-domain.com/admin/login`
|
||||||
|
- 用户名:`admin`
|
||||||
|
- 密码:`admin123`(首次登录后立即修改)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 九、常用管理命令
|
||||||
|
|
||||||
|
### 9.1 重启应用
|
||||||
|
|
||||||
|
**使用Supervisor:**
|
||||||
|
```bash
|
||||||
|
supervisorctl restart zjpb
|
||||||
|
```
|
||||||
|
|
||||||
|
**使用systemd:**
|
||||||
|
```bash
|
||||||
|
sudo systemctl restart zjpb
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9.2 查看日志
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 应用日志
|
||||||
|
tail -f logs/error.log
|
||||||
|
|
||||||
|
# Nginx访问日志
|
||||||
|
tail -f /www/server/nginx/logs/your-domain.com.log
|
||||||
|
|
||||||
|
# Supervisor日志
|
||||||
|
tail -f logs/supervisor.log
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9.3 更新代码
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /www/wwwroot/zjpb
|
||||||
|
|
||||||
|
# 备份数据库
|
||||||
|
mysqldump -u ai_nav_user -p ai_nav > backup_$(date +%Y%m%d).sql
|
||||||
|
|
||||||
|
# 拉取新代码或上传新文件
|
||||||
|
# ...
|
||||||
|
|
||||||
|
# 激活虚拟环境
|
||||||
|
source venv/bin/activate
|
||||||
|
|
||||||
|
# 安装新依赖
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
# 重启应用
|
||||||
|
supervisorctl restart zjpb
|
||||||
|
# 或
|
||||||
|
sudo systemctl restart zjpb
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十、安全加固
|
||||||
|
|
||||||
|
### 10.1 修改默认管理员密码
|
||||||
|
|
||||||
|
登录后台后立即修改密码:
|
||||||
|
1. 访问:`/admin/change-password`
|
||||||
|
2. 输入旧密码:`admin123`
|
||||||
|
3. 设置新的强密码
|
||||||
|
|
||||||
|
### 10.2 配置防火墙
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 只允许80和443端口(1Panel通常已配置)
|
||||||
|
sudo ufw allow 80/tcp
|
||||||
|
sudo ufw allow 443/tcp
|
||||||
|
sudo ufw enable
|
||||||
|
```
|
||||||
|
|
||||||
|
### 10.3 配置SSL证书
|
||||||
|
|
||||||
|
在1Panel中为网站启用SSL:
|
||||||
|
1. 进入网站设置
|
||||||
|
2. 启用SSL
|
||||||
|
3. 选择Let's Encrypt免费证书
|
||||||
|
4. 自动申请并配置
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十一、故障排查
|
||||||
|
|
||||||
|
### 11.1 应用无法启动
|
||||||
|
|
||||||
|
检查日志:
|
||||||
|
```bash
|
||||||
|
tail -f logs/error.log
|
||||||
|
```
|
||||||
|
|
||||||
|
常见问题:
|
||||||
|
- 数据库连接失败:检查`.env`配置
|
||||||
|
- 端口被占用:修改`gunicorn_config.py`中的端口
|
||||||
|
- 权限问题:确保文件所有者为`www`用户
|
||||||
|
|
||||||
|
### 11.2 静态文件404
|
||||||
|
|
||||||
|
检查Nginx配置和目录权限:
|
||||||
|
```bash
|
||||||
|
ls -la static/
|
||||||
|
chmod -R 755 static/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 11.3 数据库连接失败
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 测试数据库连接
|
||||||
|
mysql -u ai_nav_user -p ai_nav
|
||||||
|
|
||||||
|
# 检查MySQL服务
|
||||||
|
sudo systemctl status mysql
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十二、备份策略
|
||||||
|
|
||||||
|
### 12.1 数据库备份
|
||||||
|
|
||||||
|
创建定时任务:
|
||||||
|
```bash
|
||||||
|
crontab -e
|
||||||
|
```
|
||||||
|
|
||||||
|
添加每日备份:
|
||||||
|
```cron
|
||||||
|
0 2 * * * mysqldump -u ai_nav_user -p密码 ai_nav > /www/backup/zjpb_$(date +\%Y\%m\%d).sql
|
||||||
|
```
|
||||||
|
|
||||||
|
### 12.2 文件备份
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 备份上传的文件
|
||||||
|
tar -czf uploads_backup_$(date +%Y%m%d).tar.gz static/uploads/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 支持与帮助
|
||||||
|
|
||||||
|
如有问题,请检查:
|
||||||
|
1. 应用日志:`logs/error.log`
|
||||||
|
2. Nginx日志:`/www/server/nginx/logs/`
|
||||||
|
3. 系统日志:`journalctl -u zjpb -f`
|
||||||
|
|
||||||
|
祝部署顺利!🚀
|
||||||
132
DEPLOY_CHECKLIST.md
Normal file
132
DEPLOY_CHECKLIST.md
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
# 📦 1Panel部署打包清单
|
||||||
|
|
||||||
|
部署前请确保以下文件都已准备好:
|
||||||
|
|
||||||
|
## ✅ 必需文件
|
||||||
|
|
||||||
|
### 应用文件
|
||||||
|
- [x] app.py - 主应用文件
|
||||||
|
- [x] wsgi.py - WSGI入口(生产环境)
|
||||||
|
- [x] config.py - 配置文件
|
||||||
|
- [x] models.py - 数据模型
|
||||||
|
- [x] init_db.py - 数据库初始化脚本
|
||||||
|
- [x] requirements.txt - Python依赖列表
|
||||||
|
|
||||||
|
### 配置文件
|
||||||
|
- [x] gunicorn_config.py - Gunicorn配置
|
||||||
|
- [x] .env.example - 环境变量模板
|
||||||
|
- [ ] .env - 生产环境变量(需在服务器上创建)
|
||||||
|
|
||||||
|
### 部署脚本
|
||||||
|
- [x] deploy.sh - 一键部署脚本
|
||||||
|
- [x] manage.sh - 应用管理脚本
|
||||||
|
|
||||||
|
### 文档
|
||||||
|
- [x] DEPLOYMENT.md - 完整部署文档
|
||||||
|
- [x] QUICK_DEPLOY.md - 快速部署指南
|
||||||
|
- [x] README.md - 项目说明
|
||||||
|
|
||||||
|
### 目录结构
|
||||||
|
```
|
||||||
|
zjpb/
|
||||||
|
├── static/ # 静态文件
|
||||||
|
│ ├── css/
|
||||||
|
│ ├── js/
|
||||||
|
│ └── uploads/ # 上传文件目录
|
||||||
|
├── templates/ # 模板文件
|
||||||
|
│ ├── admin/
|
||||||
|
│ └── *.html
|
||||||
|
├── utils/ # 工具类
|
||||||
|
│ ├── website_fetcher.py
|
||||||
|
│ ├── tag_generator.py
|
||||||
|
│ └── bookmark_parser.py
|
||||||
|
├── migrations/ # 数据库迁移脚本
|
||||||
|
├── logs/ # 日志目录(自动创建)
|
||||||
|
└── venv/ # 虚拟环境(服务器上创建)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚫 排除文件(不要上传到服务器)
|
||||||
|
|
||||||
|
- `__pycache__/` - Python缓存
|
||||||
|
- `*.pyc` - 编译的Python文件
|
||||||
|
- `.git/` - Git仓库
|
||||||
|
- `.env` - 本地环境变量
|
||||||
|
- `venv/` `env/` - 虚拟环境
|
||||||
|
- `test_*.py` - 测试文件
|
||||||
|
- `*.log` - 日志文件
|
||||||
|
- `logs/` - 日志目录
|
||||||
|
- `static/uploads/*` - 上传的文件
|
||||||
|
|
||||||
|
## 📋 部署前检查清单
|
||||||
|
|
||||||
|
### 本地准备
|
||||||
|
- [ ] 更新 requirements.txt
|
||||||
|
- [ ] 测试应用运行正常
|
||||||
|
- [ ] 准备 .env.example 模板
|
||||||
|
- [ ] 压缩项目文件
|
||||||
|
|
||||||
|
### 服务器准备
|
||||||
|
- [ ] 1Panel已安装
|
||||||
|
- [ ] MySQL数据库已创建
|
||||||
|
- [ ] 域名已解析(可选)
|
||||||
|
- [ ] SSH访问权限
|
||||||
|
|
||||||
|
### 部署步骤
|
||||||
|
- [ ] 上传项目文件到服务器
|
||||||
|
- [ ] 解压并设置目录权限
|
||||||
|
- [ ] 执行 deploy.sh 脚本
|
||||||
|
- [ ] 配置 .env 文件
|
||||||
|
- [ ] 初始化数据库
|
||||||
|
- [ ] 在1Panel中创建网站(反向代理)
|
||||||
|
- [ ] 启动应用
|
||||||
|
- [ ] 配置SSL证书(可选)
|
||||||
|
|
||||||
|
### 部署后验证
|
||||||
|
- [ ] 前台页面访问正常
|
||||||
|
- [ ] 后台登录成功
|
||||||
|
- [ ] 修改默认管理员密码
|
||||||
|
- [ ] 测试网站添加功能
|
||||||
|
- [ ] 测试标签创建功能
|
||||||
|
- [ ] 测试图片上传功能
|
||||||
|
|
||||||
|
## 🔐 安全检查
|
||||||
|
|
||||||
|
- [ ] 修改默认管理员密码
|
||||||
|
- [ ] 设置强密码的 SECRET_KEY
|
||||||
|
- [ ] 配置 .env 权限(chmod 600)
|
||||||
|
- [ ] 启用 SSL/HTTPS
|
||||||
|
- [ ] 配置防火墙规则
|
||||||
|
- [ ] 定期备份数据库
|
||||||
|
|
||||||
|
## 📝 压缩命令
|
||||||
|
|
||||||
|
Windows PowerShell:
|
||||||
|
```powershell
|
||||||
|
Compress-Archive -Path * -DestinationPath zjpb.zip -Force
|
||||||
|
```
|
||||||
|
|
||||||
|
Linux/Mac:
|
||||||
|
```bash
|
||||||
|
zip -r zjpb.zip . -x "*.pyc" "*__pycache__*" "*.git*" ".env" "venv/*" "test_*.py" "logs/*"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 快速部署(服务器上)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 上传并解压
|
||||||
|
cd /www/wwwroot/zjpb
|
||||||
|
unzip zjpb.zip
|
||||||
|
|
||||||
|
# 2. 一键部署
|
||||||
|
chmod +x deploy.sh
|
||||||
|
./deploy.sh
|
||||||
|
|
||||||
|
# 3. 配置环境变量
|
||||||
|
nano .env
|
||||||
|
|
||||||
|
# 4. 启动应用
|
||||||
|
chmod +x manage.sh
|
||||||
|
./manage.sh start
|
||||||
|
```
|
||||||
|
|
||||||
|
详细步骤请参考:`DEPLOYMENT.md`
|
||||||
208
INCREMENTAL_DEPLOY.md
Normal file
208
INCREMENTAL_DEPLOY.md
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
# ZJPB v2.1 增量部署指南
|
||||||
|
|
||||||
|
## 📋 本次更新内容
|
||||||
|
|
||||||
|
### 新增功能
|
||||||
|
1. **Prompt管理系统** - 后台可管理AI提示词模板
|
||||||
|
2. **详情页图标优化** - Material Icons替换为Emoji
|
||||||
|
3. **标签显示修复** - 修复编辑页标签名称无法显示问题
|
||||||
|
4. **页脚优化** - 添加ICP备案号和Microsoft Clarity统计
|
||||||
|
|
||||||
|
### 数据库变更
|
||||||
|
- 新增表:`prompt_templates` (Prompt模板表)
|
||||||
|
- 无现有表结构变更,完全兼容旧数据
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔒 增量部署步骤
|
||||||
|
|
||||||
|
### 第一步:备份生产数据库(必须!)
|
||||||
|
|
||||||
|
在1Panel中备份数据库:
|
||||||
|
```bash
|
||||||
|
# 方法1:使用1Panel面板
|
||||||
|
1. 进入1Panel -> 数据库
|
||||||
|
2. 找到 ai_nav 数据库
|
||||||
|
3. 点击"备份"按钮
|
||||||
|
4. 下载备份文件到本地保存
|
||||||
|
|
||||||
|
# 方法2:使用命令行
|
||||||
|
mysqldump -h 112.124.42.38 -u ai_nav_user -p ai_nav > backup_$(date +%Y%m%d_%H%M%S).sql
|
||||||
|
```
|
||||||
|
|
||||||
|
### 第二步:停止生产应用
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# SSH登录到生产服务器
|
||||||
|
ssh root@your-server-ip
|
||||||
|
|
||||||
|
# 进入应用目录
|
||||||
|
cd /www/wwwroot/zjpb
|
||||||
|
|
||||||
|
# 停止应用
|
||||||
|
./manage.sh stop
|
||||||
|
```
|
||||||
|
|
||||||
|
### 第三步:备份现有代码
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 创建备份目录
|
||||||
|
cd /www/wwwroot
|
||||||
|
cp -r zjpb zjpb_backup_$(date +%Y%m%d_%H%M%S)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 第四步:上传新代码
|
||||||
|
|
||||||
|
**方法1:使用Git(推荐)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 在本地提交所有修改
|
||||||
|
git add .
|
||||||
|
git commit -m "release: v2.1 - Prompt管理系统、页脚优化、图标修复"
|
||||||
|
git push origin master
|
||||||
|
|
||||||
|
# 在服务器上拉取
|
||||||
|
cd /www/wwwroot/zjpb
|
||||||
|
git pull origin master
|
||||||
|
```
|
||||||
|
|
||||||
|
**方法2:手动上传**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 在本地压缩(排除不需要的文件)
|
||||||
|
zip -r zjpb_v2.1.zip . -x "*.pyc" "*__pycache__*" "*.git*" ".env" "venv/*" "test_*.py" "*.db" "nul"
|
||||||
|
|
||||||
|
# 上传到服务器 /www/wwwroot/zjpb_new.zip
|
||||||
|
# 然后解压覆盖
|
||||||
|
cd /www/wwwroot/zjpb
|
||||||
|
unzip -o ../zjpb_new.zip
|
||||||
|
```
|
||||||
|
|
||||||
|
### 第五步:安装新依赖(如有)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /www/wwwroot/zjpb
|
||||||
|
source venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### 第六步:运行数据库迁移
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 激活虚拟环境
|
||||||
|
source venv/bin/activate
|
||||||
|
|
||||||
|
# 运行迁移脚本(创建 prompt_templates 表)
|
||||||
|
python migrate_prompts.py
|
||||||
|
```
|
||||||
|
|
||||||
|
**预期输出:**
|
||||||
|
```
|
||||||
|
正在创建 prompt_templates 表...
|
||||||
|
[OK] 表创建成功
|
||||||
|
正在初始化默认prompt模板...
|
||||||
|
[OK] 默认prompt模板初始化成功
|
||||||
|
- 标签生成: 1
|
||||||
|
- 主要功能生成: 2
|
||||||
|
- 详细介绍生成: 3
|
||||||
|
```
|
||||||
|
|
||||||
|
### 第七步:重启应用
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./manage.sh start
|
||||||
|
```
|
||||||
|
|
||||||
|
### 第八步:验证部署
|
||||||
|
|
||||||
|
**检查项:**
|
||||||
|
|
||||||
|
1. **访问前台首页**
|
||||||
|
- 检查页脚是否显示ICP备案号
|
||||||
|
- 检查Clarity统计是否加载(F12查看Network)
|
||||||
|
|
||||||
|
2. **访问详情页**
|
||||||
|
- 检查图标是否正常显示(不是Material Icons文本)
|
||||||
|
|
||||||
|
3. **登录后台**
|
||||||
|
- 检查是否有"Prompt管理"菜单
|
||||||
|
- 进入Prompt管理,查看是否有3条默认数据
|
||||||
|
|
||||||
|
4. **测试网站编辑**
|
||||||
|
- 编辑任意网站,检查标签是否正常显示(不是空白蓝框)
|
||||||
|
|
||||||
|
5. **测试AI功能**
|
||||||
|
- 创建新网站,测试"AI生成标签"功能
|
||||||
|
- 测试"AI生成详细介绍"功能
|
||||||
|
- 测试"AI生成主要功能"功能
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 回滚方案(如出现问题)
|
||||||
|
|
||||||
|
### 快速回滚代码
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /www/wwwroot
|
||||||
|
./zjpb/manage.sh stop
|
||||||
|
|
||||||
|
# 删除新版本
|
||||||
|
rm -rf zjpb
|
||||||
|
|
||||||
|
# 恢复备份
|
||||||
|
mv zjpb_backup_YYYYMMDD_HHMMSS zjpb
|
||||||
|
|
||||||
|
# 重启
|
||||||
|
cd zjpb
|
||||||
|
./manage.sh start
|
||||||
|
```
|
||||||
|
|
||||||
|
### 恢复数据库
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 如果新表导致问题,可以删除新表
|
||||||
|
mysql -h 112.124.42.38 -u ai_nav_user -p ai_nav
|
||||||
|
|
||||||
|
# 在MySQL中执行
|
||||||
|
DROP TABLE IF EXISTS prompt_templates;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 注意事项
|
||||||
|
|
||||||
|
1. ✅ **本次更新不会影响现有数据** - 只是新增表,不修改现有表
|
||||||
|
2. ✅ **完全向后兼容** - 即使不运行迁移脚本,前台也能正常访问
|
||||||
|
3. ✅ **可以随时回滚** - 保留了完整备份
|
||||||
|
4. ⚠️ **必须备份数据库** - 虽然风险很低,但备份是必须的
|
||||||
|
5. ⚠️ **检查.env配置** - 确保生产环境的.env配置正确
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐛 常见问题
|
||||||
|
|
||||||
|
### Q1: 迁移脚本报错 "表已存在"
|
||||||
|
**A:** 说明之前已经运行过迁移,可以跳过此步骤
|
||||||
|
|
||||||
|
### Q2: Prompt管理菜单看不到
|
||||||
|
**A:** 清除浏览器缓存,重新登录后台
|
||||||
|
|
||||||
|
### Q3: 标签还是显示不出来
|
||||||
|
**A:** 清除浏览器缓存,强制刷新(Ctrl+F5)
|
||||||
|
|
||||||
|
### Q4: Clarity统计没加载
|
||||||
|
**A:** 检查网络是否能访问 clarity.ms,可能被墙
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 技术支持
|
||||||
|
|
||||||
|
如遇问题,请检查日志:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查看应用日志
|
||||||
|
./manage.sh logs
|
||||||
|
|
||||||
|
# 查看错误日志
|
||||||
|
tail -f logs/error.log
|
||||||
|
```
|
||||||
94
QUICK_DEPLOY.md
Normal file
94
QUICK_DEPLOY.md
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
# 1Panel部署快速指南
|
||||||
|
|
||||||
|
## 简化部署步骤
|
||||||
|
|
||||||
|
### 1. 准备工作(本地)
|
||||||
|
|
||||||
|
1. 压缩项目:
|
||||||
|
```bash
|
||||||
|
# 排除不需要的文件
|
||||||
|
zip -r zjpb.zip . -x "*.pyc" "*__pycache__*" "*.git*" ".env" "venv/*" "test_*.py"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 服务器操作
|
||||||
|
|
||||||
|
#### 2.1 上传并解压
|
||||||
|
```bash
|
||||||
|
cd /www/wwwroot
|
||||||
|
mkdir zjpb
|
||||||
|
cd zjpb
|
||||||
|
# 上传zjpb.zip到此目录
|
||||||
|
unzip zjpb.zip
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.2 一键部署脚本
|
||||||
|
```bash
|
||||||
|
# 赋予执行权限
|
||||||
|
chmod +x deploy.sh
|
||||||
|
|
||||||
|
# 执行部署
|
||||||
|
./deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 配置数据库
|
||||||
|
|
||||||
|
在1Panel中创建数据库:
|
||||||
|
- 名称:ai_nav
|
||||||
|
- 用户:ai_nav_user
|
||||||
|
- 密码:(记录下来)
|
||||||
|
|
||||||
|
### 4. 配置环境变量
|
||||||
|
|
||||||
|
编辑 `.env` 文件:
|
||||||
|
```bash
|
||||||
|
nano .env
|
||||||
|
```
|
||||||
|
|
||||||
|
填写数据库信息和密钥。
|
||||||
|
|
||||||
|
### 5. 初始化
|
||||||
|
|
||||||
|
```bash
|
||||||
|
source venv/bin/activate
|
||||||
|
python init_db.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. 启动服务
|
||||||
|
|
||||||
|
**方法1: 使用管理脚本**
|
||||||
|
```bash
|
||||||
|
chmod +x manage.sh
|
||||||
|
./manage.sh start
|
||||||
|
```
|
||||||
|
|
||||||
|
**方法2: 使用1Panel**
|
||||||
|
在1Panel中创建反向代理网站,指向 `http://127.0.0.1:5000`
|
||||||
|
|
||||||
|
### 7. 访问
|
||||||
|
|
||||||
|
- 前台:http://your-domain.com
|
||||||
|
- 后台:http://your-domain.com/admin/login
|
||||||
|
- 默认账号:admin / admin123
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 快速命令参考
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 启动应用
|
||||||
|
./manage.sh start
|
||||||
|
|
||||||
|
# 停止应用
|
||||||
|
./manage.sh stop
|
||||||
|
|
||||||
|
# 重启应用
|
||||||
|
./manage.sh restart
|
||||||
|
|
||||||
|
# 查看状态
|
||||||
|
./manage.sh status
|
||||||
|
|
||||||
|
# 查看日志
|
||||||
|
./manage.sh logs
|
||||||
|
```
|
||||||
|
|
||||||
|
详细部署文档请查看:`DEPLOYMENT.md`
|
||||||
313
app.py
313
app.py
@@ -1,11 +1,12 @@
|
|||||||
import os
|
import os
|
||||||
|
import markdown
|
||||||
from flask import Flask, render_template, redirect, url_for, request, flash, jsonify
|
from flask import Flask, render_template, redirect, url_for, request, flash, jsonify
|
||||||
from flask_login import LoginManager, login_user, logout_user, login_required, current_user
|
from flask_login import LoginManager, login_user, logout_user, login_required, current_user
|
||||||
from flask_admin import Admin, AdminIndexView, expose
|
from flask_admin import Admin, AdminIndexView, expose
|
||||||
from flask_admin.contrib.sqla import ModelView
|
from flask_admin.contrib.sqla import ModelView
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from config import config
|
from config import config
|
||||||
from models import db, Site, Tag, Admin as AdminModel, News
|
from models import db, Site, Tag, Admin as AdminModel, News, site_tags, PromptTemplate
|
||||||
from utils.website_fetcher import WebsiteFetcher
|
from utils.website_fetcher import WebsiteFetcher
|
||||||
from utils.tag_generator import TagGenerator
|
from utils.tag_generator import TagGenerator
|
||||||
|
|
||||||
@@ -19,6 +20,14 @@ def create_app(config_name='default'):
|
|||||||
# 初始化数据库
|
# 初始化数据库
|
||||||
db.init_app(app)
|
db.init_app(app)
|
||||||
|
|
||||||
|
# 添加Markdown过滤器
|
||||||
|
@app.template_filter('markdown')
|
||||||
|
def markdown_filter(text):
|
||||||
|
"""将Markdown文本转换为HTML"""
|
||||||
|
if not text:
|
||||||
|
return ''
|
||||||
|
return markdown.markdown(text, extensions=['nl2br', 'fenced_code'])
|
||||||
|
|
||||||
# 初始化登录管理
|
# 初始化登录管理
|
||||||
login_manager = LoginManager()
|
login_manager = LoginManager()
|
||||||
login_manager.init_app(app)
|
login_manager.init_app(app)
|
||||||
@@ -36,6 +45,22 @@ def create_app(config_name='default'):
|
|||||||
# 获取所有启用的标签
|
# 获取所有启用的标签
|
||||||
tags = Tag.query.order_by(Tag.sort_order.desc(), Tag.id).all()
|
tags = Tag.query.order_by(Tag.sort_order.desc(), Tag.id).all()
|
||||||
|
|
||||||
|
# 优化:使用一次SQL查询统计所有标签的网站数量
|
||||||
|
tag_counts = {}
|
||||||
|
if tags:
|
||||||
|
# 使用JOIN查询一次性获取所有标签的网站数量
|
||||||
|
from sqlalchemy import func
|
||||||
|
counts_query = db.session.query(
|
||||||
|
site_tags.c.tag_id,
|
||||||
|
func.count(site_tags.c.site_id).label('count')
|
||||||
|
).join(
|
||||||
|
Site, site_tags.c.site_id == Site.id
|
||||||
|
).filter(
|
||||||
|
Site.is_active == True
|
||||||
|
).group_by(site_tags.c.tag_id).all()
|
||||||
|
|
||||||
|
tag_counts = {tag_id: count for tag_id, count in counts_query}
|
||||||
|
|
||||||
# 获取筛选参数
|
# 获取筛选参数
|
||||||
tag_slug = request.args.get('tag')
|
tag_slug = request.args.get('tag')
|
||||||
search_query = request.args.get('q', '').strip()
|
search_query = request.args.get('q', '').strip()
|
||||||
@@ -57,7 +82,7 @@ def create_app(config_name='default'):
|
|||||||
pagination = None
|
pagination = None
|
||||||
return render_template('index_new.html', sites=sites, tags=tags,
|
return render_template('index_new.html', sites=sites, tags=tags,
|
||||||
selected_tag=selected_tag, search_query=search_query,
|
selected_tag=selected_tag, search_query=search_query,
|
||||||
pagination=pagination)
|
pagination=pagination, tag_counts=tag_counts)
|
||||||
|
|
||||||
# 搜索功能
|
# 搜索功能
|
||||||
if search_query:
|
if search_query:
|
||||||
@@ -79,7 +104,7 @@ def create_app(config_name='default'):
|
|||||||
|
|
||||||
return render_template('index_new.html', sites=sites, tags=tags,
|
return render_template('index_new.html', sites=sites, tags=tags,
|
||||||
selected_tag=selected_tag, search_query=search_query,
|
selected_tag=selected_tag, search_query=search_query,
|
||||||
pagination=pagination)
|
pagination=pagination, tag_counts=tag_counts)
|
||||||
|
|
||||||
@app.route('/site/<code>')
|
@app.route('/site/<code>')
|
||||||
def site_detail(code):
|
def site_detail(code):
|
||||||
@@ -138,6 +163,51 @@ def create_app(config_name='default'):
|
|||||||
logout_user()
|
logout_user()
|
||||||
return redirect(url_for('index'))
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
|
@app.route('/admin/change-password', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
def change_password():
|
||||||
|
"""修改密码"""
|
||||||
|
if request.method == 'POST':
|
||||||
|
old_password = request.form.get('old_password', '').strip()
|
||||||
|
new_password = request.form.get('new_password', '').strip()
|
||||||
|
confirm_password = request.form.get('confirm_password', '').strip()
|
||||||
|
|
||||||
|
# 验证旧密码
|
||||||
|
if not current_user.check_password(old_password):
|
||||||
|
flash('旧密码错误', 'error')
|
||||||
|
return render_template('admin/change_password.html')
|
||||||
|
|
||||||
|
# 验证新密码
|
||||||
|
if not new_password:
|
||||||
|
flash('新密码不能为空', 'error')
|
||||||
|
return render_template('admin/change_password.html')
|
||||||
|
|
||||||
|
if len(new_password) < 6:
|
||||||
|
flash('新密码长度至少6位', 'error')
|
||||||
|
return render_template('admin/change_password.html')
|
||||||
|
|
||||||
|
if new_password != confirm_password:
|
||||||
|
flash('两次输入的新密码不一致', 'error')
|
||||||
|
return render_template('admin/change_password.html')
|
||||||
|
|
||||||
|
if old_password == new_password:
|
||||||
|
flash('新密码不能与旧密码相同', 'error')
|
||||||
|
return render_template('admin/change_password.html')
|
||||||
|
|
||||||
|
# 修改密码
|
||||||
|
try:
|
||||||
|
current_user.set_password(new_password)
|
||||||
|
db.session.commit()
|
||||||
|
flash('密码修改成功,请重新登录', 'success')
|
||||||
|
logout_user()
|
||||||
|
return redirect(url_for('admin_login'))
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
flash(f'密码修改失败:{str(e)}', 'error')
|
||||||
|
return render_template('admin/change_password.html')
|
||||||
|
|
||||||
|
return render_template('admin/change_password.html')
|
||||||
|
|
||||||
# ========== API路由 ==========
|
# ========== API路由 ==========
|
||||||
@app.route('/api/fetch-website-info', methods=['POST'])
|
@app.route('/api/fetch-website-info', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
@@ -165,17 +235,18 @@ def create_app(config_name='default'):
|
|||||||
'message': '无法获取网站信息,请检查URL是否正确或手动填写'
|
'message': '无法获取网站信息,请检查URL是否正确或手动填写'
|
||||||
})
|
})
|
||||||
|
|
||||||
# 下载Logo(如果有)
|
# 下载Logo到本地(如果有)
|
||||||
logo_path = None
|
logo_path = None
|
||||||
if info.get('logo_url'):
|
if info.get('logo_url'):
|
||||||
logo_path = fetcher.download_logo(info['logo_url'])
|
logo_path = fetcher.download_logo(info['logo_url'], save_dir='static/logos')
|
||||||
|
|
||||||
|
# 如果下载失败,不返回远程URL,让用户手动上传
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': True,
|
'success': True,
|
||||||
'data': {
|
'data': {
|
||||||
'name': info.get('name', ''),
|
'name': info.get('name', ''),
|
||||||
'description': info.get('description', ''),
|
'description': info.get('description', ''),
|
||||||
'logo': logo_path or info.get('logo_url', '')
|
'logo': logo_path if logo_path else ''
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -185,6 +256,148 @@ def create_app(config_name='default'):
|
|||||||
'message': f'抓取失败: {str(e)}'
|
'message': f'抓取失败: {str(e)}'
|
||||||
}), 500
|
}), 500
|
||||||
|
|
||||||
|
@app.route('/api/upload-logo', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def upload_logo():
|
||||||
|
"""上传Logo图片API"""
|
||||||
|
try:
|
||||||
|
# 检查文件是否存在
|
||||||
|
if 'logo' not in request.files:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': '请选择要上传的图片'
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
file = request.files['logo']
|
||||||
|
|
||||||
|
# 检查文件名
|
||||||
|
if file.filename == '':
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': '未选择文件'
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
# 检查文件类型
|
||||||
|
allowed_extensions = {'png', 'jpg', 'jpeg', 'gif', 'svg', 'ico', 'webp'}
|
||||||
|
filename = file.filename.lower()
|
||||||
|
if not any(filename.endswith('.' + ext) for ext in allowed_extensions):
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': '不支持的文件格式,请上传图片文件(png, jpg, gif, svg, ico, webp)'
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
# 创建保存目录
|
||||||
|
save_dir = 'static/logos'
|
||||||
|
os.makedirs(save_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# 生成安全的文件名
|
||||||
|
import time
|
||||||
|
import hashlib
|
||||||
|
ext = os.path.splitext(filename)[1]
|
||||||
|
timestamp = str(int(time.time() * 1000))
|
||||||
|
hash_name = hashlib.md5(f"{filename}{timestamp}".encode()).hexdigest()[:16]
|
||||||
|
safe_filename = f"logo_{hash_name}{ext}"
|
||||||
|
filepath = os.path.join(save_dir, safe_filename)
|
||||||
|
|
||||||
|
# 保存文件
|
||||||
|
file.save(filepath)
|
||||||
|
|
||||||
|
# 返回相对路径
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'path': f'/{filepath.replace(os.sep, "/")}'
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': f'上传失败: {str(e)}'
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
@app.route('/api/generate-features', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def generate_features():
|
||||||
|
"""使用DeepSeek自动生成网站主要功能"""
|
||||||
|
try:
|
||||||
|
data = request.get_json()
|
||||||
|
name = data.get('name', '').strip()
|
||||||
|
description = data.get('description', '').strip()
|
||||||
|
url = data.get('url', '').strip()
|
||||||
|
|
||||||
|
if not name or not description:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': '请提供网站名称和描述'
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
# 生成功能列表
|
||||||
|
generator = TagGenerator()
|
||||||
|
features = generator.generate_features(name, description, url)
|
||||||
|
|
||||||
|
if not features:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': 'DeepSeek功能生成失败,请检查API配置'
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'features': features
|
||||||
|
})
|
||||||
|
|
||||||
|
except ValueError as e:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': str(e)
|
||||||
|
}), 400
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': f'生成失败: {str(e)}'
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
@app.route('/api/generate-description', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def generate_description():
|
||||||
|
"""使用DeepSeek自动生成网站详细介绍"""
|
||||||
|
try:
|
||||||
|
data = request.get_json()
|
||||||
|
name = data.get('name', '').strip()
|
||||||
|
short_desc = data.get('short_desc', '').strip()
|
||||||
|
url = data.get('url', '').strip()
|
||||||
|
|
||||||
|
if not name:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': '请提供网站名称'
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
# 生成详细介绍
|
||||||
|
generator = TagGenerator()
|
||||||
|
description = generator.generate_description(name, short_desc, url)
|
||||||
|
|
||||||
|
if not description:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': 'DeepSeek详细介绍生成失败,请检查API配置'
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'description': description
|
||||||
|
})
|
||||||
|
|
||||||
|
except ValueError as e:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': str(e)
|
||||||
|
}), 400
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': f'生成失败: {str(e)}'
|
||||||
|
}), 500
|
||||||
|
|
||||||
@app.route('/api/generate-tags', methods=['POST'])
|
@app.route('/api/generate-tags', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def generate_tags():
|
def generate_tags():
|
||||||
@@ -345,11 +558,11 @@ def create_app(config_name='default'):
|
|||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 4. 下载Logo(失败不影响导入)
|
# 4. 下载Logo到本地(失败不影响导入)
|
||||||
logo_path = None
|
logo_path = None
|
||||||
if info.get('logo_url'):
|
if info.get('logo_url'):
|
||||||
try:
|
try:
|
||||||
logo_path = fetcher.download_logo(info['logo_url'])
|
logo_path = fetcher.download_logo(info['logo_url'], save_dir='static/logos')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"下载Logo失败 ({url}): {str(e)}")
|
print(f"下载Logo失败 ({url}): {str(e)}")
|
||||||
# Logo下载失败不影响网站导入
|
# Logo下载失败不影响网站导入
|
||||||
@@ -544,9 +757,47 @@ def create_app(config_name='default'):
|
|||||||
import re
|
import re
|
||||||
import random
|
import random
|
||||||
from pypinyin import lazy_pinyin
|
from pypinyin import lazy_pinyin
|
||||||
|
from flask import request
|
||||||
|
|
||||||
# 使用no_autoflush防止在查询时触发提前flush
|
# 使用no_autoflush防止在查询时触发提前flush
|
||||||
with db.session.no_autoflush:
|
with db.session.no_autoflush:
|
||||||
|
# 处理手动输入的新标签
|
||||||
|
new_tags_str = request.form.get('new_tags', '')
|
||||||
|
if new_tags_str:
|
||||||
|
new_tag_names = [name.strip() for name in new_tags_str.split(',') if name.strip()]
|
||||||
|
for tag_name in new_tag_names:
|
||||||
|
# 检查标签是否已存在
|
||||||
|
existing_tag = Tag.query.filter_by(name=tag_name).first()
|
||||||
|
if not existing_tag:
|
||||||
|
# 创建新标签
|
||||||
|
tag_slug = ''.join(lazy_pinyin(tag_name))
|
||||||
|
tag_slug = tag_slug.lower()
|
||||||
|
tag_slug = re.sub(r'[^\w\s-]', '', tag_slug)
|
||||||
|
tag_slug = re.sub(r'[-\s]+', '-', tag_slug).strip('-')
|
||||||
|
|
||||||
|
# 确保slug唯一
|
||||||
|
base_tag_slug = tag_slug[:50]
|
||||||
|
counter = 1
|
||||||
|
final_tag_slug = tag_slug
|
||||||
|
while Tag.query.filter_by(slug=final_tag_slug).first():
|
||||||
|
final_tag_slug = f"{base_tag_slug}-{counter}"
|
||||||
|
counter += 1
|
||||||
|
if counter > 100:
|
||||||
|
final_tag_slug = f"{base_tag_slug}-{random.randint(1000, 9999)}"
|
||||||
|
break
|
||||||
|
|
||||||
|
new_tag = Tag(name=tag_name, slug=final_tag_slug)
|
||||||
|
db.session.add(new_tag)
|
||||||
|
db.session.flush() # 确保新标签有ID
|
||||||
|
|
||||||
|
# 添加到模型的标签列表
|
||||||
|
if new_tag not in model.tags:
|
||||||
|
model.tags.append(new_tag)
|
||||||
|
else:
|
||||||
|
# 添加已存在的标签
|
||||||
|
if existing_tag not in model.tags:
|
||||||
|
model.tags.append(existing_tag)
|
||||||
|
|
||||||
# 如果code为空,自动生成唯一的8位数字编码
|
# 如果code为空,自动生成唯一的8位数字编码
|
||||||
if not model.code or model.code.strip() == '':
|
if not model.code or model.code.strip() == '':
|
||||||
max_attempts = 10
|
max_attempts = 10
|
||||||
@@ -684,6 +935,51 @@ def create_app(config_name='default'):
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Prompt模板管理视图
|
||||||
|
class PromptAdmin(SecureModelView):
|
||||||
|
can_edit = True
|
||||||
|
can_delete = False # 不允许删除,避免系统必需的prompt被删除
|
||||||
|
can_create = True
|
||||||
|
can_view_details = False
|
||||||
|
|
||||||
|
# 显示操作列
|
||||||
|
column_display_actions = True
|
||||||
|
|
||||||
|
column_list = ['id', 'key', 'name', 'description', 'is_active', 'updated_at']
|
||||||
|
column_searchable_list = ['key', 'name', 'description']
|
||||||
|
column_filters = ['is_active', 'key']
|
||||||
|
column_labels = {
|
||||||
|
'id': 'ID',
|
||||||
|
'key': '唯一标识',
|
||||||
|
'name': '模板名称',
|
||||||
|
'system_prompt': '系统提示词',
|
||||||
|
'user_prompt_template': '用户提示词模板',
|
||||||
|
'description': '模板说明',
|
||||||
|
'is_active': '是否启用',
|
||||||
|
'created_at': '创建时间',
|
||||||
|
'updated_at': '更新时间'
|
||||||
|
}
|
||||||
|
form_columns = ['key', 'name', 'description', 'system_prompt', 'user_prompt_template', 'is_active']
|
||||||
|
|
||||||
|
# 字段说明
|
||||||
|
column_descriptions = {
|
||||||
|
'key': '唯一标识,如: tags, features, description',
|
||||||
|
'system_prompt': 'AI的系统角色设定',
|
||||||
|
'user_prompt_template': '用户提示词模板,支持变量如 {name}, {description}, {url}',
|
||||||
|
}
|
||||||
|
|
||||||
|
# 表单字段配置
|
||||||
|
form_widget_args = {
|
||||||
|
'system_prompt': {
|
||||||
|
'rows': 3,
|
||||||
|
'style': 'font-family: monospace;'
|
||||||
|
},
|
||||||
|
'user_prompt_template': {
|
||||||
|
'rows': 20,
|
||||||
|
'style': 'font-family: monospace;'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# 初始化 Flask-Admin
|
# 初始化 Flask-Admin
|
||||||
admin = Admin(
|
admin = Admin(
|
||||||
app,
|
app,
|
||||||
@@ -696,6 +992,7 @@ def create_app(config_name='default'):
|
|||||||
admin.add_view(SiteAdmin(Site, db.session, name='网站管理'))
|
admin.add_view(SiteAdmin(Site, db.session, name='网站管理'))
|
||||||
admin.add_view(TagAdmin(Tag, db.session, name='标签管理'))
|
admin.add_view(TagAdmin(Tag, db.session, name='标签管理'))
|
||||||
admin.add_view(NewsAdmin(News, db.session, name='新闻管理'))
|
admin.add_view(NewsAdmin(News, db.session, name='新闻管理'))
|
||||||
|
admin.add_view(PromptAdmin(PromptTemplate, db.session, name='Prompt管理'))
|
||||||
admin.add_view(AdminAdmin(AdminModel, db.session, name='管理员', endpoint='admin_users'))
|
admin.add_view(AdminAdmin(AdminModel, db.session, name='管理员', endpoint='admin_users'))
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|||||||
96
deploy.sh
Normal file
96
deploy.sh
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# ZJPB 一键部署脚本
|
||||||
|
# 用于1Panel环境快速部署
|
||||||
|
|
||||||
|
echo "================================"
|
||||||
|
echo "ZJPB - 焦提示词 部署脚本"
|
||||||
|
echo "================================"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 检查Python版本
|
||||||
|
python_version=$(python3 --version 2>&1 | awk '{print $2}')
|
||||||
|
echo "✓ Python版本: $python_version"
|
||||||
|
|
||||||
|
# 创建必要目录
|
||||||
|
echo "创建必要目录..."
|
||||||
|
mkdir -p logs
|
||||||
|
mkdir -p static/uploads
|
||||||
|
echo "✓ 目录创建完成"
|
||||||
|
|
||||||
|
# 创建虚拟环境
|
||||||
|
if [ ! -d "venv" ]; then
|
||||||
|
echo "创建Python虚拟环境..."
|
||||||
|
python3 -m venv venv
|
||||||
|
echo "✓ 虚拟环境创建完成"
|
||||||
|
else
|
||||||
|
echo "✓ 虚拟环境已存在"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 激活虚拟环境
|
||||||
|
source venv/bin/activate
|
||||||
|
|
||||||
|
# 升级pip
|
||||||
|
echo "升级pip..."
|
||||||
|
pip install --upgrade pip -q
|
||||||
|
|
||||||
|
# 安装依赖
|
||||||
|
echo "安装Python依赖包..."
|
||||||
|
pip install -r requirements.txt -q
|
||||||
|
echo "✓ 依赖包安装完成"
|
||||||
|
|
||||||
|
# 检查.env文件
|
||||||
|
if [ ! -f ".env" ]; then
|
||||||
|
echo ""
|
||||||
|
echo "⚠️ 警告: .env文件不存在"
|
||||||
|
echo "请从.env.example复制并配置:"
|
||||||
|
echo " cp .env.example .env"
|
||||||
|
echo " nano .env"
|
||||||
|
echo ""
|
||||||
|
read -p "是否现在创建.env文件? (y/n) " -n 1 -r
|
||||||
|
echo
|
||||||
|
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||||
|
cp .env.example .env
|
||||||
|
echo "✓ .env文件已创建,请编辑配置"
|
||||||
|
nano .env
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "✓ .env配置文件存在"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 询问是否初始化数据库
|
||||||
|
echo ""
|
||||||
|
read -p "是否需要初始化数据库? (首次部署选择y) (y/n) " -n 1 -r
|
||||||
|
echo
|
||||||
|
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||||
|
echo "初始化数据库..."
|
||||||
|
python init_db.py
|
||||||
|
echo "✓ 数据库初始化完成"
|
||||||
|
echo ""
|
||||||
|
echo "默认管理员账号:"
|
||||||
|
echo " 用户名: admin"
|
||||||
|
echo " 密码: admin123"
|
||||||
|
echo " ⚠️ 请登录后立即修改密码!"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 设置权限
|
||||||
|
echo ""
|
||||||
|
echo "设置目录权限..."
|
||||||
|
chmod 755 logs static/uploads
|
||||||
|
chmod +x manage.sh
|
||||||
|
echo "✓ 权限设置完成"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "================================"
|
||||||
|
echo "部署完成!"
|
||||||
|
echo "================================"
|
||||||
|
echo ""
|
||||||
|
echo "后续步骤:"
|
||||||
|
echo "1. 确保已在1Panel中创建MySQL数据库"
|
||||||
|
echo "2. 确保.env文件配置正确"
|
||||||
|
echo "3. 在1Panel中创建反向代理网站,指向: http://127.0.0.1:5000"
|
||||||
|
echo "4. 启动应用: ./manage.sh start"
|
||||||
|
echo "5. 访问网站并修改默认密码"
|
||||||
|
echo ""
|
||||||
|
echo "详细文档: 查看 DEPLOYMENT.md"
|
||||||
|
echo ""
|
||||||
58
export_data.py
Normal file
58
export_data.py
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
数据导出脚本
|
||||||
|
导出现有数据到SQL文件
|
||||||
|
"""
|
||||||
|
import subprocess
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
def export_database():
|
||||||
|
"""导出数据库到SQL文件"""
|
||||||
|
# 从.env读取数据库配置
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
db_host = os.getenv('DB_HOST', 'localhost')
|
||||||
|
db_port = os.getenv('DB_PORT', '3306')
|
||||||
|
db_user = os.getenv('DB_USER')
|
||||||
|
db_password = os.getenv('DB_PASSWORD')
|
||||||
|
db_name = os.getenv('DB_NAME')
|
||||||
|
|
||||||
|
# 生成备份文件名
|
||||||
|
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||||
|
backup_file = f'backup_{db_name}_{timestamp}.sql'
|
||||||
|
|
||||||
|
# mysqldump命令
|
||||||
|
cmd = [
|
||||||
|
'mysqldump',
|
||||||
|
'-h', db_host,
|
||||||
|
'-P', db_port,
|
||||||
|
'-u', db_user,
|
||||||
|
f'-p{db_password}',
|
||||||
|
'--single-transaction',
|
||||||
|
'--routines',
|
||||||
|
'--triggers',
|
||||||
|
db_name
|
||||||
|
]
|
||||||
|
|
||||||
|
print(f"正在导出数据库 {db_name} ...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(backup_file, 'w', encoding='utf-8') as f:
|
||||||
|
subprocess.run(cmd, stdout=f, stderr=subprocess.PIPE, check=True)
|
||||||
|
|
||||||
|
file_size = os.path.getsize(backup_file) / 1024 # KB
|
||||||
|
print(f"✓ 数据库导出成功!")
|
||||||
|
print(f" 文件: {backup_file}")
|
||||||
|
print(f" 大小: {file_size:.2f} KB")
|
||||||
|
print(f"\n请将此文件上传到服务器进行恢复")
|
||||||
|
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(f"✗ 导出失败: {e.stderr.decode()}")
|
||||||
|
except FileNotFoundError:
|
||||||
|
print("✗ 错误: 找不到 mysqldump 命令")
|
||||||
|
print(" 请确保MySQL客户端工具已安装并在PATH中")
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
export_database()
|
||||||
93
git_patch_deploy.sh
Normal file
93
git_patch_deploy.sh
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ZJPB v2.1 Git Patch 部署脚本
|
||||||
|
# 在生产服务器上执行
|
||||||
|
|
||||||
|
echo "================================"
|
||||||
|
echo "ZJPB v2.1 Git Patch 部署"
|
||||||
|
echo "================================"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 项目路径
|
||||||
|
PROJECT_DIR="/opt/1panel/apps/zjpb"
|
||||||
|
|
||||||
|
# 检查是否在正确目录
|
||||||
|
cd $PROJECT_DIR || { echo "❌ 项目目录不存在"; exit 1; }
|
||||||
|
|
||||||
|
echo "当前目录: $(pwd)"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 停止应用
|
||||||
|
echo "1. 停止应用..."
|
||||||
|
./manage.sh stop
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# 检查Git状态
|
||||||
|
echo "2. 检查Git状态..."
|
||||||
|
git status
|
||||||
|
|
||||||
|
# 备份未提交的修改(如果有)
|
||||||
|
echo "3. 备份当前修改(如有)..."
|
||||||
|
if ! git diff-index --quiet HEAD --; then
|
||||||
|
echo " 发现未提交的修改,正在保存..."
|
||||||
|
git stash save "backup_before_v2.1_$(date +%Y%m%d_%H%M%S)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 应用patch
|
||||||
|
echo "4. 应用v2.1.0补丁..."
|
||||||
|
if [ -f "v2.1.0.patch" ]; then
|
||||||
|
git apply --check v2.1.0.patch
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
git apply v2.1.0.patch
|
||||||
|
echo " ✅ 补丁应用成功"
|
||||||
|
else
|
||||||
|
echo " ❌ 补丁应用失败,请检查"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo " ❌ v2.1.0.patch 文件不存在"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 提交更改
|
||||||
|
echo "5. 提交更改到Git..."
|
||||||
|
git add .
|
||||||
|
git commit -m "release: v2.1.0 - Prompt管理系统、页脚优化、图标修复
|
||||||
|
|
||||||
|
通过patch部署,包含以下更新:
|
||||||
|
- Prompt管理系统
|
||||||
|
- 页脚ICP备案和统计代码
|
||||||
|
- 详情页图标修复
|
||||||
|
- 标签显示修复
|
||||||
|
"
|
||||||
|
|
||||||
|
# 激活虚拟环境
|
||||||
|
echo "6. 激活虚拟环境..."
|
||||||
|
source venv/bin/activate
|
||||||
|
|
||||||
|
# 安装依赖
|
||||||
|
echo "7. 检查依赖..."
|
||||||
|
pip install -r requirements.txt -q
|
||||||
|
|
||||||
|
# 运行数据库迁移
|
||||||
|
echo "8. 运行数据库迁移..."
|
||||||
|
python migrate_prompts.py
|
||||||
|
|
||||||
|
# 重启应用
|
||||||
|
echo "9. 重启应用..."
|
||||||
|
./manage.sh start
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
# 检查状态
|
||||||
|
echo "10. 检查应用状态..."
|
||||||
|
./manage.sh status
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "================================"
|
||||||
|
echo "✅ 部署完成!"
|
||||||
|
echo "================================"
|
||||||
|
echo ""
|
||||||
|
echo "Git提交历史:"
|
||||||
|
git log --oneline -3
|
||||||
|
echo ""
|
||||||
|
echo "请访问网站验证更新是否成功"
|
||||||
|
echo ""
|
||||||
44
gunicorn_config.py
Normal file
44
gunicorn_config.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# Gunicorn配置文件
|
||||||
|
import os
|
||||||
|
|
||||||
|
# 绑定地址和端口
|
||||||
|
bind = "0.0.0.0:5000"
|
||||||
|
|
||||||
|
# 工作进程数 (推荐: CPU核心数 * 2 + 1)
|
||||||
|
workers = 4
|
||||||
|
|
||||||
|
# 工作线程数
|
||||||
|
threads = 2
|
||||||
|
|
||||||
|
# 工作模式
|
||||||
|
worker_class = "sync"
|
||||||
|
|
||||||
|
# 超时时间(秒)
|
||||||
|
timeout = 120
|
||||||
|
|
||||||
|
# 访问日志
|
||||||
|
accesslog = "logs/access.log"
|
||||||
|
|
||||||
|
# 错误日志
|
||||||
|
errorlog = "logs/error.log"
|
||||||
|
|
||||||
|
# 日志级别
|
||||||
|
loglevel = "info"
|
||||||
|
|
||||||
|
# 进程名称
|
||||||
|
proc_name = "zjpb_app"
|
||||||
|
|
||||||
|
# 守护进程模式(设置为False,由systemd或supervisor管理)
|
||||||
|
daemon = False
|
||||||
|
|
||||||
|
# PID文件
|
||||||
|
pidfile = "logs/gunicorn.pid"
|
||||||
|
|
||||||
|
# 预加载应用(提高性能)
|
||||||
|
preload_app = True
|
||||||
|
|
||||||
|
# 优雅重启超时
|
||||||
|
graceful_timeout = 30
|
||||||
|
|
||||||
|
# 保持连接
|
||||||
|
keepalive = 5
|
||||||
61
manage.sh
Normal file
61
manage.sh
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# ZJPB 应用管理脚本
|
||||||
|
# 用法: ./manage.sh [start|stop|restart|status|logs]
|
||||||
|
|
||||||
|
APP_NAME="zjpb"
|
||||||
|
APP_DIR="/www/wwwroot/zjpb"
|
||||||
|
VENV_DIR="$APP_DIR/venv"
|
||||||
|
PID_FILE="$APP_DIR/logs/gunicorn.pid"
|
||||||
|
|
||||||
|
cd $APP_DIR
|
||||||
|
|
||||||
|
case "$1" in
|
||||||
|
start)
|
||||||
|
echo "启动 $APP_NAME..."
|
||||||
|
source $VENV_DIR/bin/activate
|
||||||
|
gunicorn -c gunicorn_config.py app:app
|
||||||
|
echo "$APP_NAME 已启动"
|
||||||
|
;;
|
||||||
|
|
||||||
|
stop)
|
||||||
|
echo "停止 $APP_NAME..."
|
||||||
|
if [ -f $PID_FILE ]; then
|
||||||
|
kill $(cat $PID_FILE)
|
||||||
|
echo "$APP_NAME 已停止"
|
||||||
|
else
|
||||||
|
echo "PID文件不存在,可能未运行"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
|
||||||
|
restart)
|
||||||
|
$0 stop
|
||||||
|
sleep 2
|
||||||
|
$0 start
|
||||||
|
;;
|
||||||
|
|
||||||
|
status)
|
||||||
|
if [ -f $PID_FILE ]; then
|
||||||
|
PID=$(cat $PID_FILE)
|
||||||
|
if ps -p $PID > /dev/null; then
|
||||||
|
echo "$APP_NAME 正在运行 (PID: $PID)"
|
||||||
|
else
|
||||||
|
echo "$APP_NAME 未运行(但PID文件存在)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "$APP_NAME 未运行"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
|
||||||
|
logs)
|
||||||
|
echo "实时查看日志(Ctrl+C退出):"
|
||||||
|
tail -f logs/error.log
|
||||||
|
;;
|
||||||
|
|
||||||
|
*)
|
||||||
|
echo "用法: $0 {start|stop|restart|status|logs}"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
exit 0
|
||||||
49
migrate_db.py
Normal file
49
migrate_db.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
数据库安全迁移脚本
|
||||||
|
只创建缺失的表,不删除现有数据
|
||||||
|
用于生产环境部署
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from app import create_app
|
||||||
|
from models import db, Admin
|
||||||
|
|
||||||
|
def migrate_database():
|
||||||
|
"""安全迁移数据库(不删除数据)"""
|
||||||
|
app = create_app('production')
|
||||||
|
|
||||||
|
with app.app_context():
|
||||||
|
print("正在检查数据库表...")
|
||||||
|
|
||||||
|
# 只创建不存在的表(不会删除数据)
|
||||||
|
db.create_all()
|
||||||
|
print("✓ 数据库表检查完成")
|
||||||
|
|
||||||
|
# 检查是否存在管理员
|
||||||
|
admin_count = Admin.query.count()
|
||||||
|
|
||||||
|
if admin_count == 0:
|
||||||
|
print("\n未找到管理员账号,正在创建默认管理员...")
|
||||||
|
admin = Admin(
|
||||||
|
username='admin',
|
||||||
|
email='admin@example.com',
|
||||||
|
is_active=True
|
||||||
|
)
|
||||||
|
admin.set_password('admin123')
|
||||||
|
db.session.add(admin)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
print("✓ 默认管理员创建成功")
|
||||||
|
print(" 用户名: admin")
|
||||||
|
print(" 密码: admin123")
|
||||||
|
print("\n⚠️ 请立即登录后台修改密码!")
|
||||||
|
else:
|
||||||
|
print(f"\n✓ 已存在 {admin_count} 个管理员账号")
|
||||||
|
|
||||||
|
print("\n" + "="*50)
|
||||||
|
print("数据库迁移完成!")
|
||||||
|
print("="*50)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
migrate_database()
|
||||||
141
migrate_prompts.py
Normal file
141
migrate_prompts.py
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
"""创建 prompt_templates 表并初始化默认数据"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from app import create_app
|
||||||
|
from models import db, PromptTemplate
|
||||||
|
|
||||||
|
# 设置输出编码为UTF-8
|
||||||
|
if sys.platform.startswith('win'):
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8')
|
||||||
|
|
||||||
|
def migrate_prompts():
|
||||||
|
"""创建表并初始化默认prompt模板"""
|
||||||
|
app = create_app(os.getenv('FLASK_ENV', 'development'))
|
||||||
|
|
||||||
|
with app.app_context():
|
||||||
|
# 创建表
|
||||||
|
print("正在创建 prompt_templates 表...")
|
||||||
|
db.create_all()
|
||||||
|
print("[OK] 表创建成功")
|
||||||
|
|
||||||
|
# 检查是否已有数据
|
||||||
|
existing_count = PromptTemplate.query.count()
|
||||||
|
if existing_count > 0:
|
||||||
|
print(f"[WARN] 表中已有 {existing_count} 条数据,跳过初始化")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 初始化默认prompt模板
|
||||||
|
print("正在初始化默认prompt模板...")
|
||||||
|
|
||||||
|
# 1. 标签生成模板
|
||||||
|
tags_prompt = PromptTemplate(
|
||||||
|
key='tags',
|
||||||
|
name='标签生成',
|
||||||
|
description='根据网站名称和描述生成3-5个分类标签',
|
||||||
|
system_prompt='你是一个专业的AI工具分类专家,擅长为各类AI产品生成准确的标签。',
|
||||||
|
user_prompt_template='''你是一个AI工具导航网站的标签生成助手。根据以下产品信息,生成3-5个最合适的标签。
|
||||||
|
|
||||||
|
产品名称: {name}
|
||||||
|
|
||||||
|
产品描述: {description}
|
||||||
|
{existing_tags}
|
||||||
|
|
||||||
|
要求:
|
||||||
|
1. 标签应该准确描述产品的功能、类型或应用场景
|
||||||
|
2. 每个标签2-4个汉字
|
||||||
|
3. 标签要具体且有区分度
|
||||||
|
4. 如果是AI工具,可以标注具体的AI类型(如"GPT"、"图像生成"等)
|
||||||
|
5. 只返回标签,用逗号分隔,不要其他说明
|
||||||
|
|
||||||
|
示例输出格式:写作助手,营销,GPT,内容生成
|
||||||
|
|
||||||
|
请生成标签:''',
|
||||||
|
is_active=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2. 主要功能生成模板
|
||||||
|
features_prompt = PromptTemplate(
|
||||||
|
key='features',
|
||||||
|
name='主要功能生成',
|
||||||
|
description='根据网站名称和描述生成5-8个主要功能点',
|
||||||
|
system_prompt='你是一个专业的AI产品文案专家,擅长提炼产品核心功能和价值点。',
|
||||||
|
user_prompt_template='''你是一个AI工具导航网站的内容编辑助手。根据以下产品信息,生成详细的主要功能列表。
|
||||||
|
|
||||||
|
产品名称: {name}
|
||||||
|
|
||||||
|
产品描述: {description}
|
||||||
|
{url_info}
|
||||||
|
|
||||||
|
要求:
|
||||||
|
1. 生成5-8个主要功能点
|
||||||
|
2. 每个功能点要具体、清晰、有吸引力
|
||||||
|
3. 使用Markdown无序列表格式(以"- "开头)
|
||||||
|
4. 每个功能点一行,简洁有力(10-30字)
|
||||||
|
5. 突出产品的核心价值和特色功能
|
||||||
|
6. 使用专业但易懂的语言
|
||||||
|
7. 不要添加任何标题或额外说明,直接输出功能列表
|
||||||
|
|
||||||
|
示例输出格式:
|
||||||
|
- 智能文本生成,支持多种写作场景
|
||||||
|
- 实时语法检查和优化建议
|
||||||
|
- 多语言翻译,准确率高达95%
|
||||||
|
- 一键生成营销文案和广告语
|
||||||
|
- 团队协作,支持多人同时编辑
|
||||||
|
|
||||||
|
请生成功能列表:''',
|
||||||
|
is_active=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# 3. 详细介绍生成模板
|
||||||
|
description_prompt = PromptTemplate(
|
||||||
|
key='description',
|
||||||
|
name='详细介绍生成',
|
||||||
|
description='根据网站名称和简短描述生成200-400字的详细介绍',
|
||||||
|
system_prompt='你是一个专业的AI产品文案专家,擅长撰写准确、客观、有吸引力的产品介绍。',
|
||||||
|
user_prompt_template='''你是一个AI工具导航网站的内容编辑助手。根据以下产品信息,生成详细、专业且吸引人的产品介绍。
|
||||||
|
|
||||||
|
产品名称: {name}
|
||||||
|
{short_desc_info}
|
||||||
|
{url_info}
|
||||||
|
|
||||||
|
要求:
|
||||||
|
1. 生成200-400字的详细介绍
|
||||||
|
2. 包含以下内容:
|
||||||
|
- 产品定位和核心价值(这是什么产品,解决什么问题)
|
||||||
|
- 主要特点和优势(为什么选择这个产品)
|
||||||
|
- 适用场景和目标用户(谁会用,用在哪里)
|
||||||
|
3. 使用Markdown格式,可以包含:
|
||||||
|
- 段落分隔(空行)
|
||||||
|
- 加粗重点内容(**文字**)
|
||||||
|
- 列表(- 列表项)
|
||||||
|
4. 语言专业但易懂,突出产品价值
|
||||||
|
5. 不要添加标题,直接输出正文内容
|
||||||
|
6. 语气客观、事实性强,避免过度营销
|
||||||
|
|
||||||
|
示例输出格式:
|
||||||
|
ChatGPT是由OpenAI开发的**先进对话式AI助手**,基于GPT-4大语言模型构建。它能够理解和生成自然语言,为用户提供智能对话、内容创作、代码编写等多种服务。
|
||||||
|
|
||||||
|
**核心优势:**
|
||||||
|
- 强大的语言理解和生成能力
|
||||||
|
- 支持多轮对话,上下文连贯
|
||||||
|
- 覆盖编程、写作、翻译等多个领域
|
||||||
|
|
||||||
|
适用于内容创作者、程序员、学生等各类用户,可用于日常问答、文案撰写、学习辅导、编程助手等多种场景。
|
||||||
|
|
||||||
|
请生成详细介绍:''',
|
||||||
|
is_active=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# 添加到数据库
|
||||||
|
db.session.add(tags_prompt)
|
||||||
|
db.session.add(features_prompt)
|
||||||
|
db.session.add(description_prompt)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
print("[OK] 默认prompt模板初始化成功")
|
||||||
|
print(f" - 标签生成: {tags_prompt.id}")
|
||||||
|
print(f" - 主要功能生成: {features_prompt.id}")
|
||||||
|
print(f" - 详细介绍生成: {description_prompt.id}")
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
migrate_prompts()
|
||||||
32
models.py
32
models.py
@@ -136,3 +136,35 @@ class Admin(UserMixin, db.Model):
|
|||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'<Admin {self.username}>'
|
return f'<Admin {self.username}>'
|
||||||
|
|
||||||
|
class PromptTemplate(db.Model):
|
||||||
|
"""AI提示词模板模型"""
|
||||||
|
__tablename__ = 'prompt_templates'
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
key = db.Column(db.String(50), unique=True, nullable=False, comment='唯一标识(tags/features/description)')
|
||||||
|
name = db.Column(db.String(100), nullable=False, comment='模板名称')
|
||||||
|
system_prompt = db.Column(db.Text, nullable=False, comment='系统提示词')
|
||||||
|
user_prompt_template = db.Column(db.Text, nullable=False, comment='用户提示词模板(支持变量)')
|
||||||
|
description = db.Column(db.String(200), comment='模板说明')
|
||||||
|
is_active = db.Column(db.Boolean, default=True, comment='是否启用')
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.now, comment='创建时间')
|
||||||
|
updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now, comment='更新时间')
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<PromptTemplate {self.name}>'
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
"""转换为字典"""
|
||||||
|
return {
|
||||||
|
'id': self.id,
|
||||||
|
'key': self.key,
|
||||||
|
'name': self.name,
|
||||||
|
'system_prompt': self.system_prompt,
|
||||||
|
'user_prompt_template': self.user_prompt_template,
|
||||||
|
'description': self.description,
|
||||||
|
'is_active': self.is_active,
|
||||||
|
'created_at': self.created_at.strftime('%Y-%m-%d %H:%M:%S') if self.created_at else None,
|
||||||
|
'updated_at': self.updated_at.strftime('%Y-%m-%d %H:%M:%S') if self.updated_at else None
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
2
nul
Normal file
2
nul
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
timeout: invalid time interval ‘/t’
|
||||||
|
Try 'timeout --help' for more information.
|
||||||
121
one_click_deploy.sh
Normal file
121
one_click_deploy.sh
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ZJPB v2.1 一键部署命令集合
|
||||||
|
# 复制整个脚本到服务器终端执行即可
|
||||||
|
|
||||||
|
set -e # 遇到错误立即停止
|
||||||
|
|
||||||
|
echo "================================"
|
||||||
|
echo "ZJPB v2.1 一键部署"
|
||||||
|
echo "================================"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 进入项目目录
|
||||||
|
cd /opt/1panel/apps/zjpb
|
||||||
|
|
||||||
|
# 显示当前位置
|
||||||
|
echo "📂 当前目录: $(pwd)"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 停止应用
|
||||||
|
echo "⏸️ 停止应用..."
|
||||||
|
./manage.sh stop 2>/dev/null || echo "应用未运行"
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# 检查Git状态
|
||||||
|
echo ""
|
||||||
|
echo "🔍 检查Git状态..."
|
||||||
|
git status --short
|
||||||
|
|
||||||
|
# 备份未提交的修改
|
||||||
|
echo ""
|
||||||
|
echo "💾 备份当前修改..."
|
||||||
|
if ! git diff-index --quiet HEAD -- 2>/dev/null; then
|
||||||
|
git stash save "backup_before_v2.1_$(date +%Y%m%d_%H%M%S)"
|
||||||
|
echo " ✅ 已保存未提交的修改"
|
||||||
|
else
|
||||||
|
echo " ℹ️ 无需备份"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 检查patch文件
|
||||||
|
echo ""
|
||||||
|
echo "📦 检查补丁文件..."
|
||||||
|
if [ ! -f "v2.1.0.patch" ]; then
|
||||||
|
echo " ❌ v2.1.0.patch 不存在,请先上传"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo " ✅ v2.1.0.patch 存在"
|
||||||
|
|
||||||
|
# 应用patch
|
||||||
|
echo ""
|
||||||
|
echo "🔧 应用v2.1.0补丁..."
|
||||||
|
git apply --check v2.1.0.patch 2>&1
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
git apply v2.1.0.patch
|
||||||
|
echo " ✅ 补丁应用成功"
|
||||||
|
else
|
||||||
|
echo " ❌ 补丁应用失败"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 提交更改
|
||||||
|
echo ""
|
||||||
|
echo "📝 提交更改到Git..."
|
||||||
|
git add .
|
||||||
|
git commit -m "release: v2.1.0 - Prompt管理系统、页脚优化、图标修复
|
||||||
|
|
||||||
|
部署时间: $(date '+%Y-%m-%d %H:%M:%S')
|
||||||
|
部署方式: Git Patch
|
||||||
|
|
||||||
|
更新内容:
|
||||||
|
- 新增Prompt管理系统
|
||||||
|
- 页脚添加ICP备案号和统计代码
|
||||||
|
- 详情页图标优化(Material Icons → Emoji)
|
||||||
|
- 修复标签显示问题
|
||||||
|
" 2>&1
|
||||||
|
|
||||||
|
# 激活虚拟环境
|
||||||
|
echo ""
|
||||||
|
echo "🐍 激活虚拟环境..."
|
||||||
|
source venv/bin/activate
|
||||||
|
|
||||||
|
# 安装依赖
|
||||||
|
echo ""
|
||||||
|
echo "📚 检查依赖..."
|
||||||
|
pip install -r requirements.txt -q
|
||||||
|
|
||||||
|
# 运行数据库迁移
|
||||||
|
echo ""
|
||||||
|
echo "🗄️ 运行数据库迁移..."
|
||||||
|
python migrate_prompts.py
|
||||||
|
|
||||||
|
# 重启应用
|
||||||
|
echo ""
|
||||||
|
echo "🚀 重启应用..."
|
||||||
|
./manage.sh start
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
# 检查状态
|
||||||
|
echo ""
|
||||||
|
echo "✅ 检查应用状态..."
|
||||||
|
./manage.sh status
|
||||||
|
|
||||||
|
# 显示Git历史
|
||||||
|
echo ""
|
||||||
|
echo "📜 Git提交历史(最近3条):"
|
||||||
|
git log --oneline -3
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "================================"
|
||||||
|
echo "🎉 部署完成!"
|
||||||
|
echo "================================"
|
||||||
|
echo ""
|
||||||
|
echo "📋 验证清单:"
|
||||||
|
echo " 1. 访问首页,检查页脚ICP备案号"
|
||||||
|
echo " 2. 访问详情页,检查图标是否为emoji"
|
||||||
|
echo " 3. 登录后台,检查Prompt管理菜单"
|
||||||
|
echo " 4. 编辑网站,检查标签显示是否正常"
|
||||||
|
echo " 5. 测试AI生成功能"
|
||||||
|
echo ""
|
||||||
|
echo "🔧 查看日志: ./manage.sh logs"
|
||||||
|
echo "📊 查看状态: ./manage.sh status"
|
||||||
|
echo ""
|
||||||
370
templates/admin/change_password.html
Normal file
370
templates/admin/change_password.html
Normal file
@@ -0,0 +1,370 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>修改密码 - ZJPB 焦提示词</title>
|
||||||
|
|
||||||
|
<!-- Google Fonts -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=Noto+Sans:wght@400;500;700&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- Google Material Symbols -->
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" />
|
||||||
|
|
||||||
|
<!-- Bootstrap CSS -->
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- Custom Admin Theme -->
|
||||||
|
<link href="{{ url_for('static', filename='css/admin-sidebar.css') }}" rel="stylesheet">
|
||||||
|
<link href="{{ url_for('static', filename='css/admin-actions.css') }}" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body class="admin-sidebar-layout">
|
||||||
|
<!-- 左侧菜单栏 -->
|
||||||
|
<aside class="admin-sidebar">
|
||||||
|
<!-- Logo -->
|
||||||
|
<div class="sidebar-logo">
|
||||||
|
<span class="material-symbols-outlined logo-icon">blur_on</span>
|
||||||
|
<span class="logo-text">ZJPB 焦提示词</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 主菜单 -->
|
||||||
|
<nav class="sidebar-nav">
|
||||||
|
<div class="nav-section">
|
||||||
|
<div class="nav-section-title">主菜单</div>
|
||||||
|
<ul class="nav-menu">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="{{ url_for('admin.index') }}" class="nav-link">
|
||||||
|
<span class="material-symbols-outlined nav-icon">dashboard</span>
|
||||||
|
<span class="nav-text">控制台</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="{{ url_for('site.index_view') }}" class="nav-link">
|
||||||
|
<span class="material-symbols-outlined nav-icon">public</span>
|
||||||
|
<span class="nav-text">网站管理</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="{{ url_for('tag.index_view') }}" class="nav-link">
|
||||||
|
<span class="material-symbols-outlined nav-icon">label</span>
|
||||||
|
<span class="nav-text">标签管理</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="{{ url_for('news.index_view') }}" class="nav-link">
|
||||||
|
<span class="material-symbols-outlined nav-icon">newspaper</span>
|
||||||
|
<span class="nav-text">新闻管理</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="{{ url_for('admin_users.index_view') }}" class="nav-link">
|
||||||
|
<span class="material-symbols-outlined nav-icon">admin_panel_settings</span>
|
||||||
|
<span class="nav-text">管理员</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 系统菜单 -->
|
||||||
|
<div class="nav-section">
|
||||||
|
<div class="nav-section-title">系统</div>
|
||||||
|
<ul class="nav-menu">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="{{ url_for('batch_import') }}" class="nav-link">
|
||||||
|
<span class="material-symbols-outlined nav-icon">upload_file</span>
|
||||||
|
<span class="nav-text">批量导入</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item active">
|
||||||
|
<a href="{{ url_for('change_password') }}" class="nav-link">
|
||||||
|
<span class="material-symbols-outlined nav-icon">lock_reset</span>
|
||||||
|
<span class="nav-text">修改密码</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="{{ url_for('index') }}" class="nav-link" target="_blank">
|
||||||
|
<span class="material-symbols-outlined nav-icon">open_in_new</span>
|
||||||
|
<span class="nav-text">查看网站</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="{{ url_for('admin_logout') }}" class="nav-link">
|
||||||
|
<span class="material-symbols-outlined nav-icon">logout</span>
|
||||||
|
<span class="nav-text">退出登录</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- 用户信息 -->
|
||||||
|
<div class="sidebar-user">
|
||||||
|
<div class="user-avatar">
|
||||||
|
<span class="material-symbols-outlined">account_circle</span>
|
||||||
|
</div>
|
||||||
|
<div class="user-info">
|
||||||
|
<div class="user-name">{{ current_user.username }}</div>
|
||||||
|
<div class="user-email">{{ current_user.email or 'admin@zjpb.com' }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- 右侧主内容区 -->
|
||||||
|
<div class="admin-main">
|
||||||
|
<!-- 顶部导航栏 -->
|
||||||
|
<header class="admin-header">
|
||||||
|
<div class="header-breadcrumb">
|
||||||
|
<a href="{{ url_for('admin.index') }}" class="breadcrumb-link">控制台</a>
|
||||||
|
<span class="breadcrumb-separator">/</span>
|
||||||
|
<span class="breadcrumb-current">修改密码</span>
|
||||||
|
</div>
|
||||||
|
<div class="header-actions">
|
||||||
|
<div class="search-box">
|
||||||
|
<span class="material-symbols-outlined search-icon">search</span>
|
||||||
|
<input type="text" placeholder="全局搜索..." class="search-input">
|
||||||
|
</div>
|
||||||
|
<button class="header-btn">
|
||||||
|
<span class="material-symbols-outlined">notifications</span>
|
||||||
|
</button>
|
||||||
|
<button class="header-btn">
|
||||||
|
<span class="material-symbols-outlined">settings</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- 页面内容 -->
|
||||||
|
<main class="admin-content">
|
||||||
|
<div class="page-header">
|
||||||
|
<div>
|
||||||
|
<h1 class="page-title">修改密码</h1>
|
||||||
|
<p class="page-description">修改您的管理员账户登录密码</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Flash Messages -->
|
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% if messages %}
|
||||||
|
{% for category, message in messages %}
|
||||||
|
<div class="alert alert-{{ 'danger' if category == 'error' else category }} alert-dismissible fade show" role="alert">
|
||||||
|
<span class="material-symbols-outlined" style="font-size: 18px; vertical-align: middle; margin-right: 8px;">
|
||||||
|
{% if category == 'error' %}error{% else %}check_circle{% endif %}
|
||||||
|
</span>
|
||||||
|
{{ message }}
|
||||||
|
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8 col-lg-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="POST" action="{{ url_for('change_password') }}">
|
||||||
|
<!-- 旧密码 -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="old_password">旧密码</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<span class="input-group-text">
|
||||||
|
<span class="material-symbols-outlined" style="font-size: 20px;">lock</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<input type="password"
|
||||||
|
class="form-control"
|
||||||
|
id="old_password"
|
||||||
|
name="old_password"
|
||||||
|
placeholder="请输入旧密码"
|
||||||
|
required
|
||||||
|
autofocus>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 新密码 -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="new_password">新密码</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<span class="input-group-text">
|
||||||
|
<span class="material-symbols-outlined" style="font-size: 20px;">lock_reset</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<input type="password"
|
||||||
|
class="form-control"
|
||||||
|
id="new_password"
|
||||||
|
name="new_password"
|
||||||
|
placeholder="请输入新密码(至少6位)"
|
||||||
|
required
|
||||||
|
minlength="6">
|
||||||
|
</div>
|
||||||
|
<small class="form-text text-muted">密码长度至少6位</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 确认新密码 -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="confirm_password">确认新密码</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<span class="input-group-text">
|
||||||
|
<span class="material-symbols-outlined" style="font-size: 20px;">check_circle</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<input type="password"
|
||||||
|
class="form-control"
|
||||||
|
id="confirm_password"
|
||||||
|
name="confirm_password"
|
||||||
|
placeholder="请再次输入新密码"
|
||||||
|
required
|
||||||
|
minlength="6">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 提交按钮 -->
|
||||||
|
<div class="form-group mb-0 mt-4">
|
||||||
|
<button type="submit" class="btn btn-primary btn-block">
|
||||||
|
<span class="material-symbols-outlined" style="font-size: 18px; vertical-align: middle; margin-right: 4px;">save</span>
|
||||||
|
确认修改
|
||||||
|
</button>
|
||||||
|
<a href="{{ url_for('admin.index') }}" class="btn btn-secondary btn-block mt-2">
|
||||||
|
<span class="material-symbols-outlined" style="font-size: 18px; vertical-align: middle; margin-right: 4px;">cancel</span>
|
||||||
|
取消
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 安全提示 -->
|
||||||
|
<div class="alert alert-info mt-3">
|
||||||
|
<span class="material-symbols-outlined" style="font-size: 18px; vertical-align: middle; margin-right: 8px;">info</span>
|
||||||
|
<strong>安全提示:</strong>
|
||||||
|
<ul class="mb-0 mt-2" style="padding-left: 20px;">
|
||||||
|
<li>密码修改成功后,您将被自动登出,需要使用新密码重新登录</li>
|
||||||
|
<li>请妥善保管您的密码,不要与他人分享</li>
|
||||||
|
<li>建议定期修改密码以保证账号安全</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bootstrap JS -->
|
||||||
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.page-title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-description {
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
border: 1px solid #DCDFE6;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, .05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body {
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
color: #000000;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group-text {
|
||||||
|
background: #F5F7FA;
|
||||||
|
border: 1px solid #DCDFE6;
|
||||||
|
border-right: none;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control {
|
||||||
|
border: 1px solid #DCDFE6;
|
||||||
|
border-radius: 0 4px 4px 0;
|
||||||
|
padding: 10px 12px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:focus {
|
||||||
|
border-color: #0052D9;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: #0052D9;
|
||||||
|
border-color: #0052D9;
|
||||||
|
padding: 10px 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: #003FA3;
|
||||||
|
border-color: #003FA3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: #F5F7FA;
|
||||||
|
border-color: #DCDFE6;
|
||||||
|
color: #606266;
|
||||||
|
padding: 10px 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: #E6E8EB;
|
||||||
|
border-color: #C0C4CC;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-info {
|
||||||
|
background: #ECF5FF;
|
||||||
|
border-color: #B3D8FF;
|
||||||
|
color: #0052D9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-danger {
|
||||||
|
background: #FEF0F0;
|
||||||
|
border-color: #FBC4C4;
|
||||||
|
color: #D54941;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-success {
|
||||||
|
background: #F0F9FF;
|
||||||
|
border-color: #C1E7C1;
|
||||||
|
color: #00A870;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// 验证两次密码是否一致
|
||||||
|
document.querySelector('form').addEventListener('submit', function(e) {
|
||||||
|
const newPassword = document.getElementById('new_password').value;
|
||||||
|
const confirmPassword = document.getElementById('confirm_password').value;
|
||||||
|
|
||||||
|
if (newPassword !== confirmPassword) {
|
||||||
|
e.preventDefault();
|
||||||
|
alert('两次输入的新密码不一致,请重新输入');
|
||||||
|
document.getElementById('confirm_password').focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -11,6 +11,28 @@
|
|||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
}
|
}
|
||||||
|
.generate-features-btn {
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
.upload-logo-btn {
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
.logo-preview {
|
||||||
|
margin-top: 10px;
|
||||||
|
max-width: 200px;
|
||||||
|
max-height: 200px;
|
||||||
|
display: none;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
.logo-preview img {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 150px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
.fetch-status {
|
.fetch-status {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
@@ -47,6 +69,26 @@
|
|||||||
from { transform: rotate(0deg); }
|
from { transform: rotate(0deg); }
|
||||||
to { transform: rotate(360deg); }
|
to { transform: rotate(360deg); }
|
||||||
}
|
}
|
||||||
|
/* 标签输入框样式 */
|
||||||
|
.tag-input-wrapper {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.tag-input-field {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #DCDFE6;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.tag-input-field:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #0052D9;
|
||||||
|
}
|
||||||
|
.tag-input-help {
|
||||||
|
margin-top: 5px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -130,9 +172,238 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 在标签字段后添加"AI生成标签"按钮
|
// 在Logo字段后添加"上传Logo"功能
|
||||||
const tagsField = document.querySelector('select[name="tags"]');
|
const logoField = document.querySelector('input[name="logo"]');
|
||||||
if (tagsField) {
|
if (logoField) {
|
||||||
|
// 创建文件输入框
|
||||||
|
const fileInput = document.createElement('input');
|
||||||
|
fileInput.type = 'file';
|
||||||
|
fileInput.accept = 'image/*';
|
||||||
|
fileInput.style.display = 'none';
|
||||||
|
|
||||||
|
// 创建上传按钮
|
||||||
|
const uploadBtn = document.createElement('button');
|
||||||
|
uploadBtn.type = 'button';
|
||||||
|
uploadBtn.className = 'btn btn-warning upload-logo-btn';
|
||||||
|
uploadBtn.innerHTML = '📁 上传Logo图片';
|
||||||
|
|
||||||
|
// 创建预览容器
|
||||||
|
const previewDiv = document.createElement('div');
|
||||||
|
previewDiv.className = 'logo-preview';
|
||||||
|
previewDiv.innerHTML = '<img src="" alt="Logo预览"><p style="margin-top:5px; font-size:12px; color:#666;">Logo预览</p>';
|
||||||
|
|
||||||
|
logoField.parentNode.appendChild(fileInput);
|
||||||
|
logoField.parentNode.appendChild(uploadBtn);
|
||||||
|
logoField.parentNode.appendChild(previewDiv);
|
||||||
|
|
||||||
|
// 点击按钮触发文件选择
|
||||||
|
uploadBtn.addEventListener('click', function() {
|
||||||
|
fileInput.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 文件选择后自动上传
|
||||||
|
fileInput.addEventListener('change', function() {
|
||||||
|
const file = fileInput.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
// 验证文件类型
|
||||||
|
if (!file.type.startsWith('image/')) {
|
||||||
|
alert('请选择图片文件!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证文件大小(限制5MB)
|
||||||
|
if (file.size > 5 * 1024 * 1024) {
|
||||||
|
alert('图片文件不能超过5MB!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传文件
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('logo', file);
|
||||||
|
|
||||||
|
uploadBtn.disabled = true;
|
||||||
|
uploadBtn.textContent = '上传中...';
|
||||||
|
|
||||||
|
fetch('/api/upload-logo', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
// 设置Logo字段值
|
||||||
|
logoField.value = data.path;
|
||||||
|
|
||||||
|
// 显示预览
|
||||||
|
const img = previewDiv.querySelector('img');
|
||||||
|
img.src = data.path;
|
||||||
|
previewDiv.style.display = 'block';
|
||||||
|
|
||||||
|
alert('✓ Logo上传成功!');
|
||||||
|
} else {
|
||||||
|
alert('✗ ' + (data.message || '上传失败'));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
alert('✗ 上传失败,请重试');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
uploadBtn.disabled = false;
|
||||||
|
uploadBtn.innerHTML = '📁 上传Logo图片';
|
||||||
|
fileInput.value = '';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 如果Logo字段有值,显示预览
|
||||||
|
if (logoField.value) {
|
||||||
|
const img = previewDiv.querySelector('img');
|
||||||
|
img.src = logoField.value;
|
||||||
|
previewDiv.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听Logo字段变化,更新预览
|
||||||
|
logoField.addEventListener('input', function() {
|
||||||
|
if (logoField.value) {
|
||||||
|
const img = previewDiv.querySelector('img');
|
||||||
|
img.src = logoField.value;
|
||||||
|
previewDiv.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
previewDiv.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理标签字段 - 添加手动输入功能
|
||||||
|
const tagsSelect = document.querySelector('select[name="tags"]');
|
||||||
|
if (tagsSelect) {
|
||||||
|
// 隐藏原始的select字段
|
||||||
|
tagsSelect.style.display = 'none';
|
||||||
|
|
||||||
|
// 创建文本输入框
|
||||||
|
const tagInputWrapper = document.createElement('div');
|
||||||
|
tagInputWrapper.className = 'tag-input-wrapper';
|
||||||
|
|
||||||
|
const tagInput = document.createElement('input');
|
||||||
|
tagInput.type = 'text';
|
||||||
|
tagInput.className = 'tag-input-field';
|
||||||
|
tagInput.placeholder = '输入标签名称,按回车添加(如:AI工具、图像处理、免费)';
|
||||||
|
|
||||||
|
const tagHelpText = document.createElement('div');
|
||||||
|
tagHelpText.className = 'tag-input-help';
|
||||||
|
tagHelpText.textContent = '💡 提示:输入标签名称后按回车键添加,可以添加多个标签。已选标签会自动添加到下方列表。';
|
||||||
|
|
||||||
|
const selectedTagsDiv = document.createElement('div');
|
||||||
|
selectedTagsDiv.className = 'selected-tags';
|
||||||
|
selectedTagsDiv.style.marginTop = '10px';
|
||||||
|
|
||||||
|
tagInputWrapper.appendChild(tagInput);
|
||||||
|
tagInputWrapper.appendChild(tagHelpText);
|
||||||
|
tagInputWrapper.appendChild(selectedTagsDiv);
|
||||||
|
tagsSelect.parentNode.insertBefore(tagInputWrapper, tagsSelect.nextSibling);
|
||||||
|
|
||||||
|
// 显示已选标签
|
||||||
|
function updateSelectedTags() {
|
||||||
|
selectedTagsDiv.innerHTML = '';
|
||||||
|
const selectedOptions = Array.from(tagsSelect.selectedOptions);
|
||||||
|
|
||||||
|
// 如果没有选中的标签,显示提示
|
||||||
|
if (selectedOptions.length === 0) {
|
||||||
|
selectedTagsDiv.innerHTML = '<span style="color:#999; font-size:12px;">暂无已选标签</span>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedOptions.forEach(option => {
|
||||||
|
const tag = document.createElement('span');
|
||||||
|
tag.style.cssText = 'display:inline-block; background:#0052D9; color:white; padding:4px 10px; margin:4px; border-radius:4px; font-size:12px;';
|
||||||
|
|
||||||
|
// 获取标签文本 - 兼容多种方式
|
||||||
|
let tagText = option.textContent || option.innerText || option.text || option.innerHTML || option.label || `标签${option.value}`;
|
||||||
|
|
||||||
|
// 处理 <Tag XXX> 格式,提取出实际标签名称
|
||||||
|
const match = tagText.match(/<Tag\s+(.+?)>/);
|
||||||
|
if (match) {
|
||||||
|
tagText = match[1]; // 提取标签名称
|
||||||
|
}
|
||||||
|
|
||||||
|
tag.innerHTML = tagText + ' <span style="cursor:pointer; margin-left:5px; font-weight:bold;" data-tag-id="' + option.value + '">×</span>';
|
||||||
|
selectedTagsDiv.appendChild(tag);
|
||||||
|
|
||||||
|
// 为删除按钮添加点击事件
|
||||||
|
const deleteBtn = tag.querySelector('span[data-tag-id]');
|
||||||
|
if (deleteBtn) {
|
||||||
|
deleteBtn.addEventListener('click', function(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
option.selected = false;
|
||||||
|
updateSelectedTags();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加标签
|
||||||
|
tagInput.addEventListener('keypress', function(e) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
const tagName = tagInput.value.trim();
|
||||||
|
|
||||||
|
if (!tagName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查标签是否已存在
|
||||||
|
let existingOption = null;
|
||||||
|
for (let option of tagsSelect.options) {
|
||||||
|
if (option.text.toLowerCase() === tagName.toLowerCase()) {
|
||||||
|
existingOption = option;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingOption) {
|
||||||
|
// 选中已存在的标签
|
||||||
|
existingOption.selected = true;
|
||||||
|
} else {
|
||||||
|
// 创建新标签选项(使用负数ID表示新标签)
|
||||||
|
const newOption = document.createElement('option');
|
||||||
|
newOption.value = 'new_' + Date.now();
|
||||||
|
newOption.text = tagName;
|
||||||
|
newOption.selected = true;
|
||||||
|
newOption.setAttribute('data-new-tag', 'true');
|
||||||
|
tagsSelect.appendChild(newOption);
|
||||||
|
}
|
||||||
|
|
||||||
|
tagInput.value = '';
|
||||||
|
updateSelectedTags();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 初始化显示
|
||||||
|
updateSelectedTags();
|
||||||
|
|
||||||
|
// 表单提交时处理新标签
|
||||||
|
const form = tagsSelect.closest('form');
|
||||||
|
form.addEventListener('submit', function(e) {
|
||||||
|
// 收集所有新标签名称
|
||||||
|
const newTags = [];
|
||||||
|
Array.from(tagsSelect.options).forEach(option => {
|
||||||
|
if (option.selected && option.hasAttribute('data-new-tag')) {
|
||||||
|
newTags.push(option.text);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 如果有新标签,添加到隐藏字段
|
||||||
|
if (newTags.length > 0) {
|
||||||
|
const hiddenInput = document.createElement('input');
|
||||||
|
hiddenInput.type = 'hidden';
|
||||||
|
hiddenInput.name = 'new_tags';
|
||||||
|
hiddenInput.value = newTags.join(',');
|
||||||
|
form.appendChild(hiddenInput);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 在标签字段后添加"AI生成标签"按钮
|
||||||
const generateBtn = document.createElement('button');
|
const generateBtn = document.createElement('button');
|
||||||
generateBtn.type = 'button';
|
generateBtn.type = 'button';
|
||||||
generateBtn.className = 'btn btn-success generate-tags-btn';
|
generateBtn.className = 'btn btn-success generate-tags-btn';
|
||||||
@@ -141,8 +412,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
const tagsStatusDiv = document.createElement('div');
|
const tagsStatusDiv = document.createElement('div');
|
||||||
tagsStatusDiv.className = 'tags-status';
|
tagsStatusDiv.className = 'tags-status';
|
||||||
|
|
||||||
tagsField.parentNode.appendChild(generateBtn);
|
tagInputWrapper.appendChild(generateBtn);
|
||||||
tagsField.parentNode.appendChild(tagsStatusDiv);
|
tagInputWrapper.appendChild(tagsStatusDiv);
|
||||||
|
|
||||||
generateBtn.addEventListener('click', function() {
|
generateBtn.addEventListener('click', function() {
|
||||||
const nameField = document.querySelector('input[name="name"]');
|
const nameField = document.querySelector('input[name="name"]');
|
||||||
@@ -175,9 +446,31 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.success && data.tags && data.tags.length > 0) {
|
if (data.success && data.tags && data.tags.length > 0) {
|
||||||
// 显示生成的标签
|
// 自动添加生成的标签
|
||||||
const tagsText = data.tags.join(', ');
|
data.tags.forEach(tagName => {
|
||||||
showTagsStatus('✓ AI生成的标签建议: ' + tagsText + '\n(请在标签字段中手动选择或创建这些标签)', 'success');
|
// 检查是否已存在
|
||||||
|
let exists = false;
|
||||||
|
for (let option of tagsSelect.options) {
|
||||||
|
if (option.text.toLowerCase() === tagName.toLowerCase()) {
|
||||||
|
option.selected = true;
|
||||||
|
exists = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果不存在,创建新标签
|
||||||
|
if (!exists) {
|
||||||
|
const newOption = document.createElement('option');
|
||||||
|
newOption.value = 'new_' + Date.now() + '_' + Math.random();
|
||||||
|
newOption.text = tagName;
|
||||||
|
newOption.selected = true;
|
||||||
|
newOption.setAttribute('data-new-tag', 'true');
|
||||||
|
tagsSelect.appendChild(newOption);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
updateSelectedTags();
|
||||||
|
showTagsStatus('✓ AI已自动添加推荐标签:' + data.tags.join(', '), 'success');
|
||||||
} else {
|
} else {
|
||||||
showTagsStatus('✗ ' + (data.message || '标签生成失败'), 'error');
|
showTagsStatus('✗ ' + (data.message || '标签生成失败'), 'error');
|
||||||
}
|
}
|
||||||
@@ -198,6 +491,152 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
tagsStatusDiv.style.display = 'block';
|
tagsStatusDiv.style.display = 'block';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 在Description字段后添加"AI生成详细介绍"按钮
|
||||||
|
const descriptionField = document.querySelector('textarea[name="description"]');
|
||||||
|
if (descriptionField) {
|
||||||
|
// 创建生成按钮
|
||||||
|
const generateDescBtn = document.createElement('button');
|
||||||
|
generateDescBtn.type = 'button';
|
||||||
|
generateDescBtn.className = 'btn btn-success generate-features-btn';
|
||||||
|
generateDescBtn.innerHTML = '<span class="normal-icon">✨</span><span class="loading-icon">↻</span> AI生成详细介绍';
|
||||||
|
|
||||||
|
const descStatusDiv = document.createElement('div');
|
||||||
|
descStatusDiv.className = 'tags-status';
|
||||||
|
|
||||||
|
descriptionField.parentNode.appendChild(generateDescBtn);
|
||||||
|
descriptionField.parentNode.appendChild(descStatusDiv);
|
||||||
|
|
||||||
|
generateDescBtn.addEventListener('click', function() {
|
||||||
|
const nameField = document.querySelector('input[name="name"]');
|
||||||
|
const shortDescField = document.querySelector('input[name="short_desc"]');
|
||||||
|
const urlField = document.querySelector('input[name="url"]');
|
||||||
|
|
||||||
|
const name = nameField ? nameField.value.trim() : '';
|
||||||
|
const shortDesc = shortDescField ? shortDescField.value.trim() : '';
|
||||||
|
const url = urlField ? urlField.value.trim() : '';
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
showDescStatus('请先填写网站名称', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示加载状态
|
||||||
|
generateDescBtn.disabled = true;
|
||||||
|
generateDescBtn.classList.add('loading');
|
||||||
|
descStatusDiv.style.display = 'none';
|
||||||
|
|
||||||
|
// 调用API生成详细介绍
|
||||||
|
fetch('/api/generate-description', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: name,
|
||||||
|
short_desc: shortDesc,
|
||||||
|
url: url
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success && data.description) {
|
||||||
|
// 自动填充到description字段
|
||||||
|
descriptionField.value = data.description;
|
||||||
|
showDescStatus('✓ AI已生成详细介绍', 'success');
|
||||||
|
} else {
|
||||||
|
showDescStatus('✗ ' + (data.message || '详细介绍生成失败'), 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
showDescStatus('✗ 网络请求失败,请重试', 'error');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
generateDescBtn.disabled = false;
|
||||||
|
generateDescBtn.classList.remove('loading');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function showDescStatus(message, type) {
|
||||||
|
descStatusDiv.textContent = message;
|
||||||
|
descStatusDiv.className = 'tags-status ' + type;
|
||||||
|
descStatusDiv.style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在Features字段后添加"AI生成功能"按钮
|
||||||
|
const featuresField = document.querySelector('textarea[name="features"]');
|
||||||
|
if (featuresField) {
|
||||||
|
// 创建生成按钮
|
||||||
|
const generateFeaturesBtn = document.createElement('button');
|
||||||
|
generateFeaturesBtn.type = 'button';
|
||||||
|
generateFeaturesBtn.className = 'btn btn-success generate-features-btn';
|
||||||
|
generateFeaturesBtn.innerHTML = '<span class="normal-icon">✨</span><span class="loading-icon">↻</span> AI生成主要功能';
|
||||||
|
|
||||||
|
const featuresStatusDiv = document.createElement('div');
|
||||||
|
featuresStatusDiv.className = 'tags-status';
|
||||||
|
|
||||||
|
featuresField.parentNode.appendChild(generateFeaturesBtn);
|
||||||
|
featuresField.parentNode.appendChild(featuresStatusDiv);
|
||||||
|
|
||||||
|
generateFeaturesBtn.addEventListener('click', function() {
|
||||||
|
const nameField = document.querySelector('input[name="name"]');
|
||||||
|
const descriptionField = document.querySelector('textarea[name="description"]');
|
||||||
|
const urlField = document.querySelector('input[name="url"]');
|
||||||
|
|
||||||
|
const name = nameField ? nameField.value.trim() : '';
|
||||||
|
const description = descriptionField ? descriptionField.value.trim() : '';
|
||||||
|
const url = urlField ? urlField.value.trim() : '';
|
||||||
|
|
||||||
|
if (!name || !description) {
|
||||||
|
showFeaturesStatus('请先填写网站名称和描述', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示加载状态
|
||||||
|
generateFeaturesBtn.disabled = true;
|
||||||
|
generateFeaturesBtn.classList.add('loading');
|
||||||
|
featuresStatusDiv.style.display = 'none';
|
||||||
|
|
||||||
|
// 调用API生成功能
|
||||||
|
fetch('/api/generate-features', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: name,
|
||||||
|
description: description,
|
||||||
|
url: url
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success && data.features) {
|
||||||
|
// 自动填充到features字段
|
||||||
|
featuresField.value = data.features;
|
||||||
|
showFeaturesStatus('✓ AI已生成主要功能列表', 'success');
|
||||||
|
} else {
|
||||||
|
showFeaturesStatus('✗ ' + (data.message || '功能生成失败'), 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
showFeaturesStatus('✗ 网络请求失败,请重试', 'error');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
generateFeaturesBtn.disabled = false;
|
||||||
|
generateFeaturesBtn.classList.remove('loading');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function showFeaturesStatus(message, type) {
|
||||||
|
featuresStatusDiv.textContent = message;
|
||||||
|
featuresStatusDiv.className = 'tags-status ' + type;
|
||||||
|
featuresStatusDiv.style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -11,6 +11,28 @@
|
|||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
}
|
}
|
||||||
|
.generate-features-btn {
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
.upload-logo-btn {
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
.logo-preview {
|
||||||
|
margin-top: 10px;
|
||||||
|
max-width: 200px;
|
||||||
|
max-height: 200px;
|
||||||
|
display: none;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
.logo-preview img {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 150px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
.fetch-status {
|
.fetch-status {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
@@ -47,6 +69,26 @@
|
|||||||
from { transform: rotate(0deg); }
|
from { transform: rotate(0deg); }
|
||||||
to { transform: rotate(360deg); }
|
to { transform: rotate(360deg); }
|
||||||
}
|
}
|
||||||
|
/* 标签输入框样式 */
|
||||||
|
.tag-input-wrapper {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.tag-input-field {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #DCDFE6;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.tag-input-field:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #0052D9;
|
||||||
|
}
|
||||||
|
.tag-input-help {
|
||||||
|
margin-top: 5px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -130,72 +172,490 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 在标签字段后添加"AI生成标签"按钮
|
// 在Logo字段后添加"上传Logo"功能
|
||||||
const tagsField = document.querySelector('select[name="tags"]');
|
const logoField = document.querySelector('input[name="logo"]');
|
||||||
if (tagsField) {
|
if (logoField) {
|
||||||
const generateBtn = document.createElement('button');
|
// 创建文件输入框
|
||||||
generateBtn.type = 'button';
|
const fileInput = document.createElement('input');
|
||||||
generateBtn.className = 'btn btn-success generate-tags-btn';
|
fileInput.type = 'file';
|
||||||
generateBtn.innerHTML = '<span class="normal-icon">✨</span><span class="loading-icon">↻</span> AI生成标签';
|
fileInput.accept = 'image/*';
|
||||||
|
fileInput.style.display = 'none';
|
||||||
|
|
||||||
const tagsStatusDiv = document.createElement('div');
|
// 创建上传按钮
|
||||||
tagsStatusDiv.className = 'tags-status';
|
const uploadBtn = document.createElement('button');
|
||||||
|
uploadBtn.type = 'button';
|
||||||
|
uploadBtn.className = 'btn btn-warning upload-logo-btn';
|
||||||
|
uploadBtn.innerHTML = '📁 上传Logo图片';
|
||||||
|
|
||||||
tagsField.parentNode.appendChild(generateBtn);
|
// 创建预览容器
|
||||||
tagsField.parentNode.appendChild(tagsStatusDiv);
|
const previewDiv = document.createElement('div');
|
||||||
|
previewDiv.className = 'logo-preview';
|
||||||
|
previewDiv.innerHTML = '<img src="" alt="Logo预览"><p style="margin-top:5px; font-size:12px; color:#666;">Logo预览</p>';
|
||||||
|
|
||||||
generateBtn.addEventListener('click', function() {
|
logoField.parentNode.appendChild(fileInput);
|
||||||
|
logoField.parentNode.appendChild(uploadBtn);
|
||||||
|
logoField.parentNode.appendChild(previewDiv);
|
||||||
|
|
||||||
|
// 点击按钮触发文件选择
|
||||||
|
uploadBtn.addEventListener('click', function() {
|
||||||
|
fileInput.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 文件选择后自动上传
|
||||||
|
fileInput.addEventListener('change', function() {
|
||||||
|
const file = fileInput.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
// 验证文件类型
|
||||||
|
if (!file.type.startsWith('image/')) {
|
||||||
|
alert('请选择图片文件!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证文件大小(限制5MB)
|
||||||
|
if (file.size > 5 * 1024 * 1024) {
|
||||||
|
alert('图片文件不能超过5MB!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传文件
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('logo', file);
|
||||||
|
|
||||||
|
uploadBtn.disabled = true;
|
||||||
|
uploadBtn.textContent = '上传中...';
|
||||||
|
|
||||||
|
fetch('/api/upload-logo', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
// 设置Logo字段值
|
||||||
|
logoField.value = data.path;
|
||||||
|
|
||||||
|
// 显示预览
|
||||||
|
const img = previewDiv.querySelector('img');
|
||||||
|
img.src = data.path;
|
||||||
|
previewDiv.style.display = 'block';
|
||||||
|
|
||||||
|
alert('✓ Logo上传成功!');
|
||||||
|
} else {
|
||||||
|
alert('✗ ' + (data.message || '上传失败'));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
alert('✗ 上传失败,请重试');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
uploadBtn.disabled = false;
|
||||||
|
uploadBtn.innerHTML = '📁 上传Logo图片';
|
||||||
|
fileInput.value = '';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 如果Logo字段有值,显示预览
|
||||||
|
if (logoField.value) {
|
||||||
|
const img = previewDiv.querySelector('img');
|
||||||
|
img.src = logoField.value;
|
||||||
|
previewDiv.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听Logo字段变化,更新预览
|
||||||
|
logoField.addEventListener('input', function() {
|
||||||
|
if (logoField.value) {
|
||||||
|
const img = previewDiv.querySelector('img');
|
||||||
|
img.src = logoField.value;
|
||||||
|
previewDiv.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
previewDiv.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理标签字段 - 添加手动输入功能
|
||||||
|
const tagsSelect = document.querySelector('select[name="tags"]');
|
||||||
|
if (tagsSelect) {
|
||||||
|
// 先等待一下,确保 Flask-Admin 已经初始化好 select
|
||||||
|
setTimeout(function() {
|
||||||
|
// 保存原始选中的标签(从数据库加载的)
|
||||||
|
const originalSelectedOptions = Array.from(tagsSelect.selectedOptions);
|
||||||
|
|
||||||
|
// 隐藏原始的select字段
|
||||||
|
tagsSelect.style.display = 'none';
|
||||||
|
|
||||||
|
// 创建文本输入框
|
||||||
|
const tagInputWrapper = document.createElement('div');
|
||||||
|
tagInputWrapper.className = 'tag-input-wrapper';
|
||||||
|
|
||||||
|
const tagInput = document.createElement('input');
|
||||||
|
tagInput.type = 'text';
|
||||||
|
tagInput.className = 'tag-input-field';
|
||||||
|
tagInput.placeholder = '输入标签名称,按回车添加(如:AI工具、图像处理、免费)';
|
||||||
|
|
||||||
|
const tagHelpText = document.createElement('div');
|
||||||
|
tagHelpText.className = 'tag-input-help';
|
||||||
|
tagHelpText.textContent = '💡 提示:输入标签名称后按回车键添加,可以添加多个标签。已选标签会自动添加到下方列表。';
|
||||||
|
|
||||||
|
const selectedTagsDiv = document.createElement('div');
|
||||||
|
selectedTagsDiv.className = 'selected-tags';
|
||||||
|
selectedTagsDiv.style.marginTop = '10px';
|
||||||
|
|
||||||
|
tagInputWrapper.appendChild(tagInput);
|
||||||
|
tagInputWrapper.appendChild(tagHelpText);
|
||||||
|
tagInputWrapper.appendChild(selectedTagsDiv);
|
||||||
|
tagsSelect.parentNode.insertBefore(tagInputWrapper, tagsSelect.nextSibling);
|
||||||
|
|
||||||
|
// 显示已选标签
|
||||||
|
function updateSelectedTags() {
|
||||||
|
selectedTagsDiv.innerHTML = '';
|
||||||
|
const selectedOptions = Array.from(tagsSelect.selectedOptions);
|
||||||
|
|
||||||
|
// 如果没有选中的标签,显示提示
|
||||||
|
if (selectedOptions.length === 0) {
|
||||||
|
selectedTagsDiv.innerHTML = '<span style="color:#999; font-size:12px;">暂无已选标签</span>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedOptions.forEach(option => {
|
||||||
|
const tag = document.createElement('span');
|
||||||
|
tag.style.cssText = 'display:inline-block; background:#0052D9; color:white; padding:4px 10px; margin:4px; border-radius:4px; font-size:12px;';
|
||||||
|
|
||||||
|
// 获取标签文本 - 兼容多种方式
|
||||||
|
let tagText = option.textContent || option.innerText || option.text || option.innerHTML || option.label || `标签${option.value}`;
|
||||||
|
|
||||||
|
// 处理 <Tag XXX> 格式,提取出实际标签名称
|
||||||
|
const match = tagText.match(/<Tag\s+(.+?)>/);
|
||||||
|
if (match) {
|
||||||
|
tagText = match[1]; // 提取标签名称
|
||||||
|
}
|
||||||
|
|
||||||
|
tag.innerHTML = tagText + ' <span style="cursor:pointer; margin-left:5px; font-weight:bold;" data-tag-id="' + option.value + '">×</span>';
|
||||||
|
selectedTagsDiv.appendChild(tag);
|
||||||
|
|
||||||
|
// 为删除按钮添加点击事件
|
||||||
|
const deleteBtn = tag.querySelector('span[data-tag-id]');
|
||||||
|
if (deleteBtn) {
|
||||||
|
deleteBtn.addEventListener('click', function(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
option.selected = false;
|
||||||
|
updateSelectedTags();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加标签
|
||||||
|
tagInput.addEventListener('keypress', function(e) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
const tagName = tagInput.value.trim();
|
||||||
|
|
||||||
|
if (!tagName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查标签是否已存在
|
||||||
|
let existingOption = null;
|
||||||
|
for (let option of tagsSelect.options) {
|
||||||
|
if (option.text.toLowerCase() === tagName.toLowerCase()) {
|
||||||
|
existingOption = option;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingOption) {
|
||||||
|
// 选中已存在的标签
|
||||||
|
existingOption.selected = true;
|
||||||
|
} else {
|
||||||
|
// 创建新标签选项(使用负数ID表示新标签)
|
||||||
|
const newOption = document.createElement('option');
|
||||||
|
newOption.value = 'new_' + Date.now();
|
||||||
|
newOption.text = tagName;
|
||||||
|
newOption.selected = true;
|
||||||
|
newOption.setAttribute('data-new-tag', 'true');
|
||||||
|
tagsSelect.appendChild(newOption);
|
||||||
|
}
|
||||||
|
|
||||||
|
tagInput.value = '';
|
||||||
|
updateSelectedTags();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 初始化显示 - 使用延迟确保数据已加载
|
||||||
|
updateSelectedTags();
|
||||||
|
|
||||||
|
// 再次确保原始选中的标签保持选中状态
|
||||||
|
originalSelectedOptions.forEach(opt => {
|
||||||
|
// 在所有选项中找到对应的选项并设置为选中
|
||||||
|
for (let option of tagsSelect.options) {
|
||||||
|
if (option.value === opt.value) {
|
||||||
|
option.selected = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 再次更新显示
|
||||||
|
setTimeout(function() {
|
||||||
|
updateSelectedTags();
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
// 表单提交时处理新标签
|
||||||
|
const form = tagsSelect.closest('form');
|
||||||
|
form.addEventListener('submit', function(e) {
|
||||||
|
// 收集所有新标签名称
|
||||||
|
const newTags = [];
|
||||||
|
Array.from(tagsSelect.options).forEach(option => {
|
||||||
|
if (option.selected && option.hasAttribute('data-new-tag')) {
|
||||||
|
newTags.push(option.text);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 如果有新标签,添加到隐藏字段
|
||||||
|
if (newTags.length > 0) {
|
||||||
|
const hiddenInput = document.createElement('input');
|
||||||
|
hiddenInput.type = 'hidden';
|
||||||
|
hiddenInput.name = 'new_tags';
|
||||||
|
hiddenInput.value = newTags.join(',');
|
||||||
|
form.appendChild(hiddenInput);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 在标签字段后添加"AI生成标签"按钮
|
||||||
|
const generateBtn = document.createElement('button');
|
||||||
|
generateBtn.type = 'button';
|
||||||
|
generateBtn.className = 'btn btn-success generate-tags-btn';
|
||||||
|
generateBtn.innerHTML = '<span class="normal-icon">✨</span><span class="loading-icon">↻</span> AI生成标签';
|
||||||
|
|
||||||
|
const tagsStatusDiv = document.createElement('div');
|
||||||
|
tagsStatusDiv.className = 'tags-status';
|
||||||
|
|
||||||
|
tagInputWrapper.appendChild(generateBtn);
|
||||||
|
tagInputWrapper.appendChild(tagsStatusDiv);
|
||||||
|
|
||||||
|
generateBtn.addEventListener('click', function() {
|
||||||
|
const nameField = document.querySelector('input[name="name"]');
|
||||||
|
const descriptionField = document.querySelector('textarea[name="description"]');
|
||||||
|
|
||||||
|
const name = nameField ? nameField.value.trim() : '';
|
||||||
|
const description = descriptionField ? descriptionField.value.trim() : '';
|
||||||
|
|
||||||
|
if (!name || !description) {
|
||||||
|
showTagsStatus('请先填写网站名称和描述', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示加载状态
|
||||||
|
generateBtn.disabled = true;
|
||||||
|
generateBtn.classList.add('loading');
|
||||||
|
tagsStatusDiv.style.display = 'none';
|
||||||
|
|
||||||
|
// 调用API生成标签
|
||||||
|
fetch('/api/generate-tags', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: name,
|
||||||
|
description: description
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success && data.tags && data.tags.length > 0) {
|
||||||
|
// 自动添加生成的标签
|
||||||
|
data.tags.forEach(tagName => {
|
||||||
|
// 检查是否已存在
|
||||||
|
let exists = false;
|
||||||
|
for (let option of tagsSelect.options) {
|
||||||
|
if (option.text.toLowerCase() === tagName.toLowerCase()) {
|
||||||
|
option.selected = true;
|
||||||
|
exists = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果不存在,创建新标签
|
||||||
|
if (!exists) {
|
||||||
|
const newOption = document.createElement('option');
|
||||||
|
newOption.value = 'new_' + Date.now() + '_' + Math.random();
|
||||||
|
newOption.text = tagName;
|
||||||
|
newOption.selected = true;
|
||||||
|
newOption.setAttribute('data-new-tag', 'true');
|
||||||
|
tagsSelect.appendChild(newOption);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
updateSelectedTags();
|
||||||
|
showTagsStatus('✓ AI已自动添加推荐标签:' + data.tags.join(', '), 'success');
|
||||||
|
} else {
|
||||||
|
showTagsStatus('✗ ' + (data.message || '标签生成失败'), 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
showTagsStatus('✗ 网络请求失败,请重试', 'error');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
generateBtn.disabled = false;
|
||||||
|
generateBtn.classList.remove('loading');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function showTagsStatus(message, type) {
|
||||||
|
tagsStatusDiv.textContent = message;
|
||||||
|
tagsStatusDiv.className = 'tags-status ' + type;
|
||||||
|
tagsStatusDiv.style.display = 'block';
|
||||||
|
}
|
||||||
|
}, 50); // setTimeout 结束
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在Description字段后添加"AI生成详细介绍"按钮
|
||||||
|
const descriptionField = document.querySelector('textarea[name="description"]');
|
||||||
|
if (descriptionField) {
|
||||||
|
// 创建生成按钮
|
||||||
|
const generateDescBtn = document.createElement('button');
|
||||||
|
generateDescBtn.type = 'button';
|
||||||
|
generateDescBtn.className = 'btn btn-success generate-features-btn';
|
||||||
|
generateDescBtn.innerHTML = '<span class="normal-icon">✨</span><span class="loading-icon">↻</span> AI生成详细介绍';
|
||||||
|
|
||||||
|
const descStatusDiv = document.createElement('div');
|
||||||
|
descStatusDiv.className = 'tags-status';
|
||||||
|
|
||||||
|
descriptionField.parentNode.appendChild(generateDescBtn);
|
||||||
|
descriptionField.parentNode.appendChild(descStatusDiv);
|
||||||
|
|
||||||
|
generateDescBtn.addEventListener('click', function() {
|
||||||
const nameField = document.querySelector('input[name="name"]');
|
const nameField = document.querySelector('input[name="name"]');
|
||||||
const descriptionField = document.querySelector('textarea[name="description"]');
|
const shortDescField = document.querySelector('input[name="short_desc"]');
|
||||||
|
const urlField = document.querySelector('input[name="url"]');
|
||||||
|
|
||||||
const name = nameField ? nameField.value.trim() : '';
|
const name = nameField ? nameField.value.trim() : '';
|
||||||
const description = descriptionField ? descriptionField.value.trim() : '';
|
const shortDesc = shortDescField ? shortDescField.value.trim() : '';
|
||||||
|
const url = urlField ? urlField.value.trim() : '';
|
||||||
|
|
||||||
if (!name || !description) {
|
if (!name) {
|
||||||
showTagsStatus('请先填写网站名称和描述', 'error');
|
showDescStatus('请先填写网站名称', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示加载状态
|
// 显示加载状态
|
||||||
generateBtn.disabled = true;
|
generateDescBtn.disabled = true;
|
||||||
generateBtn.classList.add('loading');
|
generateDescBtn.classList.add('loading');
|
||||||
tagsStatusDiv.style.display = 'none';
|
descStatusDiv.style.display = 'none';
|
||||||
|
|
||||||
// 调用API生成标签
|
// 调用API生成详细介绍
|
||||||
fetch('/api/generate-tags', {
|
fetch('/api/generate-description', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
name: name,
|
name: name,
|
||||||
description: description
|
short_desc: shortDesc,
|
||||||
|
url: url
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.success && data.tags && data.tags.length > 0) {
|
if (data.success && data.description) {
|
||||||
// 显示生成的标签
|
// 自动填充到description字段
|
||||||
const tagsText = data.tags.join(', ');
|
descriptionField.value = data.description;
|
||||||
showTagsStatus('✓ AI生成的标签建议: ' + tagsText + '\n(请在标签字段中手动选择或创建这些标签)', 'success');
|
showDescStatus('✓ AI已生成详细介绍', 'success');
|
||||||
} else {
|
} else {
|
||||||
showTagsStatus('✗ ' + (data.message || '标签生成失败'), 'error');
|
showDescStatus('✗ ' + (data.message || '详细介绍生成失败'), 'error');
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('Error:', error);
|
console.error('Error:', error);
|
||||||
showTagsStatus('✗ 网络请求失败,请重试', 'error');
|
showDescStatus('✗ 网络请求失败,请重试', 'error');
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
generateBtn.disabled = false;
|
generateDescBtn.disabled = false;
|
||||||
generateBtn.classList.remove('loading');
|
generateDescBtn.classList.remove('loading');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function showTagsStatus(message, type) {
|
function showDescStatus(message, type) {
|
||||||
tagsStatusDiv.textContent = message;
|
descStatusDiv.textContent = message;
|
||||||
tagsStatusDiv.className = 'tags-status ' + type;
|
descStatusDiv.className = 'tags-status ' + type;
|
||||||
tagsStatusDiv.style.display = 'block';
|
descStatusDiv.style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在Features字段后添加"AI生成功能"按钮
|
||||||
|
const featuresField = document.querySelector('textarea[name="features"]');
|
||||||
|
if (featuresField) {
|
||||||
|
// 创建生成按钮
|
||||||
|
const generateFeaturesBtn = document.createElement('button');
|
||||||
|
generateFeaturesBtn.type = 'button';
|
||||||
|
generateFeaturesBtn.className = 'btn btn-success generate-features-btn';
|
||||||
|
generateFeaturesBtn.innerHTML = '<span class="normal-icon">✨</span><span class="loading-icon">↻</span> AI生成主要功能';
|
||||||
|
|
||||||
|
const featuresStatusDiv = document.createElement('div');
|
||||||
|
featuresStatusDiv.className = 'tags-status';
|
||||||
|
|
||||||
|
featuresField.parentNode.appendChild(generateFeaturesBtn);
|
||||||
|
featuresField.parentNode.appendChild(featuresStatusDiv);
|
||||||
|
|
||||||
|
generateFeaturesBtn.addEventListener('click', function() {
|
||||||
|
const nameField = document.querySelector('input[name="name"]');
|
||||||
|
const descriptionField = document.querySelector('textarea[name="description"]');
|
||||||
|
const urlField = document.querySelector('input[name="url"]');
|
||||||
|
|
||||||
|
const name = nameField ? nameField.value.trim() : '';
|
||||||
|
const description = descriptionField ? descriptionField.value.trim() : '';
|
||||||
|
const url = urlField ? urlField.value.trim() : '';
|
||||||
|
|
||||||
|
if (!name || !description) {
|
||||||
|
showFeaturesStatus('请先填写网站名称和描述', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示加载状态
|
||||||
|
generateFeaturesBtn.disabled = true;
|
||||||
|
generateFeaturesBtn.classList.add('loading');
|
||||||
|
featuresStatusDiv.style.display = 'none';
|
||||||
|
|
||||||
|
// 调用API生成功能
|
||||||
|
fetch('/api/generate-features', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: name,
|
||||||
|
description: description,
|
||||||
|
url: url
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success && data.features) {
|
||||||
|
// 自动填充到features字段
|
||||||
|
featuresField.value = data.features;
|
||||||
|
showFeaturesStatus('✓ AI已生成主要功能列表', 'success');
|
||||||
|
} else {
|
||||||
|
showFeaturesStatus('✗ ' + (data.message || '功能生成失败'), 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
showFeaturesStatus('✗ 网络请求失败,请重试', 'error');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
generateFeaturesBtn.disabled = false;
|
||||||
|
generateFeaturesBtn.classList.remove('loading');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function showFeaturesStatus(message, type) {
|
||||||
|
featuresStatusDiv.textContent = message;
|
||||||
|
featuresStatusDiv.className = 'tags-status ' + type;
|
||||||
|
featuresStatusDiv.style.display = 'block';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,14 +5,6 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>{% block title %}ZJPB - 焦提示词 | AI工具导航{% endblock %}</title>
|
<title>{% block title %}ZJPB - 焦提示词 | AI工具导航{% endblock %}</title>
|
||||||
|
|
||||||
<!-- Google Fonts -->
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
|
|
||||||
|
|
||||||
<!-- Material Symbols -->
|
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" />
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
* {
|
* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -38,7 +30,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
|
||||||
background: var(--bg-page);
|
background: var(--bg-page);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
@@ -139,12 +131,12 @@
|
|||||||
background: var(--bg-white);
|
background: var(--bg-white);
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-box .material-symbols-outlined {
|
.search-box .search-icon {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 10px;
|
left: 10px;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
font-size: 20px;
|
font-size: 16px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,7 +227,7 @@
|
|||||||
<div class="nav-left">
|
<div class="nav-left">
|
||||||
<a href="/" class="nav-logo">
|
<a href="/" class="nav-logo">
|
||||||
<div class="nav-logo-icon">
|
<div class="nav-logo-icon">
|
||||||
<span class="material-symbols-outlined" style="font-size: 20px;">blur_on</span>
|
<span style="font-size: 20px;">✦</span>
|
||||||
</div>
|
</div>
|
||||||
<span>ZJPB</span>
|
<span>ZJPB</span>
|
||||||
</a>
|
</a>
|
||||||
@@ -247,7 +239,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="nav-right">
|
<div class="nav-right">
|
||||||
<form action="/" method="get" class="search-box">
|
<form action="/" method="get" class="search-box">
|
||||||
<span class="material-symbols-outlined">search</span>
|
<span class="search-icon">🔍</span>
|
||||||
<input type="text" name="q" placeholder="搜索 AI 工具..." value="{{ search_query or '' }}">
|
<input type="text" name="q" placeholder="搜索 AI 工具..." value="{{ search_query or '' }}">
|
||||||
</form>
|
</form>
|
||||||
<a href="/admin/login" class="btn btn-secondary">登录</a>
|
<a href="/admin/login" class="btn btn-secondary">登录</a>
|
||||||
@@ -263,16 +255,30 @@
|
|||||||
<footer class="footer">
|
<footer class="footer">
|
||||||
<div class="footer-container">
|
<div class="footer-container">
|
||||||
<div class="footer-text">
|
<div class="footer-text">
|
||||||
© 2023 ZJPB AI Directory. All rights reserved.
|
<div>© 2025 ZJPB - 焦提示词 | AI工具导航. All rights reserved.</div>
|
||||||
|
<div style="margin-top: 8px;">
|
||||||
|
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener noreferrer" style="color: var(--text-secondary); text-decoration: none;">
|
||||||
|
浙ICP备2025154782号-1
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer-links">
|
<div class="footer-links">
|
||||||
<a href="#">Twitter</a>
|
<a href="#">关于我们</a>
|
||||||
<a href="#">Discord</a>
|
<a href="#">隐私政策</a>
|
||||||
<a href="#">Privacy Policy</a>
|
<a href="#">用户协议</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
<!-- Microsoft Clarity 统计代码 -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
(function(c,l,a,r,i,t,y){
|
||||||
|
c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
|
||||||
|
t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
|
||||||
|
y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
|
||||||
|
})(window, document, "clarity", "script", "uoa2j40sf0");
|
||||||
|
</script>
|
||||||
|
|
||||||
{% block extra_js %}{% endblock %}
|
{% block extra_js %}{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -20,12 +20,6 @@
|
|||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.back-link .material-symbols-outlined {
|
|
||||||
font-size: 24px;
|
|
||||||
line-height: 1;
|
|
||||||
vertical-align: middle;
|
|
||||||
margin-top: -2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 产品头部区域 */
|
/* 产品头部区域 */
|
||||||
.product-header-wrapper {
|
.product-header-wrapper {
|
||||||
@@ -84,9 +78,6 @@
|
|||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-link .material-symbols-outlined {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-meta {
|
.product-meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -103,9 +94,6 @@
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.meta-item .material-symbols-outlined {
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-tags-list {
|
.product-tags-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -195,9 +183,6 @@
|
|||||||
box-shadow: 0 4px 12px rgba(14, 165, 233, 0.3);
|
box-shadow: 0 4px 12px rgba(14, 165, 233, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.visit-btn .material-symbols-outlined {
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.visit-hint {
|
.visit-hint {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -242,10 +227,6 @@
|
|||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-block h2 .material-symbols-outlined {
|
|
||||||
font-size: 24px;
|
|
||||||
color: var(--primary-blue);
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-block p {
|
.content-block p {
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
@@ -399,6 +380,126 @@
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Markdown内容样式 */
|
||||||
|
.markdown-content {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content h1,
|
||||||
|
.markdown-content h2,
|
||||||
|
.markdown-content h3 {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-weight: 600;
|
||||||
|
margin-top: 24px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content h2 {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content h3 {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content p {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content ul,
|
||||||
|
.markdown-content ol {
|
||||||
|
margin: 16px 0;
|
||||||
|
padding-left: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content ul li {
|
||||||
|
list-style: none;
|
||||||
|
position: relative;
|
||||||
|
padding-left: 20px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content ul li:before {
|
||||||
|
content: "▸";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
color: var(--primary-blue);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content ol li {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
line-height: 1.8;
|
||||||
|
padding-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content code {
|
||||||
|
background: #f1f5f9;
|
||||||
|
color: #e11d48;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content pre {
|
||||||
|
background: #1e293b;
|
||||||
|
color: #e2e8f0;
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content pre code {
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content strong {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content em {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content a {
|
||||||
|
color: var(--primary-blue);
|
||||||
|
text-decoration: none;
|
||||||
|
border-bottom: 1px solid transparent;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content a:hover {
|
||||||
|
border-bottom-color: var(--primary-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content blockquote {
|
||||||
|
border-left: 4px solid var(--primary-blue);
|
||||||
|
padding-left: 16px;
|
||||||
|
margin: 16px 0;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content hr {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
margin: 24px 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* 响应式 */
|
/* 响应式 */
|
||||||
@media (max-width: 968px) {
|
@media (max-width: 968px) {
|
||||||
.product-header-wrapper {
|
.product-header-wrapper {
|
||||||
@@ -432,7 +533,7 @@
|
|||||||
|
|
||||||
<!-- 返回链接 -->
|
<!-- 返回链接 -->
|
||||||
<a href="/" class="back-link">
|
<a href="/" class="back-link">
|
||||||
<span class="material-symbols-outlined">arrow_back</span>
|
<span>←</span>
|
||||||
返回首页
|
返回首页
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
@@ -456,16 +557,16 @@
|
|||||||
<h1>{{ site.name }}</h1>
|
<h1>{{ site.name }}</h1>
|
||||||
<a href="{{ site.url }}" target="_blank" class="product-link">
|
<a href="{{ site.url }}" target="_blank" class="product-link">
|
||||||
{{ site.url }}
|
{{ site.url }}
|
||||||
<span class="material-symbols-outlined">open_in_new</span>
|
<span>↗</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div class="product-meta">
|
<div class="product-meta">
|
||||||
<div class="meta-item">
|
<div class="meta-item">
|
||||||
<span class="material-symbols-outlined">visibility</span>
|
<span>👁</span>
|
||||||
<span>{{ site.view_count | default(0) }} 次浏览</span>
|
<span>{{ site.view_count | default(0) }} 次浏览</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="meta-item">
|
<div class="meta-item">
|
||||||
<span class="material-symbols-outlined">calendar_today</span>
|
<span>📅</span>
|
||||||
<span>添加于 {{ site.created_at.strftime('%Y年%m月%d日') }}</span>
|
<span>添加于 {{ site.created_at.strftime('%Y年%m月%d日') }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -488,7 +589,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<a href="{{ site.url }}" target="_blank" class="visit-btn">
|
<a href="{{ site.url }}" target="_blank" class="visit-btn">
|
||||||
访问网站
|
访问网站
|
||||||
<span class="material-symbols-outlined">north_east</span>
|
<span>↗</span>
|
||||||
</a>
|
</a>
|
||||||
<p class="visit-hint">在新标签页打开 • {{ site.url.split('/')[2] if site.url else '' }}</p>
|
<p class="visit-hint">在新标签页打开 • {{ site.url.split('/')[2] if site.url else '' }}</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -501,20 +602,20 @@
|
|||||||
<!-- Product Overview -->
|
<!-- Product Overview -->
|
||||||
<div class="content-block">
|
<div class="content-block">
|
||||||
<h2>
|
<h2>
|
||||||
<span class="material-symbols-outlined">info</span>
|
<span>ℹ️</span>
|
||||||
产品概述
|
产品概述
|
||||||
</h2>
|
</h2>
|
||||||
<p>{{ site.description }}</p>
|
<div class="markdown-content">{{ site.description | markdown | safe }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Detailed Description -->
|
<!-- Detailed Description -->
|
||||||
{% if site.features %}
|
{% if site.features %}
|
||||||
<div class="content-block">
|
<div class="content-block">
|
||||||
<h2>
|
<h2>
|
||||||
<span class="material-symbols-outlined">description</span>
|
<span>📋</span>
|
||||||
详细描述
|
主要功能
|
||||||
</h2>
|
</h2>
|
||||||
<div>{{ site.features | safe }}</div>
|
<div class="markdown-content">{{ site.features | markdown | safe }}</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
@@ -522,7 +623,7 @@
|
|||||||
{% if news_list %}
|
{% if news_list %}
|
||||||
<div class="content-block">
|
<div class="content-block">
|
||||||
<h2>
|
<h2>
|
||||||
<span class="material-symbols-outlined">newspaper</span>
|
<span>📰</span>
|
||||||
相关新闻
|
相关新闻
|
||||||
</h2>
|
</h2>
|
||||||
{% for news in news_list %}
|
{% for news in news_list %}
|
||||||
@@ -540,7 +641,7 @@
|
|||||||
{% if recommended_sites %}
|
{% if recommended_sites %}
|
||||||
<div class="content-block">
|
<div class="content-block">
|
||||||
<h2>
|
<h2>
|
||||||
<span class="material-symbols-outlined">auto_awesome</span>
|
<span>✨</span>
|
||||||
相似推荐
|
相似推荐
|
||||||
</h2>
|
</h2>
|
||||||
<div class="recommendations-grid">
|
<div class="recommendations-grid">
|
||||||
@@ -560,7 +661,7 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="material-symbols-outlined arrow-icon">north_east</span>
|
<span class="arrow-icon">↗</span>
|
||||||
</a>
|
</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
52
test_deepseek.py
Normal file
52
test_deepseek.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
"""测试DeepSeek API配置"""
|
||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from openai import OpenAI
|
||||||
|
|
||||||
|
# 加载环境变量
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
def test_deepseek_api():
|
||||||
|
"""测试DeepSeek API连接"""
|
||||||
|
api_key = os.getenv('DEEPSEEK_API_KEY')
|
||||||
|
base_url = os.getenv('DEEPSEEK_BASE_URL', 'https://api.deepseek.com')
|
||||||
|
|
||||||
|
print(f"API Key: {api_key[:20]}..." if api_key else "未找到API Key")
|
||||||
|
print(f"Base URL: {base_url}")
|
||||||
|
|
||||||
|
if not api_key:
|
||||||
|
print("[ERROR] DEEPSEEK_API_KEY not configured")
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 创建客户端
|
||||||
|
client = OpenAI(
|
||||||
|
api_key=api_key,
|
||||||
|
base_url=base_url
|
||||||
|
)
|
||||||
|
|
||||||
|
# 发送测试请求
|
||||||
|
print("\nTesting API connection...")
|
||||||
|
response = client.chat.completions.create(
|
||||||
|
model="deepseek-chat",
|
||||||
|
messages=[
|
||||||
|
{"role": "system", "content": "你是一个AI助手"},
|
||||||
|
{"role": "user", "content": "你好,请用一句话介绍你自己"}
|
||||||
|
],
|
||||||
|
max_tokens=100
|
||||||
|
)
|
||||||
|
|
||||||
|
result = response.choices[0].message.content
|
||||||
|
# 移除emoji和特殊字符
|
||||||
|
result_clean = result.encode('ascii', 'ignore').decode('ascii')
|
||||||
|
print(f"\n[SUCCESS] API connection successful!")
|
||||||
|
print(f"Response: {result_clean if result_clean else result[:50]}")
|
||||||
|
print(f"Usage: {response.usage}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n[ERROR] API connection failed: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
test_deepseek_api()
|
||||||
2438
v2.1.0.patch
Normal file
2438
v2.1.0.patch
Normal file
File diff suppressed because it is too large
Load Diff
15
wsgi.py
Normal file
15
wsgi.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
生产环境应用入口
|
||||||
|
用于gunicorn启动
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
from app import create_app
|
||||||
|
|
||||||
|
# 创建应用实例
|
||||||
|
app = create_app(os.getenv('FLASK_ENV', 'production'))
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# 这个分支仅用于直接运行脚本测试
|
||||||
|
# 生产环境应该使用 gunicorn
|
||||||
|
app.run(host='0.0.0.0', port=5000, debug=False)
|
||||||
Reference in New Issue
Block a user