commit 2fbca6ebc7b983f63ddb8b80af1f66edd73cfb1b Author: Jowe <123822645+Selei1983@users.noreply.github.com> Date: Sat Dec 27 22:45:09 2025 +0800 feat: 完成全站UI优化 - 科技感/未来风设计 - 前台页面全面升级为Tailwind CSS框架 - 引入Google Fonts (Space Grotesk, Noto Sans) - 主色调更新为#25c0f4 (cyan blue) - 实现玻璃态效果和渐变背景 - 优化首页网格卡片布局和悬停动画 - 优化详情页双栏布局和渐变Logo光晕 - 优化管理员登录页,添加科技网格背景 - Flask-Admin后台完整深色主题 - 统一Material Symbols图标系统 - 网站自动抓取功能界面优化 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..4ae9836 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,22 @@ +{ + "permissions": { + "allow": [ + "Bash(if [ -d \".git\" ])", + "Bash(then echo \"Git repository exists\")", + "Bash(else echo \"No git repository\")", + "Bash(fi)", + "Bash(python:*)", + "Bash(python3:*)", + "Bash(py test_db.py:*)", + "Bash(where:*)", + "Bash(/c/Users/linha/AppData/Local/Microsoft/WindowsApps/python test_db.py)", + "Bash(pip install:*)", + "Bash(pip uninstall:*)", + "Bash(tasklist:*)", + "Bash(findstr:*)", + "Bash(dir:*)", + "Bash(git init:*)", + "Bash(git add:*)" + ] + } +} diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..439c0a5 --- /dev/null +++ b/.env.example @@ -0,0 +1,12 @@ +# 数据库配置 +DB_HOST=localhost +DB_PORT=3306 +DB_USER=root +DB_PASSWORD=your_password +DB_NAME=ai_nav + +# 安全配置 +SECRET_KEY=your-secret-key-here + +# 运行环境 (development/production) +FLASK_ENV=development diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..037ea8f --- /dev/null +++ b/.gitignore @@ -0,0 +1,64 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# 虚拟环境 +venv/ +ENV/ +env/ +.venv + +# Flask +instance/ +.webassets-cache + +# 环境变量 +.env +.env.local + +# 数据库 +*.db +*.sqlite +*.sqlite3 + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# 日志 +*.log +logs/ + +# 系统文件 +.DS_Store +Thumbs.db + +# 上传文件 +static/uploads/* +!static/uploads/.gitkeep + +# 临时文件 +*.tmp +*.bak +*.cache diff --git a/README.md b/README.md new file mode 100644 index 0000000..56be161 --- /dev/null +++ b/README.md @@ -0,0 +1,219 @@ +# AI工具导航网站 + +一个简洁美观的AI产品导航网站,用于展示和管理各类AI工具和应用。 + +## 功能特点 + +- ✅ 按标签分类展示AI工具 +- ✅ 卡片式设计,美观易用 +- ✅ 详细的工具介绍页面 +- ✅ 完善的后台管理系统 +- ✅ SEO友好的URL结构 +- ✅ 响应式设计,支持移动端 +- ✅ 浏览量统计 + +## 技术栈 + +- **后端**: Flask 3.0 + Python 3.8+ +- **数据库**: MySQL 5.7+ +- **前端**: Bootstrap 5 + Jinja2模板 +- **管理后台**: Flask-Admin +- **用户认证**: Flask-Login + +## 项目结构 + +``` +zjpb/ +├── app.py # Flask应用主文件 +├── config.py # 配置文件 +├── models.py # 数据库模型 +├── init_db.py # 数据库初始化脚本 +├── requirements.txt # Python依赖 +├── .env.example # 环境变量示例 +├── templates/ # HTML模板 +│ ├── base.html +│ ├── index.html # 首页 +│ ├── detail.html # 详情页 +│ └── admin_login.html # 登录页 +├── static/ # 静态资源 +│ ├── css/ +│ │ └── style.css +│ ├── js/ +│ │ └── main.js +│ └── images/ +└── migrations/ # 数据库迁移文件 +``` + +## 快速开始 + +### 1. 环境准备 + +确保已安装以下软件: +- Python 3.8+ +- MySQL 5.7+ +- pip + +### 2. 安装依赖 + +```bash +# 创建虚拟环境(推荐) +python -m venv venv + +# 激活虚拟环境 +# Windows: +venv\Scripts\activate +# Linux/Mac: +source venv/bin/activate + +# 安装依赖 +pip install -r requirements.txt +``` + +### 3. 配置数据库 + +1. 在MySQL中创建数据库: +```sql +CREATE DATABASE ai_nav CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +``` + +2. 复制环境变量配置文件: +```bash +cp .env.example .env +``` + +3. 编辑 `.env` 文件,修改数据库配置: +``` +DB_HOST=localhost +DB_PORT=3306 +DB_USER=root +DB_PASSWORD=your_password +DB_NAME=ai_nav +SECRET_KEY=your-secret-key-here +``` + +### 4. 初始化数据库 + +```bash +python init_db.py +``` + +这将创建所有数据表,并添加示例数据和默认管理员账号。 + +### 5. 运行应用 + +```bash +python app.py +``` + +访问 `http://localhost:5000` 查看网站。 + +## 管理后台 + +### 访问后台 + +- 后台地址: `http://localhost:5000/admin` +- 登录页面: `http://localhost:5000/admin/login` + +### 默认管理员账号 + +``` +用户名: admin +密码: admin123 +``` + +**⚠️ 重要**: 首次登录后请立即修改默认密码! + +### 后台功能 + +- **网站管理**: 添加、编辑、删除AI工具 +- **标签管理**: 管理分类标签 +- **管理员管理**: 添加和管理管理员账号 + +## 宝塔面板部署 + +### 1. 安装Python环境 + +在宝塔面板中安装Python项目管理器,选择Python 3.8+版本。 + +### 2. 上传项目 + +将项目文件上传到服务器,例如 `/www/wwwroot/ai_nav` + +### 3. 配置项目 + +1. 在宝塔面板中创建Python项目 +2. 项目路径: `/www/wwwroot/ai_nav` +3. 启动文件: `app.py` +4. 端口: 5000(或其他可用端口) + +### 4. 安装依赖 + +在项目目录下执行: +```bash +pip install -r requirements.txt +``` + +### 5. 配置反向代理 + +在宝塔面板的网站设置中配置反向代理: +- 目标URL: `http://127.0.0.1:5000` +- 启用缓存和gzip压缩 + +### 6. 配置SSL证书(可选) + +为网站配置SSL证书以启用HTTPS。 + +## 使用说明 + +### 添加新网站 + +1. 登录后台管理系统 +2. 点击"网站管理" -> "Create" +3. 填写网站信息: + - 网站名称 + - URL地址 + - URL别名(用于SEO友好的URL) + - Logo图片URL + - 简短描述 + - 详细介绍 + - 主要功能 + - 选择标签 + - 排序权重(数字越大越靠前) + +### 管理标签 + +1. 登录后台 +2. 点击"标签管理" +3. 可以添加、编辑或删除标签 +4. 为标签设置图标(Font Awesome类名) + +## 开发计划 + +- [ ] 搜索功能 +- [ ] 用户评论和评分 +- [ ] 关联新闻搜索(2.0版本) +- [ ] 数据统计和分析 +- [ ] API接口 +- [ ] 网站收藏功能 + +## 常见问题 + +### 1. 数据库连接失败 + +检查 `.env` 文件中的数据库配置是否正确,确保MySQL服务正在运行。 + +### 2. 启动时出现端口占用 + +修改 `app.py` 中的端口号,或关闭占用5000端口的其他程序。 + +### 3. 静态资源加载失败 + +检查 `static` 目录权限,确保Web服务器有读取权限。 + +## 许可证 + +MIT License + +## 联系方式 + +如有问题或建议,请提交Issue。 diff --git a/app.py b/app.py new file mode 100644 index 0000000..24a943f --- /dev/null +++ b/app.py @@ -0,0 +1,243 @@ +import os +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 +from flask_admin.contrib.sqla import ModelView +from datetime import datetime +from config import config +from models import db, Site, Tag, Admin as AdminModel +from utils.website_fetcher import WebsiteFetcher + +def create_app(config_name='default'): + """应用工厂函数""" + app = Flask(__name__) + + # 加载配置 + app.config.from_object(config[config_name]) + + # 初始化数据库 + db.init_app(app) + + # 初始化登录管理 + login_manager = LoginManager() + login_manager.init_app(app) + login_manager.login_view = 'admin_login' + login_manager.login_message = '请先登录' + + @login_manager.user_loader + def load_user(user_id): + return AdminModel.query.get(int(user_id)) + + # ========== 前台路由 ========== + @app.route('/') + def index(): + """首页""" + # 获取所有启用的标签 + tags = Tag.query.order_by(Tag.sort_order.desc(), Tag.id).all() + + # 获取筛选的标签 + tag_slug = request.args.get('tag') + selected_tag = None + + if tag_slug: + selected_tag = Tag.query.filter_by(slug=tag_slug).first() + if selected_tag: + sites = Site.query.filter( + Site.is_active == True, + Site.tags.contains(selected_tag) + ).order_by(Site.sort_order.desc(), Site.id.desc()).all() + else: + sites = [] + else: + # 获取所有启用的网站 + sites = Site.query.filter_by(is_active=True).order_by( + Site.sort_order.desc(), Site.id.desc() + ).all() + + return render_template('index.html', sites=sites, tags=tags, selected_tag=selected_tag) + + @app.route('/site/') + def site_detail(slug): + """网站详情页""" + site = Site.query.filter_by(slug=slug, is_active=True).first_or_404() + + # 增加浏览次数 + site.view_count += 1 + db.session.commit() + + return render_template('detail.html', site=site) + + # ========== 后台登录路由 ========== + @app.route('/admin/login', methods=['GET', 'POST']) + def admin_login(): + """管理员登录""" + if current_user.is_authenticated: + return redirect(url_for('admin.index')) + + if request.method == 'POST': + username = request.form.get('username') + password = request.form.get('password') + + admin = AdminModel.query.filter_by(username=username).first() + + if admin and admin.check_password(password) and admin.is_active: + login_user(admin) + admin.last_login = datetime.now() + db.session.commit() + return redirect(url_for('admin.index')) + else: + flash('用户名或密码错误', 'error') + + return render_template('admin_login.html') + + @app.route('/admin/logout') + @login_required + def admin_logout(): + """管理员登出""" + logout_user() + return redirect(url_for('index')) + + # ========== API路由 ========== + @app.route('/api/fetch-website-info', methods=['POST']) + @login_required + def fetch_website_info(): + """抓取网站信息API""" + try: + data = request.get_json() + url = data.get('url', '').strip() + + if not url: + return jsonify({ + 'success': False, + 'message': '请提供网站URL' + }), 400 + + # 创建抓取器 + fetcher = WebsiteFetcher(timeout=15) + + # 抓取网站信息 + info = fetcher.fetch_website_info(url) + + if not info: + return jsonify({ + 'success': False, + 'message': '无法获取网站信息,请检查URL是否正确或手动填写' + }) + + # 下载Logo(如果有) + logo_path = None + if info.get('logo_url'): + logo_path = fetcher.download_logo(info['logo_url']) + + return jsonify({ + 'success': True, + 'data': { + 'name': info.get('name', ''), + 'description': info.get('description', ''), + 'logo': logo_path or info.get('logo_url', '') + } + }) + + except Exception as e: + return jsonify({ + 'success': False, + 'message': f'抓取失败: {str(e)}' + }), 500 + + # ========== Flask-Admin 配置 ========== + class SecureModelView(ModelView): + """需要登录的模型视图""" + def is_accessible(self): + return current_user.is_authenticated + + def inaccessible_callback(self, name, **kwargs): + return redirect(url_for('admin_login')) + + class SecureAdminIndexView(AdminIndexView): + """需要登录的管理首页""" + def is_accessible(self): + return current_user.is_authenticated + + def inaccessible_callback(self, name, **kwargs): + return redirect(url_for('admin_login')) + + # 网站管理视图 + class SiteAdmin(SecureModelView): + # 自定义模板 + create_template = 'admin/site/create.html' + edit_template = 'admin/site/edit.html' + + column_list = ['id', 'name', 'url', 'slug', 'is_active', 'view_count', 'created_at'] + column_searchable_list = ['name', 'url', 'description'] + column_filters = ['is_active', 'tags'] + column_labels = { + 'id': 'ID', + 'name': '网站名称', + 'url': 'URL', + 'slug': 'URL别名', + 'logo': 'Logo', + 'short_desc': '简短描述', + 'description': '详细介绍', + 'features': '主要功能', + 'is_active': '是否启用', + 'view_count': '浏览次数', + 'sort_order': '排序权重', + 'tags': '标签', + 'created_at': '创建时间', + 'updated_at': '更新时间' + } + form_columns = ['name', 'url', 'slug', 'logo', 'short_desc', 'description', 'features', 'tags', 'is_active', 'sort_order'] + + # 标签管理视图 + class TagAdmin(SecureModelView): + column_list = ['id', 'name', 'slug', 'description', 'sort_order'] + column_searchable_list = ['name', 'description'] + column_labels = { + 'id': 'ID', + 'name': '标签名称', + 'slug': 'URL别名', + 'description': '标签描述', + 'icon': '图标', + 'sort_order': '排序权重', + 'created_at': '创建时间' + } + form_columns = ['name', 'slug', 'description', 'icon', 'sort_order'] + + # 管理员视图 + class AdminAdmin(SecureModelView): + column_list = ['id', 'username', 'email', 'is_active', 'last_login', 'created_at'] + column_searchable_list = ['username', 'email'] + column_filters = ['is_active'] + column_labels = { + 'id': 'ID', + 'username': '用户名', + 'email': '邮箱', + 'is_active': '是否启用', + 'created_at': '创建时间', + 'last_login': '最后登录' + } + form_columns = ['username', 'email', 'is_active'] + + def on_model_change(self, form, model, is_created): + # 如果是新建管理员,设置默认密码 + if is_created: + model.set_password('admin123') # 默认密码 + + # 初始化 Flask-Admin + admin = Admin( + app, + name='ZJPB 焦提示词 - 后台管理', + template_mode='bootstrap4', + index_view=SecureAdminIndexView(), + base_template='admin/custom_base.html' + ) + + admin.add_view(SiteAdmin(Site, db.session, name='网站管理')) + admin.add_view(TagAdmin(Tag, db.session, name='标签管理')) + admin.add_view(AdminAdmin(AdminModel, db.session, name='管理员', endpoint='admin_users')) + + return app + +if __name__ == '__main__': + app = create_app(os.getenv('FLASK_ENV', 'development')) + app.run(host='0.0.0.0', port=5000, debug=True) diff --git a/config.py b/config.py new file mode 100644 index 0000000..0e60817 --- /dev/null +++ b/config.py @@ -0,0 +1,46 @@ +import os +from dotenv import load_dotenv + +# 加载环境变量 +load_dotenv() + +class Config: + """基础配置""" + # 密钥配置 + SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key-change-in-production' + + # 数据库配置 + DB_HOST = os.environ.get('DB_HOST') or 'localhost' + DB_PORT = os.environ.get('DB_PORT') or '3306' + DB_USER = os.environ.get('DB_USER') or 'root' + DB_PASSWORD = os.environ.get('DB_PASSWORD') or '' + DB_NAME = os.environ.get('DB_NAME') or 'ai_nav' + + SQLALCHEMY_DATABASE_URI = f'mysql+pymysql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}?charset=utf8mb4' + SQLALCHEMY_TRACK_MODIFICATIONS = False + SQLALCHEMY_ECHO = False + + # 分页配置 + SITES_PER_PAGE = 20 + + # 上传文件配置 + UPLOAD_FOLDER = 'static/uploads' + MAX_CONTENT_LENGTH = 5 * 1024 * 1024 # 5MB + ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'webp'} + +class DevelopmentConfig(Config): + """开发环境配置""" + DEBUG = True + SQLALCHEMY_ECHO = True + +class ProductionConfig(Config): + """生产环境配置""" + DEBUG = False + SQLALCHEMY_ECHO = False + +# 配置字典 +config = { + 'development': DevelopmentConfig, + 'production': ProductionConfig, + 'default': DevelopmentConfig +} diff --git a/init_db.py b/init_db.py new file mode 100644 index 0000000..1cf8e07 --- /dev/null +++ b/init_db.py @@ -0,0 +1,117 @@ +# -*- coding: utf-8 -*- +""" +数据库初始化脚本 +用于创建数据库表和初始化示例数据 +""" + +import os +import sys +from app import create_app +from models import db, Site, Tag, Admin + +# 设置UTF-8编码输出 +if sys.platform.startswith('win'): + import io + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') + +def init_database(): + """初始化数据库""" + app = create_app('development') + + with app.app_context(): + print("正在创建数据库表...") + # 删除所有表(开发环境) + db.drop_all() + # 创建所有表 + db.create_all() + print("✓ 数据库表创建成功") + + # 创建默认管理员 + print("\n正在创建默认管理员...") + admin = Admin( + username='admin', + email='admin@example.com', + is_active=True + ) + admin.set_password('admin123') # 默认密码 + db.session.add(admin) + print("✓ 默认管理员创建成功") + print(" 用户名: admin") + print(" 密码: admin123") + + # 创建示例标签 + print("\n正在创建示例标签...") + tags_data = [ + {'name': 'AI对话', 'slug': 'ai-chat', 'description': 'AI聊天和对话工具', 'icon': 'fas fa-comments', 'sort_order': 100}, + {'name': '图像生成', 'slug': 'image-gen', 'description': 'AI图像生成和编辑工具', 'icon': 'fas fa-image', 'sort_order': 90}, + {'name': '视频制作', 'slug': 'video', 'description': 'AI视频生成和编辑工具', 'icon': 'fas fa-video', 'sort_order': 80}, + {'name': '写作助手', 'slug': 'writing', 'description': 'AI写作和文本生成工具', 'icon': 'fas fa-pen', 'sort_order': 70}, + {'name': '代码助手', 'slug': 'coding', 'description': 'AI编程和代码生成工具', 'icon': 'fas fa-code', 'sort_order': 60}, + {'name': '音频处理', 'slug': 'audio', 'description': 'AI音频生成和处理工具', 'icon': 'fas fa-music', 'sort_order': 50}, + ] + + tags = [] + for tag_data in tags_data: + tag = Tag(**tag_data) + db.session.add(tag) + tags.append(tag) + db.session.commit() + print(f"✓ 创建了 {len(tags)} 个示例标签") + + # 创建示例网站 + print("\n正在创建示例网站...") + sites_data = [ + { + 'name': 'ChatGPT', + 'url': 'https://chat.openai.com', + 'slug': 'chatgpt', + 'short_desc': '最强大的AI对话助手,可以回答问题、写作、编程等', + 'description': 'ChatGPT是OpenAI开发的大型语言模型,能够进行自然对话、回答问题、协助写作、编程等多种任务。它基于GPT-4架构,拥有强大的理解和生成能力。', + 'features': '• 自然语言对话\n• 代码编写和调试\n• 文章写作和润色\n• 数据分析\n• 创意头脑风暴', + 'tags': [tags[0], tags[3], tags[4]], + 'sort_order': 100 + }, + { + 'name': 'Midjourney', + 'url': 'https://www.midjourney.com', + 'slug': 'midjourney', + 'short_desc': '顶级AI绘画工具,可以根据文字描述生成精美图片', + 'description': 'Midjourney是一款强大的AI图像生成工具,通过简单的文字描述就能创作出高质量的艺术作品。支持多种艺术风格,广泛应用于设计、插画等领域。', + 'features': '• 文字转图像\n• 多种艺术风格\n• 高清图片输出\n• 图片变体生成\n• 社区画廊', + 'tags': [tags[1]], + 'sort_order': 95 + }, + { + 'name': 'GitHub Copilot', + 'url': 'https://github.com/features/copilot', + 'slug': 'github-copilot', + 'short_desc': 'AI编程助手,帮助你更快地编写代码', + 'description': 'GitHub Copilot是由GitHub和OpenAI联合开发的AI编程助手,可以根据上下文自动建议代码补全,支持多种编程语言。', + 'features': '• 智能代码补全\n• 多语言支持\n• 函数生成\n• 代码注释生成\n• IDE集成', + 'tags': [tags[4]], + 'sort_order': 85 + }, + ] + + for site_data in sites_data: + site = Site(**site_data) + db.session.add(site) + db.session.commit() + print(f"✓ 创建了 {len(sites_data)} 个示例网站") + + print("\n" + "="*50) + print("数据库初始化完成!") + print("="*50) + print("\n你可以使用以下命令启动应用:") + print(" python app.py") + print("\n然后访问:") + print(" 前台: http://localhost:5000") + print(" 后台: http://localhost:5000/admin") + print(" 登录: http://localhost:5000/admin/login") + print("\n管理员账号:") + print(" 用户名: admin") + print(" 密码: admin123") + print("\n⚠️ 请在生产环境中修改默认密码!") + +if __name__ == '__main__': + init_database() diff --git a/models.py b/models.py new file mode 100644 index 0000000..1c3ccfd --- /dev/null +++ b/models.py @@ -0,0 +1,102 @@ +from datetime import datetime +from flask_sqlalchemy import SQLAlchemy +from flask_login import UserMixin +from werkzeug.security import generate_password_hash, check_password_hash + +db = SQLAlchemy() + +# 网站和标签的多对多关系表 +site_tags = db.Table('site_tags', + db.Column('site_id', db.Integer, db.ForeignKey('sites.id'), primary_key=True), + db.Column('tag_id', db.Integer, db.ForeignKey('tags.id'), primary_key=True) +) + +class Site(db.Model): + """网站模型""" + __tablename__ = 'sites' + + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(100), nullable=False, comment='网站名称') + url = db.Column(db.String(500), nullable=False, comment='网站URL') + slug = db.Column(db.String(100), unique=True, nullable=False, comment='URL别名') + logo = db.Column(db.String(500), comment='Logo图片路径') + short_desc = db.Column(db.String(200), comment='简短描述') + description = db.Column(db.Text, comment='详细介绍') + features = db.Column(db.Text, comment='主要功能') + is_active = db.Column(db.Boolean, default=True, comment='是否启用') + view_count = db.Column(db.Integer, default=0, comment='浏览次数') + sort_order = db.Column(db.Integer, default=0, comment='排序权重') + created_at = db.Column(db.DateTime, default=datetime.now, comment='创建时间') + updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now, comment='更新时间') + + # 关联标签 + tags = db.relationship('Tag', secondary=site_tags, lazy='subquery', + backref=db.backref('sites', lazy=True)) + + def __repr__(self): + return f'' + + def to_dict(self): + """转换为字典""" + return { + 'id': self.id, + 'name': self.name, + 'url': self.url, + 'slug': self.slug, + 'logo': self.logo, + 'short_desc': self.short_desc, + 'description': self.description, + 'features': self.features, + 'is_active': self.is_active, + 'view_count': self.view_count, + 'tags': [tag.name for tag in self.tags], + 'created_at': self.created_at.strftime('%Y-%m-%d %H:%M:%S') if self.created_at else None + } + +class Tag(db.Model): + """标签模型""" + __tablename__ = 'tags' + + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(50), unique=True, nullable=False, comment='标签名称') + slug = db.Column(db.String(50), unique=True, nullable=False, comment='URL别名') + description = db.Column(db.String(200), comment='标签描述') + icon = db.Column(db.String(100), comment='图标') + sort_order = db.Column(db.Integer, default=0, comment='排序权重') + created_at = db.Column(db.DateTime, default=datetime.now, comment='创建时间') + + def __repr__(self): + return f'' + + def to_dict(self): + """转换为字典""" + return { + 'id': self.id, + 'name': self.name, + 'slug': self.slug, + 'description': self.description, + 'icon': self.icon + } + +class Admin(UserMixin, db.Model): + """管理员模型""" + __tablename__ = 'admins' + + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(50), unique=True, nullable=False, comment='用户名') + password_hash = db.Column(db.String(255), nullable=False, comment='密码哈希') + email = db.Column(db.String(100), comment='邮箱') + is_active = db.Column(db.Boolean, default=True, comment='是否启用') + created_at = db.Column(db.DateTime, default=datetime.now, comment='创建时间') + last_login = db.Column(db.DateTime, comment='最后登录时间') + + def set_password(self, password): + """设置密码""" + self.password_hash = generate_password_hash(password) + + def check_password(self, password): + """验证密码""" + return check_password_hash(self.password_hash, password) + + def __repr__(self): + return f'' diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c46b86a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,12 @@ +Flask==3.0.0 +Flask-SQLAlchemy==3.1.1 +Flask-Admin==1.6.1 +Flask-Login==0.6.3 +pymysql==1.1.0 +python-dotenv==1.0.0 +Werkzeug==3.0.1 +cryptography==41.0.7 +WTForms==2.3.3 +requests==2.31.0 +beautifulsoup4==4.12.2 +Pillow>=10.2.0 diff --git a/static/css/admin-theme.css b/static/css/admin-theme.css new file mode 100644 index 0000000..0a32977 --- /dev/null +++ b/static/css/admin-theme.css @@ -0,0 +1,313 @@ +/* ========== Flask-Admin 后台科技感主题 - ZJPB焦提示词 ========== */ + +/* 深色主题覆盖 */ +body.admin-theme { + background: #111618 !important; + background-image: + radial-gradient(at 20% 20%, rgba(37, 192, 244, 0.08) 0px, transparent 50%), + radial-gradient(at 80% 80%, rgba(124, 58, 237, 0.08) 0px, transparent 50%); + color: #ffffff !important; + font-family: 'Space Grotesk', 'Noto Sans', sans-serif !important; +} + +/* 导航栏 */ +.navbar-admin { + background: rgba(27, 36, 39, 0.95) !important; + backdrop-filter: blur(20px); + border-bottom: 1px solid #283539; + box-shadow: 0 4px 30px rgba(0, 0, 0, 0.3); +} + +.navbar-admin .navbar-brand { + background: linear-gradient(to right, #25c0f4, #c084fc); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + font-weight: 700; + font-family: 'Space Grotesk', sans-serif !important; +} + +/* 侧边栏 */ +.nav-sidebar { + background: rgba(27, 36, 39, 0.8) !important; +} + +.nav-sidebar .nav-link { + color: #9cb2ba !important; + transition: all 0.3s ease; +} + +.nav-sidebar .nav-link:hover, +.nav-sidebar .nav-link.active { + color: #ffffff !important; + background: rgba(37, 192, 244, 0.15) !important; + border-left: 3px solid #25c0f4; +} + +/* 卡片和面板 */ +.card, .panel { + background: rgba(27, 36, 39, 0.6) !important; + backdrop-filter: blur(20px); + border: 1px solid #283539 !important; + border-radius: 12px !important; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); +} + +.card-header, .panel-heading { + background: rgba(37, 192, 244, 0.08) !important; + border-bottom: 1px solid #283539 !important; + color: #ffffff !important; + font-weight: 600; +} + +/* 表格 */ +.table { + color: #ffffff !important; +} + +.table thead th { + background: rgba(30, 39, 44, 0.8) !important; + border-color: #283539 !important; + color: #9cb2ba !important; + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.table tbody tr { + background: transparent !important; + border-color: #283539 !important; + transition: all 0.2s ease; +} + +.table tbody tr:hover { + background: rgba(30, 39, 44, 0.5) !important; +} + +.table td, .table th { + border-color: #283539 !important; + color: #ffffff !important; +} + +/* 表单 */ +.form-control { + background: #111618 !important; + border: 1px solid #283539 !important; + color: #ffffff !important; + border-radius: 8px !important; + transition: all 0.3s ease; +} + +.form-control:focus { + background: rgba(27, 36, 39, 0.8) !important; + border-color: #25c0f4 !important; + box-shadow: 0 0 0 3px rgba(37, 192, 244, 0.1) !important; +} + +.form-control::placeholder { + color: #9cb2ba !important; +} + +.form-label, label { + color: #9cb2ba !important; + font-weight: 500; + font-size: 13px; +} + +/* 按钮 */ +.btn-primary { + background: #25c0f4 !important; + border: none !important; + color: #111618 !important; + font-weight: 600; + box-shadow: 0 0 20px rgba(37, 192, 244, 0.3); + transition: all 0.3s ease; +} + +.btn-primary:hover { + background: #1fa8d8 !important; + transform: translateY(-2px); + box-shadow: 0 0 30px rgba(37, 192, 244, 0.5); +} + +.btn-info { + background: linear-gradient(135deg, #25c0f4 0%, #00f2fe 100%) !important; + border: none !important; + color: #111618 !important; + font-weight: 600; +} + +.btn-secondary, .btn-default { + background: rgba(40, 53, 57, 0.6) !important; + border: 1px solid #283539 !important; + color: #ffffff !important; + transition: all 0.3s ease; +} + +.btn-secondary:hover, .btn-default:hover { + background: rgba(52, 66, 71, 0.8) !important; + border-color: #4a5a60 !important; +} + +/* 模态框 */ +.modal-content { + background: rgba(27, 36, 39, 0.95) !important; + backdrop-filter: blur(20px); + border: 1px solid #283539 !important; + border-radius: 12px !important; +} + +.modal-header { + border-bottom-color: #283539 !important; + background: rgba(37, 192, 244, 0.05); +} + +.modal-footer { + border-top-color: #283539 !important; +} + +/* 分页 */ +.pagination .page-link { + background: rgba(27, 36, 39, 0.6) !important; + border-color: #283539 !important; + color: #9cb2ba !important; + transition: all 0.2s ease; +} + +.pagination .page-link:hover { + background: rgba(37, 192, 244, 0.1) !important; + border-color: #25c0f4 !important; + color: #ffffff !important; +} + +.pagination .page-item.active .page-link { + background: #25c0f4 !important; + border-color: #25c0f4 !important; + color: #111618 !important; +} + +/* 警告框 */ +.alert { + background: rgba(27, 36, 39, 0.8) !important; + border: 1px solid #283539 !important; + color: #ffffff !important; + border-radius: 8px !important; +} + +.alert-success { + background: rgba(34, 197, 94, 0.1) !important; + border-color: rgba(34, 197, 94, 0.3) !important; + color: #4ade80 !important; +} + +.alert-danger { + background: rgba(239, 68, 68, 0.1) !important; + border-color: rgba(239, 68, 68, 0.3) !important; + color: #f87171 !important; +} + +.alert-info { + background: rgba(37, 192, 244, 0.1) !important; + border-color: rgba(37, 192, 244, 0.3) !important; + color: #25c0f4 !important; +} + +.alert-warning { + background: rgba(251, 191, 36, 0.1) !important; + border-color: rgba(251, 191, 36, 0.3) !important; + color: #fbbf24 !important; +} + +/* 链接 */ +a { + color: #25c0f4 !important; + text-decoration: none; + transition: color 0.2s ease; +} + +a:hover { + color: #1fa8d8 !important; +} + +/* 文本颜色 */ +.text-muted { + color: #9cb2ba !important; +} + +/* 输入组 */ +.input-group-text { + background: rgba(27, 36, 39, 0.6) !important; + border-color: #283539 !important; + color: #9cb2ba !important; +} + +/* Select2 下拉框 */ +.select2-container--bootstrap4 .select2-selection { + background: #111618 !important; + border-color: #283539 !important; + color: #ffffff !important; +} + +.select2-dropdown { + background: rgba(27, 36, 39, 0.95) !important; + border-color: #283539 !important; + backdrop-filter: blur(10px); +} + +.select2-results__option { + color: #ffffff !important; +} + +.select2-results__option--highlighted { + background: rgba(37, 192, 244, 0.2) !important; +} + +/* 徽章 */ +.badge-primary { + background: #25c0f4 !important; + color: #111618 !important; +} + +.badge-secondary { + background: #283539 !important; + color: #9cb2ba !important; +} + +/* 进度条 */ +.progress { + background: rgba(27, 36, 39, 0.6) !important; +} + +.progress-bar { + background: #25c0f4 !important; +} + +/* 额外优化 */ +.navbar-nav .nav-link { + color: #9cb2ba !important; +} + +.navbar-nav .nav-link:hover { + color: #ffffff !important; +} + +/* 自定义滚动条 */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: #111618; +} + +::-webkit-scrollbar-thumb { + background: #283539; + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: #3a4b50; +} + diff --git a/static/css/style.css b/static/css/style.css new file mode 100644 index 0000000..fd17cbf --- /dev/null +++ b/static/css/style.css @@ -0,0 +1,614 @@ +/* ========== 科技感/未来风主题样式 ========== */ + +/* 全局变量 */ +:root { + --primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + --secondary-gradient: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); + --tech-gradient: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); + --dark-bg: #0a0e27; + --dark-card: rgba(15, 23, 42, 0.8); + --glass-bg: rgba(255, 255, 255, 0.05); + --glass-border: rgba(255, 255, 255, 0.1); + --text-primary: #ffffff; + --text-secondary: #a0aec0; + --glow-color: #667eea; + --success-glow: #00f2fe; +} + +/* 全局样式 */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Microsoft YaHei', sans-serif; + background: var(--dark-bg); + background-image: + radial-gradient(at 0% 0%, rgba(102, 126, 234, 0.2) 0px, transparent 50%), + radial-gradient(at 100% 0%, rgba(118, 75, 162, 0.2) 0px, transparent 50%), + radial-gradient(at 100% 100%, rgba(79, 172, 254, 0.2) 0px, transparent 50%), + radial-gradient(at 0% 100%, rgba(0, 242, 254, 0.2) 0px, transparent 50%); + background-attachment: fixed; + color: var(--text-primary); + min-height: 100vh; +} + +/* 导航栏样式 */ +.navbar { + background: var(--dark-card) !important; + backdrop-filter: blur(20px); + border-bottom: 1px solid var(--glass-border); + box-shadow: 0 4px 30px rgba(0, 0, 0, 0.3); +} + +.navbar-brand { + font-weight: 700; + font-size: 1.4rem; + background: var(--primary-gradient); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + transition: all 0.3s ease; +} + +.navbar-brand:hover { + transform: translateY(-2px); + filter: brightness(1.2); +} + +.navbar-brand i { + background: var(--tech-gradient); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.nav-link { + color: var(--text-secondary) !important; + font-weight: 500; + transition: all 0.3s ease; + position: relative; +} + +.nav-link:hover { + color: var(--text-primary) !important; +} + +.nav-link::after { + content: ''; + position: absolute; + bottom: 0; + left: 50%; + width: 0; + height: 2px; + background: var(--tech-gradient); + transition: all 0.3s ease; + transform: translateX(-50%); +} + +.nav-link:hover::after { + width: 80%; +} + +/* 页面标题 */ +.page-header { + text-align: center; + padding: 4rem 0 2rem; +} + +.page-title { + font-size: 3.5rem; + font-weight: 800; + background: var(--primary-gradient); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + margin-bottom: 1rem; + text-shadow: 0 0 40px rgba(102, 126, 234, 0.3); +} + +.page-subtitle { + color: var(--text-secondary); + font-size: 1.2rem; + font-weight: 300; +} + +/* 标签筛选 */ +.tag-filter, .tags-filter { + display: flex; + flex-wrap: wrap; + gap: 1rem; + justify-content: center; + padding: 2rem 0; +} + +.tag-item { + padding: 0.75rem 1.5rem; + background: var(--glass-bg); + backdrop-filter: blur(10px); + border: 1px solid var(--glass-border); + border-radius: 50px; + color: var(--text-secondary); + text-decoration: none; + font-weight: 500; + transition: all 0.3s ease; + position: relative; + overflow: hidden; +} + +.tag-item::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: var(--tech-gradient); + transition: all 0.4s ease; + z-index: -1; +} + +.tag-item:hover { + color: var(--text-primary); + border-color: transparent; + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(79, 172, 254, 0.3); +} + +.tag-item:hover::before { + left: 0; +} + +.tag-item.active { + background: var(--primary-gradient); + color: var(--text-primary); + border-color: transparent; + box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4); +} + +.tag-item i { + margin-right: 0.5rem; +} + +/* 网站卡片 */ +.site-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + gap: 2rem; + padding: 2rem 0; +} + +.site-card { + background: var(--glass-bg); + backdrop-filter: blur(20px); + border: 1px solid var(--glass-border); + border-radius: 20px; + padding: 2rem; + transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); + cursor: pointer; + position: relative; + overflow: hidden; +} + +.site-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: var(--primary-gradient); + opacity: 0; + transition: opacity 0.4s ease; + z-index: 0; +} + +.site-card:hover { + transform: translateY(-10px) scale(1.02); + border-color: rgba(102, 126, 234, 0.5); + box-shadow: + 0 20px 60px rgba(102, 126, 234, 0.3), + 0 0 40px rgba(79, 172, 254, 0.2); +} + +.site-card:hover::before { + opacity: 0.1; +} + +.site-card > * { + position: relative; + z-index: 1; +} + +.site-logo { + width: 80px; + height: 80px; + object-fit: contain; + margin-bottom: 1.5rem; + border-radius: 15px; + background: rgba(255, 255, 255, 0.05); + padding: 10px; + transition: all 0.3s ease; +} + +.site-card:hover .site-logo { + transform: scale(1.1) rotate(5deg); + filter: drop-shadow(0 0 20px rgba(79, 172, 254, 0.5)); +} + +.site-logo-placeholder { + width: 80px; + height: 80px; + background: var(--tech-gradient); + border-radius: 15px; + display: flex; + align-items: center; + justify-content: center; + color: white; + font-size: 32px; + margin-bottom: 1.5rem; +} + +.site-name, .card-title { + font-size: 1.5rem; + font-weight: 700; + color: var(--text-primary); + margin-bottom: 0.75rem; +} + +.site-desc, .card-text { + color: var(--text-secondary); + font-size: 0.95rem; + line-height: 1.6; + margin-bottom: 1rem; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.site-tags { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin-top: 1rem; +} + +.site-tags .badge { + padding: 0.4rem 0.8rem; + background: rgba(79, 172, 254, 0.1); + border: 1px solid rgba(79, 172, 254, 0.3); + border-radius: 20px; + color: #4facfe; + font-size: 0.85rem; + font-weight: 500; +} + +.site-meta { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 1.5rem; + padding-top: 1rem; + border-top: 1px solid var(--glass-border); + color: var(--text-secondary); + font-size: 0.9rem; +} + +.site-meta i { + margin-right: 0.3rem; + color: #4facfe; +} + +/* 详情页样式 */ +.detail-container { + max-width: 900px; + margin: 0 auto; + padding: 3rem 1rem; +} + +.detail-header { + background: var(--glass-bg); + backdrop-filter: blur(20px); + border: 1px solid var(--glass-border); + border-radius: 20px; + padding: 3rem; + margin-bottom: 2rem; + text-align: center; +} + +.site-logo-large { + width: 120px !important; + height: 120px !important; + margin: 0 auto 2rem; + padding: 15px; + background: rgba(255, 255, 255, 0.05); + border-radius: 25px; + border: 2px solid var(--glass-border); +} + +.detail-title { + font-size: 2.5rem; + font-weight: 800; + margin-bottom: 1rem; + background: var(--primary-gradient); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.detail-url { + display: inline-block; + padding: 1rem 2rem; + background: var(--tech-gradient); + color: white; + text-decoration: none; + border-radius: 50px; + font-weight: 600; + transition: all 0.3s ease; + box-shadow: 0 8px 25px rgba(79, 172, 254, 0.3); +} + +.detail-url:hover { + transform: translateY(-3px); + box-shadow: 0 12px 35px rgba(79, 172, 254, 0.5); + color: white; +} + +.detail-section, .site-description, .site-features { + background: var(--glass-bg); + backdrop-filter: blur(20px); + border: 1px solid var(--glass-border); + border-radius: 20px; + padding: 2.5rem; + margin-bottom: 2rem; +} + +.detail-section h3 { + font-size: 1.5rem; + font-weight: 700; + margin-bottom: 1.5rem; + color: var(--text-primary); + display: flex; + align-items: center; +} + +.detail-section h3 i { + margin-right: 0.75rem; + background: var(--tech-gradient); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.detail-section p, +.detail-section ul, +.site-description p, +.site-description ul, +.site-features ul { + color: var(--text-secondary); + line-height: 1.8; + font-size: 1.05rem; +} + +.detail-section ul, +.site-description ul, +.site-features ul { + list-style: none; + padding: 0; +} + +.detail-section li, +.site-description li, +.site-features li { + padding: 0.75rem 0; + padding-left: 2rem; + position: relative; +} + +.detail-section li::before, +.site-description li::before, +.site-features li::before { + content: '▸'; + position: absolute; + left: 0; + color: #4facfe; + font-size: 1.2rem; +} + +/* 页脚 */ +footer { + background: var(--dark-card) !important; + backdrop-filter: blur(20px); + border-top: 1px solid var(--glass-border); + margin-top: 5rem; +} + +/* 响应式设计 */ +@media (max-width: 768px) { + .page-title { + font-size: 2.5rem; + } + + .site-grid { + grid-template-columns: 1fr; + gap: 1.5rem; + } + + .tag-filter, .tags-filter { + gap: 0.75rem; + } + + .tag-item { + padding: 0.6rem 1.2rem; + font-size: 0.9rem; + } + + .site-logo-large { + width: 80px !important; + height: 80px !important; + } +} + +/* 按钮样式 */ +.btn-primary { + background: var(--primary-gradient) !important; + border: none !important; + padding: 0.75rem 2rem; + border-radius: 50px; + font-weight: 600; + transition: all 0.3s ease; + box-shadow: 0 8px 25px rgba(102, 126, 234, 0.3); +} + +.btn-primary:hover { + transform: translateY(-3px); + box-shadow: 0 12px 35px rgba(102, 126, 234, 0.5); +} + +.btn-secondary { + background: var(--glass-bg) !important; + backdrop-filter: blur(10px); + border: 1px solid var(--glass-border) !important; + color: var(--text-primary) !important; + padding: 0.75rem 2rem; + border-radius: 50px; + font-weight: 600; + transition: all 0.3s ease; +} + +.btn-secondary:hover { + background: rgba(255, 255, 255, 0.1) !important; + border-color: rgba(255, 255, 255, 0.3) !important; +} + +.btn { + border-radius: 50px; + padding: 0.75rem 1.5rem; + font-weight: 600; + transition: all 0.3s ease; +} + +.btn:hover { + transform: translateY(-2px); +} + +/* 表单样式 */ +.form-control { + background: var(--glass-bg) !important; + border: 1px solid var(--glass-border) !important; + color: var(--text-primary) !important; + border-radius: 15px; + padding: 0.75rem 1.25rem; +} + +.form-control:focus { + background: rgba(255, 255, 255, 0.1) !important; + border-color: #4facfe !important; + box-shadow: 0 0 20px rgba(79, 172, 254, 0.3) !important; + color: var(--text-primary) !important; +} + +.form-control::placeholder { + color: var(--text-secondary); +} + +.form-label { + color: var(--text-secondary); + font-weight: 600; + margin-bottom: 0.75rem; +} + +/* 卡片样式 */ +.card { + background: var(--glass-bg) !important; + backdrop-filter: blur(20px); + border: 1px solid var(--glass-border) !important; + border-radius: 20px !important; +} + +.card-body { + color: var(--text-primary); +} + +/* 警告框样式 */ +.alert { + background: var(--glass-bg) !important; + backdrop-filter: blur(20px); + border: 1px solid var(--glass-border) !important; + border-radius: 15px !important; + color: var(--text-primary) !important; +} + +.alert-danger { + border-color: rgba(245, 87, 108, 0.5) !important; + background: rgba(245, 87, 108, 0.1) !important; +} + +.alert-success { + border-color: rgba(0, 242, 254, 0.5) !important; + background: rgba(0, 242, 254, 0.1) !important; +} + +/* 空状态样式 */ +.empty-state { + padding: 60px 20px; + text-align: center; + color: var(--text-secondary); +} + +.empty-state i { + font-size: 4rem; + background: var(--primary-gradient); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + margin-bottom: 1rem; +} + +/* 滚动条样式 */ +::-webkit-scrollbar { + width: 10px; + height: 10px; +} + +::-webkit-scrollbar-track { + background: var(--dark-bg); +} + +::-webkit-scrollbar-thumb { + background: var(--glass-bg); + border-radius: 5px; + border: 1px solid var(--glass-border); +} + +::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.15); +} + +/* 加载动画 */ +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.site-card { + animation: fadeIn 0.5s ease-out; +} + +@keyframes glow { + 0%, 100% { + box-shadow: 0 0 20px rgba(102, 126, 234, 0.5); + } + 50% { + box-shadow: 0 0 40px rgba(79, 172, 254, 0.8); + } +} diff --git a/static/js/main.js b/static/js/main.js new file mode 100644 index 0000000..5045729 --- /dev/null +++ b/static/js/main.js @@ -0,0 +1,121 @@ +// 主要JavaScript功能 + +document.addEventListener('DOMContentLoaded', function() { + // 初始化所有功能 + initCardAnimations(); + initExternalLinks(); + initTooltips(); +}); + +/** + * 卡片动画效果 + */ +function initCardAnimations() { + const cards = document.querySelectorAll('.site-card'); + + cards.forEach((card, index) => { + // 添加延迟动画效果 + card.style.animationDelay = `${index * 0.05}s`; + + // 点击卡片查看详情 + card.addEventListener('click', function(e) { + // 如果点击的是按钮,不触发卡片点击 + if (e.target.tagName === 'A' || e.target.closest('a')) { + return; + } + + // 找到"查看详情"按钮并触发点击 + const detailBtn = card.querySelector('a[href*="site_detail"]'); + if (detailBtn) { + window.location.href = detailBtn.href; + } + }); + }); +} + +/** + * 外部链接处理 + */ +function initExternalLinks() { + const externalLinks = document.querySelectorAll('a[target="_blank"]'); + + externalLinks.forEach(link => { + // 添加安全属性 + link.setAttribute('rel', 'noopener noreferrer'); + + // 添加点击统计(可选) + link.addEventListener('click', function() { + console.log('External link clicked:', this.href); + // 这里可以添加统计代码 + }); + }); +} + +/** + * 初始化Bootstrap提示框 + */ +function initTooltips() { + const tooltipTriggerList = [].slice.call( + document.querySelectorAll('[data-bs-toggle="tooltip"]') + ); + tooltipTriggerList.map(function(tooltipTriggerEl) { + return new bootstrap.Tooltip(tooltipTriggerEl); + }); +} + +/** + * 平滑滚动到顶部 + */ +function scrollToTop() { + window.scrollTo({ + top: 0, + behavior: 'smooth' + }); +} + +/** + * 显示加载动画 + */ +function showLoading() { + const loader = document.createElement('div'); + loader.id = 'loading'; + loader.className = 'loading-overlay'; + loader.innerHTML = '
'; + document.body.appendChild(loader); +} + +/** + * 隐藏加载动画 + */ +function hideLoading() { + const loader = document.getElementById('loading'); + if (loader) { + loader.remove(); + } +} + +/** + * 显示通知消息 + */ +function showNotification(message, type = 'info') { + const alert = document.createElement('div'); + alert.className = `alert alert-${type} alert-dismissible fade show position-fixed top-0 start-50 translate-middle-x mt-3`; + alert.style.zIndex = '9999'; + alert.innerHTML = ` + ${message} + + `; + + document.body.appendChild(alert); + + // 3秒后自动关闭 + setTimeout(() => { + alert.remove(); + }, 3000); +} + +// 导出函数供全局使用 +window.scrollToTop = scrollToTop; +window.showLoading = showLoading; +window.hideLoading = hideLoading; +window.showNotification = showNotification; diff --git a/static/uploads/.gitkeep b/static/uploads/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/stitch_ai_tool_detail_page/stitch_ai_tool_detail_page/admin_login_page/code.html b/stitch_ai_tool_detail_page/stitch_ai_tool_detail_page/admin_login_page/code.html new file mode 100644 index 0000000..4cd9494 --- /dev/null +++ b/stitch_ai_tool_detail_page/stitch_ai_tool_detail_page/admin_login_page/code.html @@ -0,0 +1,126 @@ + + + + + +Admin Login - AI Discovery + + + + + + + + + + + + +
+
+
+ +
+ + +
+arrow_back +
+Back to Home +
+ +
+ +
+ +
+
+
+shield_person +
+System Access +
+

管理员登录

+

Enter your credentials to access the AI Control Panel.

+
+ + + +
+ +
+ +
+ +
+person +
+
+
+ +
+
+ +Forgot password? +
+
+ +
+lock +
+ +
+
+ + +
+ +
+

+ Protected by reCAPTCHA. Privacy & Terms. +

+
+
+
+ \ No newline at end of file diff --git a/stitch_ai_tool_detail_page/stitch_ai_tool_detail_page/admin_login_page/screen.png b/stitch_ai_tool_detail_page/stitch_ai_tool_detail_page/admin_login_page/screen.png new file mode 100644 index 0000000..e666f9e Binary files /dev/null and b/stitch_ai_tool_detail_page/stitch_ai_tool_detail_page/admin_login_page/screen.png differ diff --git a/stitch_ai_tool_detail_page/stitch_ai_tool_detail_page/admin_management_interface/code.html b/stitch_ai_tool_detail_page/stitch_ai_tool_detail_page/admin_management_interface/code.html new file mode 100644 index 0000000..c42420c --- /dev/null +++ b/stitch_ai_tool_detail_page/stitch_ai_tool_detail_page/admin_management_interface/code.html @@ -0,0 +1,394 @@ + + + + + +Admin Management Interface + + + + + + + + + + + + + + + + + +
+ +
+ +
+Dashboard +/ +Website Management +
+ +
+ + + + + + +
+
+ +
+
+ +
+
+

Website Management

+

Manage and curate AI tools for the discovery platform.

+
+
+ + +
+
+ +
+ +
+ +
+
+ +search + + +
+
+ Showing 1-10 of 128 +
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +Website / ToolTagsStatusActions
+ + +
+
+
+

ChatGPT

+

openai.com/chatgpt

+
+
+
+
+LLM +Chatbot +
+
+ + Active + + +
+ + +
+
+ + +
+
+
+

Midjourney

+

midjourney.com

+
+
+
+
+Image Gen +Art +
+
+ + Active + + +
+ + +
+
+ + +
+
+
+

Copy.ai

+

copy.ai

+
+
+
+
+Writing +Marketing +
+
+ + Pending + + +
+ + +
+
+
+ + +
+
+ +
+
+
+

Add New Website

+close +
+
+ +
+ +
+
+ +
+ +
+
+ + +

+ Our AI will attempt to scrape metadata and descriptions automatically. +

+
+
+ +
+ + +
+
+ + +
+
+ +
+ + Productivity + close + + +
+
+ +
+ + +
+
+
+ + +
+
+
+
+
+
+
+ \ No newline at end of file diff --git a/stitch_ai_tool_detail_page/stitch_ai_tool_detail_page/admin_management_interface/screen.png b/stitch_ai_tool_detail_page/stitch_ai_tool_detail_page/admin_management_interface/screen.png new file mode 100644 index 0000000..9dc84d7 Binary files /dev/null and b/stitch_ai_tool_detail_page/stitch_ai_tool_detail_page/admin_management_interface/screen.png differ diff --git a/stitch_ai_tool_detail_page/stitch_ai_tool_detail_page/ai_tool_detail_page/code.html b/stitch_ai_tool_detail_page/stitch_ai_tool_detail_page/ai_tool_detail_page/code.html new file mode 100644 index 0000000..59939eb --- /dev/null +++ b/stitch_ai_tool_detail_page/stitch_ai_tool_detail_page/ai_tool_detail_page/code.html @@ -0,0 +1,270 @@ + + + + + +AI Tool Detail Page + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+token +
+

AI Discovery

+
+ + +
+ +
+ + +
+
+
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+
+
+
+ +
+ + +
+Generative AI +Copywriting +Productivity +Free Trial +
+ +
+
+visibility +12,450 Views +
+
+calendar_today +Added Oct 24, 2023 +
+
+
+
+
+ +
+ +
+

+info + Product Overview +

+

+ NeuroGen AI is an advanced copywriting assistant designed to help marketers, writers, and businesses generate high-converting content in seconds. By leveraging state-of-the-art natural language processing models, it understands context, tone, and brand voice to deliver tailored outputs. +

+
+ +
+
+
+
+
+

Dashboard Interface

+
+
+ +
+

Detailed Description

+
+

+ Writing high-quality content consistently is a challenge for modern businesses. NeuroGen AI bridges the gap between human creativity and machine speed. Unlike generic text generators, NeuroGen allows users to fine-tune specific parameters such as emotional resonance, sentence structure complexity, and SEO keyword density. +

+

+ The platform includes specialized templates for: +

+
    +
  • Social media captions (Instagram, LinkedIn, Twitter)
  • +
  • Long-form blog posts with automatic formatting
  • +
  • Email marketing sequences
  • +
  • Ad copy variants for A/B testing
  • +
+

+ Security is paramount; NeuroGen ensures that your proprietary data is never used to train public models. Enterprise-grade encryption and team collaboration features make it a suitable choice for large organizations looking to scale their content operations. +

+
+
+
+
+ +
+ +
+
+Try it now +ONLINE +
+ + +
+ + Visit Website + arrow_outward + +
+ +
+ + Visit Website + arrow_outward + +
+
+

+ Opens in a new tab • neurogen.ai +

+
+ +
+

+verified + Main Features +

+
    +
  • +
    +check +
    +
    +

    Contextual Awareness

    +

    Understands previous inputs to maintain thread continuity.

    +
    +
  • +
  • +
    +check +
    +
    +

    Multi-language Support

    +

    Generate content in over 25 languages natively.

    +
    +
  • +
  • +
    +check +
    +
    +

    SEO Optimization

    +

    Built-in keyword analyzer and suggestion tool.

    +
    +
  • +
  • +
    +check +
    +
    +

    Export to CMS

    +

    Direct integration with WordPress and Ghost.

    +
    +
  • +
+
+ +
+ + +
+
+
+ +
+
+ \ No newline at end of file diff --git a/stitch_ai_tool_detail_page/stitch_ai_tool_detail_page/ai_tool_detail_page/screen.png b/stitch_ai_tool_detail_page/stitch_ai_tool_detail_page/ai_tool_detail_page/screen.png new file mode 100644 index 0000000..24fc60f Binary files /dev/null and b/stitch_ai_tool_detail_page/stitch_ai_tool_detail_page/ai_tool_detail_page/screen.png differ diff --git a/stitch_ai_tool_detail_page/stitch_ai_tool_detail_page/ai_tool_home_page/code.html b/stitch_ai_tool_detail_page/stitch_ai_tool_detail_page/ai_tool_home_page/code.html new file mode 100644 index 0000000..561dce5 --- /dev/null +++ b/stitch_ai_tool_detail_page/stitch_ai_tool_detail_page/ai_tool_home_page/code.html @@ -0,0 +1,383 @@ + + + + + +ZJPB - AI Tool Discovery + + + + + + + + + + + + + + +
+ +
+
+ +
+
+ + +
+ + + + + + + +
+
+
+ +
+
+ +
+
+

+ ZJPB - 焦提示词 +

+

+ 发现最新最好用的AI工具和产品. Discover the best AI tools tailored for your workflow. +

+
+ +
+ +
+
+ + + + + + + + +
+
+ +
+ +
+
+
+
+
+arrow_outward +
+
+
+

ChatGPT

+

OpenAI's advanced conversational model capable of understanding and generating human-like text.

+
+
+
+Chat +NLP +
+
+visibility +1.2M +
+
+
+ +
+
+
+
+
+arrow_outward +
+
+
+

Midjourney

+

Hyper-realistic AI image generator that creates stunning visuals from text prompts.

+
+
+
+Image +Art +
+
+visibility +850k +
+
+
+ +
+
+
+
+
+arrow_outward +
+
+
+

Jasper AI

+

AI copywriter for marketing content, blog posts, and social media captions.

+
+
+
+Writing +Marketing +
+
+visibility +300k +
+
+
+ +
+
+
+
+
+arrow_outward +
+
+
+

Runway Gen-2

+

Next-generation video creation tool that turns text into high-quality video clips.

+
+
+
+Video +Gen-AI +
+
+visibility +420k +
+
+
+ +
+
+
+
+
+arrow_outward +
+
+
+

GitHub Copilot

+

Your AI pair programmer that helps you write better code faster.

+
+
+
+Dev +Coding +
+
+visibility +1.5M +
+
+
+ +
+
+
+
+
+arrow_outward +
+
+
+

ElevenLabs

+

The most realistic AI voice generator and text-to-speech software.

+
+
+
+Audio +TTS +
+
+visibility +210k +
+
+
+ +
+
+
+
+
+arrow_outward +
+
+
+

Stable Diffusion

+

Open source latent text-to-image diffusion model for image generation.

+
+
+
+Open Source +Image +
+
+visibility +950k +
+
+
+ +
+
+
+
+
+arrow_outward +
+
+
+

Notion AI

+

Access the limitless power of AI, right inside your Notion workspace.

+
+
+
+Productivity +
+
+visibility +600k +
+
+
+
+ + +
+
+ + +
+ \ No newline at end of file diff --git a/stitch_ai_tool_detail_page/stitch_ai_tool_detail_page/ai_tool_home_page/screen.png b/stitch_ai_tool_detail_page/stitch_ai_tool_detail_page/ai_tool_home_page/screen.png new file mode 100644 index 0000000..8256848 Binary files /dev/null and b/stitch_ai_tool_detail_page/stitch_ai_tool_detail_page/ai_tool_home_page/screen.png differ diff --git a/templates/admin/custom_base.html b/templates/admin/custom_base.html new file mode 100644 index 0000000..090e5ce --- /dev/null +++ b/templates/admin/custom_base.html @@ -0,0 +1,17 @@ +{% extends 'admin/base.html' %} + +{% block head_css %} +{{ super() }} + + + + + + +{% endblock %} + +{% block body %} + +{{ super() }} + +{% endblock %} diff --git a/templates/admin/site/create.html b/templates/admin/site/create.html new file mode 100644 index 0000000..6b87516 --- /dev/null +++ b/templates/admin/site/create.html @@ -0,0 +1,124 @@ +{% extends 'admin/model/create.html' %} + +{% block tail %} +{{ super() }} + + + +{% endblock %} diff --git a/templates/admin/site/edit.html b/templates/admin/site/edit.html new file mode 100644 index 0000000..355867d --- /dev/null +++ b/templates/admin/site/edit.html @@ -0,0 +1,124 @@ +{% extends 'admin/model/edit.html' %} + +{% block tail %} +{{ super() }} + + + +{% endblock %} diff --git a/templates/admin_login.html b/templates/admin_login.html new file mode 100644 index 0000000..5c57f62 --- /dev/null +++ b/templates/admin_login.html @@ -0,0 +1,121 @@ +{% extends "base.html" %} + +{% block title %}管理员登录 - ZJPB 焦提示词{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block content %} + + + + + +
+
+
+ + +
+ + +
+ arrow_back +
+ 返回首页 +
+ + +
+ +
+ + +
+
+
+ shield_person +
+ System Access +
+

管理员登录

+

输入您的登录凭据以访问后台管理系统

+
+ + + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
+ error +

{{ message }}

+
+ {% endfor %} + {% endif %} + {% endwith %} + + +
+ +
+ +
+ +
+ person +
+
+
+ + +
+ +
+ +
+ lock +
+
+
+ + + +
+ + +
+

+ ZJPB - 焦提示词 管理系统 +

+
+
+
+ +{% endblock %} diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..bd94db0 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,125 @@ + + + + + + {% block title %}ZJPB - 焦提示词{% endblock %} + + + + + + + + + + + + + + + + {% block extra_css %}{% endblock %} + + +
+ +
+
+ + +
+
+
+ + +
+ bolt +
+

ZJPB

+
+ + +
+ + +
+ {% if current_user.is_authenticated %} + + {% else %} + + {% endif %} +
+
+
+ + +
+ {% block content %}{% endblock %} +
+ + + +
+ + {% block extra_js %}{% endblock %} + + diff --git a/templates/detail.html b/templates/detail.html new file mode 100644 index 0000000..62cd79a --- /dev/null +++ b/templates/detail.html @@ -0,0 +1,136 @@ +{% extends "base.html" %} + +{% block title %}{{ site.name }} - ZJPB 焦提示词{% endblock %} +{% block description %}{{ site.short_desc or site.description[:150] }}{% endblock %} +{% block keywords %}{{ site.name }},{% for tag in site.tags %}{{ tag.name }},{% endfor %}ZJPB,焦提示词,AI工具{% endblock %} + +{% block content %} +
+ + + +
+ +
+ +
+ +
+
+ {% if site.logo %} +
+ {% else %} +
+ {{ site.name[0] }} +
+ {% endif %} +
+ + +
+ + + +
+ {% for tag in site.tags %} + + {{ tag.name }} + + {% endfor %} +
+ + +
+
+ visibility + {{ site.view_count }} 次浏览 +
+
+ calendar_today + {{ site.created_at.strftime('%Y年%m月%d日') }} +
+
+
+
+ +
+ + +
+ + {% if site.short_desc %} +
+

+ info + 产品简介 +

+

+ {{ site.short_desc }} +

+
+ {% endif %} + + + {% if site.description %} +
+

+ article + 详细介绍 +

+
+ {{ site.description|safe }} +
+
+ {% endif %} + + + {% if site.features %} +
+

+ star + 主要功能 +

+
+ {{ site.features|safe }} +
+
+ {% endif %} +
+
+ + +
+ +
+
+ 立即体验 +
+ + + + 访问网站 + arrow_outward + + + +
+

+ {{ site.url }} +

+
+
+
+
+
+{% endblock %} diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..a0c5899 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,82 @@ +{% extends "base.html" %} + +{% block title %}ZJPB - 焦提示词 - 发现最好用的AI产品{% endblock %} + +{% block content %} +
+
+ +
+
+

+ ZJPB - 焦提示词 +

+

+ 发现最新最好用的AI工具和产品 +

+
+
+ + + + + + {% if sites %} +
+ {% for site in sites %} +
+
+ +
+ {% if site.logo %} +
+ {% else %} +
+ {{ site.name[0] }} +
+ {% endif %} +
+ arrow_outward +
+
+ +
+

{{ site.name }}

+

{{ site.short_desc or '暂无描述' }}

+
+ +
+
+ {% for tag in site.tags[:2] %} + {{ tag.name }} + {% endfor %} +
+
+ visibility + {{ site.view_count }} +
+
+
+ {% endfor %} +
+ {% else %} +
+ search_off +

暂无相关AI工具

+
+ {% endif %} +
+
+{% endblock %} diff --git a/test_db.py b/test_db.py new file mode 100644 index 0000000..066f9ce --- /dev/null +++ b/test_db.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +""" +测试数据库连接 +""" +import sys +import pymysql +from dotenv import load_dotenv +import os + +# 设置UTF-8编码输出 +if sys.platform.startswith('win'): + import io + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') + +load_dotenv() + +def test_connection(): + """测试MySQL连接""" + print("正在测试数据库连接...") + print(f"主机: {os.getenv('DB_HOST')}") + print(f"端口: {os.getenv('DB_PORT')}") + print(f"用户: {os.getenv('DB_USER')}") + print(f"数据库: {os.getenv('DB_NAME')}") + + try: + connection = pymysql.connect( + host=os.getenv('DB_HOST'), + port=int(os.getenv('DB_PORT', 3306)), + user=os.getenv('DB_USER'), + password=os.getenv('DB_PASSWORD'), + database=os.getenv('DB_NAME'), + charset='utf8mb4' + ) + + print("\n✓ 数据库连接成功!") + + # 测试查询 + with connection.cursor() as cursor: + cursor.execute("SELECT VERSION()") + version = cursor.fetchone() + print(f"✓ MySQL版本: {version[0]}") + + cursor.execute("SHOW TABLES") + tables = cursor.fetchall() + if tables: + print(f"✓ 现有表: {len(tables)} 个") + for table in tables: + print(f" - {table[0]}") + else: + print(" 当前数据库为空,可以运行 init_db.py 初始化") + + connection.close() + return True + + except Exception as e: + print(f"\n✗ 数据库连接失败: {str(e)}") + print("\n请检查:") + print("1. 服务器MySQL是否开放了3306端口") + print("2. .env文件中的数据库配置是否正确") + print("3. 数据库用户是否有远程访问权限") + print("4. 服务器防火墙/安全组是否允许3306端口") + return False + +if __name__ == '__main__': + test_connection() diff --git a/test_fetch.py b/test_fetch.py new file mode 100644 index 0000000..6c5b49c --- /dev/null +++ b/test_fetch.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +""" +测试网站信息抓取功能 +""" +import sys +if sys.platform.startswith('win'): + import io + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') + +from utils.website_fetcher import WebsiteFetcher + +def test_fetch(): + """测试抓取百度网站信息""" + print("="*50) + print("测试网站信息抓取功能") + print("="*50) + + # 创建抓取器 + fetcher = WebsiteFetcher(timeout=15) + + # 测试抓取百度 + test_url = "https://www.baidu.com" + print(f"\n正在抓取: {test_url}") + + info = fetcher.fetch_website_info(test_url) + + if info: + print("\n抓取成功!") + print("-"*50) + print(f"网站名称: {info.get('name', '')}") + print(f"网站描述: {info.get('description', '')}") + print(f"Logo URL: {info.get('logo_url', '')}") + print("-"*50) + + # 测试下载Logo + if info.get('logo_url'): + print(f"\n正在下载Logo...") + logo_path = fetcher.download_logo(info['logo_url']) + if logo_path: + print(f"Logo下载成功: {logo_path}") + else: + print("Logo下载失败") + else: + print("\n抓取失败!") + +if __name__ == '__main__': + test_fetch() diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..dd7ee44 --- /dev/null +++ b/utils/__init__.py @@ -0,0 +1 @@ +# Utils package diff --git a/utils/website_fetcher.py b/utils/website_fetcher.py new file mode 100644 index 0000000..b3abe93 --- /dev/null +++ b/utils/website_fetcher.py @@ -0,0 +1,168 @@ +# -*- coding: utf-8 -*- +""" +网站信息抓取工具 +""" +import requests +from bs4 import BeautifulSoup +from urllib.parse import urljoin, urlparse +import os +from PIL import Image +from io import BytesIO + +class WebsiteFetcher: + """网站信息抓取器""" + + def __init__(self, timeout=10): + self.timeout = timeout + self.headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' + } + + def fetch_website_info(self, url): + """ + 抓取网站信息 + + Args: + url: 网站URL + + Returns: + dict: 包含name, description, logo_url的字典,失败返回None + """ + try: + # 确保URL包含协议 + if not url.startswith(('http://', 'https://')): + url = 'https://' + url + + # 请求网页 + response = requests.get(url, headers=self.headers, timeout=self.timeout, allow_redirects=True) + response.raise_for_status() + response.encoding = response.apparent_encoding # 自动检测编码 + + # 解析HTML + soup = BeautifulSoup(response.text, 'html.parser') + + # 提取信息 + info = { + 'name': self._extract_title(soup), + 'description': self._extract_description(soup), + 'logo_url': self._extract_logo(soup, url) + } + + return info + + except Exception as e: + print(f"抓取网站信息失败: {str(e)}") + return None + + def _extract_title(self, soup): + """提取网站标题""" + # 优先使用 og:title + og_title = soup.find('meta', property='og:title') + if og_title and og_title.get('content'): + return og_title['content'].strip() + + # 使用 title 标签 + title_tag = soup.find('title') + if title_tag: + return title_tag.get_text().strip() + + return '' + + def _extract_description(self, soup): + """提取网站描述""" + # 优先使用 og:description + og_desc = soup.find('meta', property='og:description') + if og_desc and og_desc.get('content'): + return og_desc['content'].strip() + + # 使用 meta description + meta_desc = soup.find('meta', attrs={'name': 'description'}) + if meta_desc and meta_desc.get('content'): + return meta_desc['content'].strip() + + # 使用 meta keywords 作为fallback + meta_keywords = soup.find('meta', attrs={'name': 'keywords'}) + if meta_keywords and meta_keywords.get('content'): + return meta_keywords['content'].strip() + + return '' + + def _extract_logo(self, soup, base_url): + """提取网站Logo""" + logo_url = None + + # 1. 尝试 og:image + og_image = soup.find('meta', property='og:image') + if og_image and og_image.get('content'): + logo_url = og_image['content'] + + # 2. 尝试 link rel="icon" 或 "shortcut icon" + if not logo_url: + icon_link = soup.find('link', rel=lambda x: x and ('icon' in x.lower() if isinstance(x, str) else 'icon' in ' '.join(x).lower())) + if icon_link and icon_link.get('href'): + logo_url = icon_link['href'] + + # 3. 尝试 apple-touch-icon + if not logo_url: + apple_icon = soup.find('link', rel='apple-touch-icon') + if apple_icon and apple_icon.get('href'): + logo_url = apple_icon['href'] + + # 4. 默认使用 /favicon.ico + if not logo_url: + logo_url = '/favicon.ico' + + # 转换为绝对URL + if logo_url: + logo_url = urljoin(base_url, logo_url) + + return logo_url + + def download_logo(self, logo_url, save_dir='static/uploads'): + """ + 下载并保存Logo + + Args: + logo_url: Logo的URL + save_dir: 保存目录 + + Returns: + str: 保存后的相对路径,失败返回None + """ + if not logo_url: + return None + + try: + # 创建保存目录 + os.makedirs(save_dir, exist_ok=True) + + # 下载图片 + response = requests.get(logo_url, headers=self.headers, timeout=self.timeout) + response.raise_for_status() + + # 检查是否是图片 + content_type = response.headers.get('content-type', '') + if not content_type.startswith('image/'): + return None + + # 生成文件名 + parsed_url = urlparse(logo_url) + ext = os.path.splitext(parsed_url.path)[1] + if not ext or len(ext) > 5: + ext = '.png' # 默认扩展名 + + # 使用域名作为文件名 + domain = parsed_url.netloc.replace(':', '_').replace('.', '_') + filename = f"logo_{domain}{ext}" + filepath = os.path.join(save_dir, filename) + + # 保存图片 + with open(filepath, 'wb') as f: + f.write(response.content) + + # 返回相对路径(用于数据库存储) + return f'/{filepath.replace(os.sep, "/")}' + + except Exception as e: + print(f"下载Logo失败: {str(e)}") + return None