release: v2.1.0 - Prompt管理系统、页脚优化、图标修复

This commit is contained in:
ZJPB Admin
2025-12-30 01:17:08 +08:00
parent 9e47ebe749
commit 9f5d006090
23 changed files with 5871 additions and 99 deletions

463
DEPLOYMENT.md Normal file
View File

@@ -0,0 +1,463 @@
# ZJPB - 焦提示词 部署文档
## 1Panel部署指南
### 前置要求
- 1Panel管理面板已安装
- MySQL 5.7+ 或 MariaDB
- Python 3.8+
- Nginx1Panel自带
---
## 一、数据库准备
### 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
View 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
View 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
View 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
View File

@@ -1,11 +1,12 @@
import os
import markdown
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_admin import Admin, AdminIndexView, expose
from flask_admin.contrib.sqla import ModelView
from datetime import datetime
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.tag_generator import TagGenerator
@@ -19,6 +20,14 @@ def create_app(config_name='default'):
# 初始化数据库
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.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()
# 优化使用一次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')
search_query = request.args.get('q', '').strip()
@@ -57,7 +82,7 @@ def create_app(config_name='default'):
pagination = None
return render_template('index_new.html', sites=sites, tags=tags,
selected_tag=selected_tag, search_query=search_query,
pagination=pagination)
pagination=pagination, tag_counts=tag_counts)
# 搜索功能
if search_query:
@@ -79,7 +104,7 @@ def create_app(config_name='default'):
return render_template('index_new.html', sites=sites, tags=tags,
selected_tag=selected_tag, search_query=search_query,
pagination=pagination)
pagination=pagination, tag_counts=tag_counts)
@app.route('/site/<code>')
def site_detail(code):
@@ -138,6 +163,51 @@ def create_app(config_name='default'):
logout_user()
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路由 ==========
@app.route('/api/fetch-website-info', methods=['POST'])
@login_required
@@ -165,17 +235,18 @@ def create_app(config_name='default'):
'message': '无法获取网站信息请检查URL是否正确或手动填写'
})
# 下载Logo如果有
# 下载Logo到本地(如果有)
logo_path = None
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({
'success': True,
'data': {
'name': info.get('name', ''),
'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)}'
}), 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'])
@login_required
def generate_tags():
@@ -345,11 +558,11 @@ def create_app(config_name='default'):
})
continue
# 4. 下载Logo失败不影响导入
# 4. 下载Logo到本地(失败不影响导入)
logo_path = None
if info.get('logo_url'):
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:
print(f"下载Logo失败 ({url}): {str(e)}")
# Logo下载失败不影响网站导入
@@ -544,9 +757,47 @@ def create_app(config_name='default'):
import re
import random
from pypinyin import lazy_pinyin
from flask import request
# 使用no_autoflush防止在查询时触发提前flush
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位数字编码
if not model.code or model.code.strip() == '':
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
admin = Admin(
app,
@@ -696,6 +992,7 @@ def create_app(config_name='default'):
admin.add_view(SiteAdmin(Site, db.session, name='网站管理'))
admin.add_view(TagAdmin(Tag, 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'))
return app

96
deploy.sh Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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()

View File

@@ -136,3 +136,35 @@ class Admin(UserMixin, db.Model):
def __repr__(self):
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
View File

@@ -0,0 +1,2 @@
timeout: invalid time interval /t
Try 'timeout --help' for more information.

121
one_click_deploy.sh Normal file
View 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 ""

View 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">&times;</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>

View File

@@ -11,6 +11,28 @@
margin-top: 10px;
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 {
margin-top: 10px;
padding: 10px;
@@ -47,6 +69,26 @@
from { transform: rotate(0deg); }
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>
<script>
@@ -130,9 +172,238 @@ document.addEventListener('DOMContentLoaded', function() {
}
}
// 在标签字段后添加"AI生成标签"按钮
const tagsField = document.querySelector('select[name="tags"]');
if (tagsField) {
// 在Logo字段后添加"上传Logo"功能
const logoField = document.querySelector('input[name="logo"]');
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');
generateBtn.type = 'button';
generateBtn.className = 'btn btn-success generate-tags-btn';
@@ -141,8 +412,8 @@ document.addEventListener('DOMContentLoaded', function() {
const tagsStatusDiv = document.createElement('div');
tagsStatusDiv.className = 'tags-status';
tagsField.parentNode.appendChild(generateBtn);
tagsField.parentNode.appendChild(tagsStatusDiv);
tagInputWrapper.appendChild(generateBtn);
tagInputWrapper.appendChild(tagsStatusDiv);
generateBtn.addEventListener('click', function() {
const nameField = document.querySelector('input[name="name"]');
@@ -175,9 +446,31 @@ document.addEventListener('DOMContentLoaded', function() {
.then(response => response.json())
.then(data => {
if (data.success && data.tags && data.tags.length > 0) {
// 显示生成的标签
const tagsText = data.tags.join(', ');
showTagsStatus('✓ AI生成的标签建议: ' + tagsText + '\n请在标签字段中手动选择或创建这些标签', 'success');
// 自动添加生成的标签
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');
}
@@ -198,6 +491,152 @@ document.addEventListener('DOMContentLoaded', function() {
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>
{% endblock %}

View File

@@ -11,6 +11,28 @@
margin-top: 10px;
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 {
margin-top: 10px;
padding: 10px;
@@ -47,6 +69,26 @@
from { transform: rotate(0deg); }
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>
<script>
@@ -130,72 +172,490 @@ document.addEventListener('DOMContentLoaded', function() {
}
}
// 在标签字段后添加"AI生成标签"按钮
const tagsField = document.querySelector('select[name="tags"]');
if (tagsField) {
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生成标签';
// 在Logo字段后添加"上传Logo"功能
const logoField = document.querySelector('input[name="logo"]');
if (logoField) {
// 创建文件输入框
const fileInput = document.createElement('input');
fileInput.type = 'file';
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 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 description = descriptionField ? descriptionField.value.trim() : '';
const shortDesc = shortDescField ? shortDescField.value.trim() : '';
const url = urlField ? urlField.value.trim() : '';
if (!name || !description) {
showTagsStatus('请先填写网站名称和描述', 'error');
if (!name) {
showDescStatus('请先填写网站名称', 'error');
return;
}
// 显示加载状态
generateBtn.disabled = true;
generateBtn.classList.add('loading');
tagsStatusDiv.style.display = 'none';
generateDescBtn.disabled = true;
generateDescBtn.classList.add('loading');
descStatusDiv.style.display = 'none';
// 调用API生成标签
fetch('/api/generate-tags', {
// 调用API生成详细介绍
fetch('/api/generate-description', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: name,
description: description
short_desc: shortDesc,
url: url
})
})
.then(response => response.json())
.then(data => {
if (data.success && data.tags && data.tags.length > 0) {
// 显示生成的标签
const tagsText = data.tags.join(', ');
showTagsStatus('✓ AI生成的标签建议: ' + tagsText + '\n请在标签字段中手动选择或创建这些标签', 'success');
if (data.success && data.description) {
// 自动填充到description字段
descriptionField.value = data.description;
showDescStatus('✓ AI生成详细介绍', 'success');
} else {
showTagsStatus('✗ ' + (data.message || '标签生成失败'), 'error');
showDescStatus('✗ ' + (data.message || '详细介绍生成失败'), 'error');
}
})
.catch(error => {
console.error('Error:', error);
showTagsStatus('✗ 网络请求失败,请重试', 'error');
showDescStatus('✗ 网络请求失败,请重试', 'error');
})
.finally(() => {
generateBtn.disabled = false;
generateBtn.classList.remove('loading');
generateDescBtn.disabled = false;
generateDescBtn.classList.remove('loading');
});
});
function showTagsStatus(message, type) {
tagsStatusDiv.textContent = message;
tagsStatusDiv.className = 'tags-status ' + type;
tagsStatusDiv.style.display = 'block';
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';
}
}
});

View File

@@ -5,14 +5,6 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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>
* {
margin: 0;
@@ -38,7 +30,7 @@
}
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);
color: var(--text-primary);
line-height: 1.6;
@@ -139,12 +131,12 @@
background: var(--bg-white);
}
.search-box .material-symbols-outlined {
.search-box .search-icon {
position: absolute;
left: 10px;
top: 50%;
transform: translateY(-50%);
font-size: 20px;
font-size: 16px;
color: var(--text-muted);
}
@@ -235,7 +227,7 @@
<div class="nav-left">
<a href="/" class="nav-logo">
<div class="nav-logo-icon">
<span class="material-symbols-outlined" style="font-size: 20px;">blur_on</span>
<span style="font-size: 20px;"></span>
</div>
<span>ZJPB</span>
</a>
@@ -247,7 +239,7 @@
</div>
<div class="nav-right">
<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 '' }}">
</form>
<a href="/admin/login" class="btn btn-secondary">登录</a>
@@ -263,16 +255,30 @@
<footer class="footer">
<div class="footer-container">
<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 class="footer-links">
<a href="#">Twitter</a>
<a href="#">Discord</a>
<a href="#">Privacy Policy</a>
<a href="#">关于我们</a>
<a href="#">隐私政策</a>
<a href="#">用户协议</a>
</div>
</div>
</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 %}
</body>
</html>

View File

@@ -20,12 +20,6 @@
color: var(--text-primary);
}
.back-link .material-symbols-outlined {
font-size: 24px;
line-height: 1;
vertical-align: middle;
margin-top: -2px;
}
/* 产品头部区域 */
.product-header-wrapper {
@@ -84,9 +78,6 @@
text-decoration: underline;
}
.product-link .material-symbols-outlined {
font-size: 16px;
}
.product-meta {
display: flex;
@@ -103,9 +94,6 @@
font-size: 14px;
}
.meta-item .material-symbols-outlined {
font-size: 18px;
}
.product-tags-list {
display: flex;
@@ -195,9 +183,6 @@
box-shadow: 0 4px 12px rgba(14, 165, 233, 0.3);
}
.visit-btn .material-symbols-outlined {
font-size: 18px;
}
.visit-hint {
text-align: center;
@@ -242,10 +227,6 @@
margin-bottom: 20px;
}
.content-block h2 .material-symbols-outlined {
font-size: 24px;
color: var(--primary-blue);
}
.content-block p {
color: var(--text-secondary);
@@ -399,6 +380,126 @@
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) {
.product-header-wrapper {
@@ -432,7 +533,7 @@
<!-- 返回链接 -->
<a href="/" class="back-link">
<span class="material-symbols-outlined">arrow_back</span>
<span></span>
返回首页
</a>
@@ -456,16 +557,16 @@
<h1>{{ site.name }}</h1>
<a href="{{ site.url }}" target="_blank" class="product-link">
{{ site.url }}
<span class="material-symbols-outlined">open_in_new</span>
<span></span>
</a>
<div class="product-meta">
<div class="meta-item">
<span class="material-symbols-outlined">visibility</span>
<span>👁</span>
<span>{{ site.view_count | default(0) }} 次浏览</span>
</div>
<div class="meta-item">
<span class="material-symbols-outlined">calendar_today</span>
<span>📅</span>
<span>添加于 {{ site.created_at.strftime('%Y年%m月%d日') }}</span>
</div>
</div>
@@ -488,7 +589,7 @@
</div>
<a href="{{ site.url }}" target="_blank" class="visit-btn">
访问网站
<span class="material-symbols-outlined">north_east</span>
<span></span>
</a>
<p class="visit-hint">在新标签页打开 • {{ site.url.split('/')[2] if site.url else '' }}</p>
</div>
@@ -501,20 +602,20 @@
<!-- Product Overview -->
<div class="content-block">
<h2>
<span class="material-symbols-outlined">info</span>
<span></span>
产品概述
</h2>
<p>{{ site.description }}</p>
<div class="markdown-content">{{ site.description | markdown | safe }}</div>
</div>
<!-- Detailed Description -->
{% if site.features %}
<div class="content-block">
<h2>
<span class="material-symbols-outlined">description</span>
详细描述
<span>📋</span>
主要功能
</h2>
<div>{{ site.features | safe }}</div>
<div class="markdown-content">{{ site.features | markdown | safe }}</div>
</div>
{% endif %}
@@ -522,7 +623,7 @@
{% if news_list %}
<div class="content-block">
<h2>
<span class="material-symbols-outlined">newspaper</span>
<span>📰</span>
相关新闻
</h2>
{% for news in news_list %}
@@ -540,7 +641,7 @@
{% if recommended_sites %}
<div class="content-block">
<h2>
<span class="material-symbols-outlined">auto_awesome</span>
<span></span>
相似推荐
</h2>
<div class="recommendations-grid">
@@ -560,7 +661,7 @@
{% endfor %}
</div>
</div>
<span class="material-symbols-outlined arrow-icon">north_east</span>
<span class="arrow-icon"></span>
</a>
{% endfor %}
</div>

52
test_deepseek.py Normal file
View 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

File diff suppressed because it is too large Load Diff

15
wsgi.py Normal file
View 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)