diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 4ae9836..679435b 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -16,7 +16,11 @@ "Bash(findstr:*)", "Bash(dir:*)", "Bash(git init:*)", - "Bash(git add:*)" + "Bash(git add:*)", + "Bash(git commit:*)", + "Bash(curl:*)", + "WebFetch(domain:zjpb.net)", + "Bash(del import_bookmarks.py test_bookmark_parse.py test_simple_parse.py result.txt)" ] } } diff --git a/.env.example b/.env.example index 439c0a5..deee59f 100644 --- a/.env.example +++ b/.env.example @@ -10,3 +10,7 @@ SECRET_KEY=your-secret-key-here # 运行环境 (development/production) FLASK_ENV=development + +# DeepSeek API配置 +DEEPSEEK_API_KEY=your_deepseek_api_key_here +DEEPSEEK_BASE_URL=https://api.deepseek.com diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..08ddb56 --- /dev/null +++ b/.mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "tdesign-mcp-server": { + "command": "npx", + "args": ["-y", "tdesign-mcp-server"] + } + } +} diff --git a/=1.0.0 b/=1.0.0 new file mode 100644 index 0000000..2f61c3c --- /dev/null +++ b/=1.0.0 @@ -0,0 +1,49 @@ +Defaulting to user installation because normal site-packages is not writeable +Collecting openai + Downloading openai-2.14.0-py3-none-any.whl.metadata (29 kB) +Collecting anyio<5,>=3.5.0 (from openai) + Downloading anyio-4.12.0-py3-none-any.whl.metadata (4.3 kB) +Collecting distro<2,>=1.7.0 (from openai) + Downloading distro-1.9.0-py3-none-any.whl.metadata (6.8 kB) +Collecting httpx<1,>=0.23.0 (from openai) + Downloading httpx-0.28.1-py3-none-any.whl.metadata (7.1 kB) +Collecting jiter<1,>=0.10.0 (from openai) + Downloading jiter-0.12.0-cp313-cp313-win_amd64.whl.metadata (5.3 kB) +Collecting pydantic<3,>=1.9.0 (from openai) + Downloading pydantic-2.12.5-py3-none-any.whl.metadata (90 kB) +Collecting sniffio (from openai) + Downloading sniffio-1.3.1-py3-none-any.whl.metadata (3.9 kB) +Collecting tqdm>4 (from openai) + Downloading tqdm-4.67.1-py3-none-any.whl.metadata (57 kB) +Requirement already satisfied: typing-extensions<5,>=4.11 in c:\users\linha\appdata\local\packages\pythonsoftwarefoundation.python.3.13_qbz5n2kfra8p0\localcache\local-packages\python313\site-packages (from openai) (4.15.0) +Requirement already satisfied: idna>=2.8 in c:\users\linha\appdata\local\packages\pythonsoftwarefoundation.python.3.13_qbz5n2kfra8p0\localcache\local-packages\python313\site-packages (from anyio<5,>=3.5.0->openai) (3.11) +Requirement already satisfied: certifi in c:\users\linha\appdata\local\packages\pythonsoftwarefoundation.python.3.13_qbz5n2kfra8p0\localcache\local-packages\python313\site-packages (from httpx<1,>=0.23.0->openai) (2025.11.12) +Collecting httpcore==1.* (from httpx<1,>=0.23.0->openai) + Downloading httpcore-1.0.9-py3-none-any.whl.metadata (21 kB) +Collecting h11>=0.16 (from httpcore==1.*->httpx<1,>=0.23.0->openai) + Downloading h11-0.16.0-py3-none-any.whl.metadata (8.3 kB) +Collecting annotated-types>=0.6.0 (from pydantic<3,>=1.9.0->openai) + Downloading annotated_types-0.7.0-py3-none-any.whl.metadata (15 kB) +Collecting pydantic-core==2.41.5 (from pydantic<3,>=1.9.0->openai) + Downloading pydantic_core-2.41.5-cp313-cp313-win_amd64.whl.metadata (7.4 kB) +Collecting typing-inspection>=0.4.2 (from pydantic<3,>=1.9.0->openai) + Downloading typing_inspection-0.4.2-py3-none-any.whl.metadata (2.6 kB) +Requirement already satisfied: colorama in c:\users\linha\appdata\local\packages\pythonsoftwarefoundation.python.3.13_qbz5n2kfra8p0\localcache\local-packages\python313\site-packages (from tqdm>4->openai) (0.4.6) +Downloading openai-2.14.0-py3-none-any.whl (1.1 MB) + ---------------------------------------- 1.1/1.1 MB 2.6 MB/s 0:00:00 +Downloading anyio-4.12.0-py3-none-any.whl (113 kB) +Downloading distro-1.9.0-py3-none-any.whl (20 kB) +Downloading httpx-0.28.1-py3-none-any.whl (73 kB) +Downloading httpcore-1.0.9-py3-none-any.whl (78 kB) +Downloading jiter-0.12.0-cp313-cp313-win_amd64.whl (204 kB) +Downloading pydantic-2.12.5-py3-none-any.whl (463 kB) +Downloading pydantic_core-2.41.5-cp313-cp313-win_amd64.whl (2.0 MB) + ---------------------------------------- 2.0/2.0 MB 6.3 MB/s 0:00:00 +Downloading annotated_types-0.7.0-py3-none-any.whl (13 kB) +Downloading h11-0.16.0-py3-none-any.whl (37 kB) +Downloading tqdm-4.67.1-py3-none-any.whl (78 kB) +Downloading typing_inspection-0.4.2-py3-none-any.whl (14 kB) +Downloading sniffio-1.3.1-py3-none-any.whl (10 kB) +Installing collected packages: typing-inspection, tqdm, sniffio, pydantic-core, jiter, h11, distro, anyio, annotated-types, pydantic, httpcore, httpx, openai + +Successfully installed annotated-types-0.7.0 anyio-4.12.0 distro-1.9.0 h11-0.16.0 httpcore-1.0.9 httpx-0.28.1 jiter-0.12.0 openai-2.14.0 pydantic-2.12.5 pydantic-core-2.41.5 sniffio-1.3.1 tqdm-4.67.1 typing-inspection-0.4.2 diff --git a/app.py b/app.py index 24a943f..14288f1 100644 --- a/app.py +++ b/app.py @@ -1,12 +1,13 @@ 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 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 +from models import db, Site, Tag, Admin as AdminModel, News from utils.website_fetcher import WebsiteFetcher +from utils.tag_generator import TagGenerator def create_app(config_name='default'): """应用工厂函数""" @@ -35,37 +36,77 @@ def create_app(config_name='default'): # 获取所有启用的标签 tags = Tag.query.order_by(Tag.sort_order.desc(), Tag.id).all() - # 获取筛选的标签 + # 获取筛选参数 tag_slug = request.args.get('tag') + search_query = request.args.get('q', '').strip() + page = request.args.get('page', 1, type=int) + per_page = 100 # 每页显示100个站点 + selected_tag = None + # 构建基础查询 + query = Site.query.filter_by(is_active=True) + + # 标签筛选 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() + query = query.filter(Site.tags.contains(selected_tag)) else: sites = [] - else: - # 获取所有启用的网站 - sites = Site.query.filter_by(is_active=True).order_by( - Site.sort_order.desc(), Site.id.desc() - ).all() + pagination = None + return render_template('index_new.html', sites=sites, tags=tags, + selected_tag=selected_tag, search_query=search_query, + pagination=pagination) - return render_template('index.html', sites=sites, tags=tags, selected_tag=selected_tag) + # 搜索功能 + if search_query: + # 使用OR条件搜索:网站名称、URL、描述 + search_pattern = f'%{search_query}%' + query = query.filter( + db.or_( + Site.name.like(search_pattern), + Site.url.like(search_pattern), + Site.description.like(search_pattern), + Site.short_desc.like(search_pattern) + ) + ) - @app.route('/site/') - def site_detail(slug): + # 排序并分页 + query = query.order_by(Site.sort_order.desc(), Site.id.desc()) + pagination = query.paginate(page=page, per_page=per_page, error_out=False) + sites = pagination.items + + return render_template('index_new.html', sites=sites, tags=tags, + selected_tag=selected_tag, search_query=search_query, + pagination=pagination) + + @app.route('/site/') + def site_detail(code): """网站详情页""" - site = Site.query.filter_by(slug=slug, is_active=True).first_or_404() + site = Site.query.filter_by(code=code, is_active=True).first_or_404() # 增加浏览次数 site.view_count += 1 db.session.commit() - return render_template('detail.html', site=site) + # 获取该网站的相关新闻(最多显示5条) + news_list = News.query.filter_by( + site_id=site.id, + is_active=True + ).order_by(News.published_at.desc()).limit(5).all() + + # 获取同类工具推荐(通过标签匹配,最多显示4个) + recommended_sites = [] + if site.tags: + # 获取有相同标签的其他网站 + recommended_sites = Site.query.filter( + Site.id != site.id, + Site.is_active == True, + Site.tags.any(Tag.id.in_([tag.id for tag in site.tags])) + ).order_by(Site.view_count.desc()).limit(4).all() + + return render_template('detail_new.html', site=site, news_list=news_list, recommended_sites=recommended_sites) # ========== 后台登录路由 ========== @app.route('/admin/login', methods=['GET', 'POST']) @@ -144,9 +185,289 @@ def create_app(config_name='default'): 'message': f'抓取失败: {str(e)}' }), 500 + @app.route('/api/generate-tags', methods=['POST']) + @login_required + def generate_tags(): + """使用DeepSeek自动生成标签""" + try: + data = request.get_json() + name = data.get('name', '').strip() + description = data.get('description', '').strip() + + if not name or not description: + return jsonify({ + 'success': False, + 'message': '请提供网站名称和描述' + }), 400 + + # 获取现有标签作为参考 + existing_tags = [tag.name for tag in Tag.query.all()] + + # 生成标签 + generator = TagGenerator() + suggested_tags = generator.generate_tags(name, description, existing_tags) + + if not suggested_tags: + return jsonify({ + 'success': False, + 'message': 'DeepSeek标签生成失败,请检查API配置' + }), 500 + + return jsonify({ + 'success': True, + 'tags': suggested_tags + }) + + 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('/admin/batch-import', methods=['GET', 'POST']) + @login_required + def batch_import(): + """批量导入网站""" + from utils.bookmark_parser import BookmarkParser + from utils.website_fetcher import WebsiteFetcher + + results = None + + if request.method == 'POST': + import_type = request.form.get('import_type') + auto_activate = request.form.get('auto_activate') == 'on' + + parser = BookmarkParser() + fetcher = WebsiteFetcher(timeout=15) + + urls_to_import = [] + + try: + # 解析输入 + if import_type == 'url_list': + url_list_text = request.form.get('url_list', '') + urls_to_import = parser.parse_url_list(url_list_text) + + elif import_type == 'bookmark_file': + bookmark_file = request.files.get('bookmark_file') + if not bookmark_file: + flash('请选择书签文件', 'error') + return render_template('admin/batch_import.html') + + html_content = bookmark_file.read().decode('utf-8', errors='ignore') + all_bookmarks = parser.parse_html_file(html_content) + + # 筛选文件夹(如果指定) + folder_filter = request.form.get('folder_filter', '').strip() + if folder_filter: + urls_to_import = [ + b for b in all_bookmarks + if folder_filter.lower() in b.get('folder', '').lower() + ] + else: + urls_to_import = all_bookmarks + + # 批量导入 + success_list = [] + failed_list = [] + + for idx, item in enumerate(urls_to_import, 1): + url = item['url'] + name = item.get('name', '') + + # 为每个URL创建独立的事务 + try: + # 1. 检查URL是否已存在 + try: + existing = Site.query.filter_by(url=url).first() + if existing: + failed_list.append({ + 'url': url, + 'name': name or existing.name, + 'error': f'该URL已存在(网站名称:{existing.name})' + }) + continue + except Exception as e: + failed_list.append({ + 'url': url, + 'name': name, + 'error': f'检查URL时出错: {str(e)}' + }) + continue + + # 2. 抓取网站信息(带超时和错误处理) + info = None + try: + info = fetcher.fetch_website_info(url) + except Exception as e: + print(f"抓取 {url} 失败: {str(e)}") + # 抓取失败不是致命错误,继续尝试使用书签名称 + + # 3. 处理网站信息 + if not info or not info.get('name'): + # 如果有书签名称,使用书签名称 + if name: + info = { + 'name': name, + 'description': '', + 'logo_url': '' + } + else: + # 尝试从URL提取域名作为名称 + from urllib.parse import urlparse + try: + parsed = urlparse(url) + domain = parsed.netloc or parsed.path + if domain: + info = { + 'name': domain, + 'description': '', + 'logo_url': '' + } + else: + failed_list.append({ + 'url': url, + 'name': name, + 'error': '无法获取网站信息且没有备用名称' + }) + continue + except Exception: + failed_list.append({ + 'url': url, + 'name': name, + 'error': '无法获取网站信息且URL解析失败' + }) + continue + + # 4. 下载Logo(失败不影响导入) + logo_path = None + if info.get('logo_url'): + try: + logo_path = fetcher.download_logo(info['logo_url']) + except Exception as e: + print(f"下载Logo失败 ({url}): {str(e)}") + # Logo下载失败不影响网站导入 + + # 5. 生成code和slug + try: + import random + from pypinyin import lazy_pinyin + import re + + # 生成唯一的code + site_code = None + max_attempts = 10 + for _ in range(max_attempts): + code = str(random.randint(10000000, 99999999)) + if not Site.query.filter_by(code=code).first(): + site_code = code + break + + if not site_code: + # 如果10次都失败,使用时间戳 + import time + site_code = str(int(time.time() * 1000))[-8:] + + # 生成slug + site_name = info.get('name', name or 'Unknown')[:100] + slug = ''.join(lazy_pinyin(site_name)) + slug = slug.lower() + slug = re.sub(r'[^\w\s-]', '', slug) + slug = re.sub(r'[-\s]+', '-', slug).strip('-') + if not slug: + slug = f"site-{site_code}" + + # 确保slug唯一 + base_slug = slug[:50] # 限制长度 + counter = 1 + final_slug = slug + while Site.query.filter_by(slug=final_slug).first(): + final_slug = f"{base_slug}-{counter}" + counter += 1 + if counter > 100: # 防止无限循环 + final_slug = f"{base_slug}-{site_code}" + break + + # 6. 创建网站记录(带code和slug) + site = Site( + code=site_code, + slug=final_slug, + name=site_name, + url=url[:500], # 限制URL长度 + logo=logo_path or info.get('logo_url', '')[:500] if info.get('logo_url') else '', + short_desc=info.get('description', '')[:200] if info.get('description') else '', + description=info.get('description', '')[:2000] if info.get('description') else '', + is_active=auto_activate + ) + + # 添加到数据库并提交 + db.session.add(site) + db.session.commit() + + success_list.append({ + 'name': site.name, + 'url': site.url + }) + + print(f"成功导入 [{idx}/{len(urls_to_import)}]: {site.name}") + + except Exception as e: + db.session.rollback() + failed_list.append({ + 'url': url, + 'name': name or info.get('name', 'Unknown'), + 'error': f'数据库保存失败: {str(e)}' + }) + continue + + except Exception as e: + # 捕获所有未预期的错误 + db.session.rollback() + failed_list.append({ + 'url': url, + 'name': name, + 'error': f'未知错误: {str(e)}' + }) + print(f"导入 {url} 时发生未知错误: {str(e)}") + continue + + results = { + 'total_count': len(urls_to_import), + 'success_count': len(success_list), + 'failed_count': len(failed_list), + 'success_list': success_list, + 'failed_list': failed_list + } + + if success_list: + flash(f'成功导入 {len(success_list)} 个网站!', 'success') + + except Exception as e: + flash(f'导入失败: {str(e)}', 'error') + + return render_template('admin/batch_import.html', results=results) + # ========== Flask-Admin 配置 ========== class SecureModelView(ModelView): """需要登录的模型视图""" + # 中文化配置 + can_set_page_size = True + page_size = 20 + + # 自定义文本 + list_template = 'admin/model/list.html' + create_template = 'admin/model/create.html' + edit_template = 'admin/model/edit.html' + + # 覆盖英文文本 + named_filter_urls = True + def is_accessible(self): return current_user.is_authenticated @@ -161,17 +482,44 @@ def create_app(config_name='default'): def inaccessible_callback(self, name, **kwargs): return redirect(url_for('admin_login')) + @expose('/') + def index(self): + """控制台首页,显示统计信息""" + # 统计数据 + stats = { + 'sites_count': Site.query.filter_by(is_active=True).count(), + 'tags_count': Tag.query.count(), + 'news_count': News.query.filter_by(is_active=True).count(), + 'total_views': db.session.query(db.func.sum(Site.view_count)).scalar() or 0 + } + + # 最近添加的工具(最多5个) + recent_sites = Site.query.order_by(Site.created_at.desc()).limit(5).all() + + return self.render('admin/index.html', stats=stats, recent_sites=recent_sites) + # 网站管理视图 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'] + # 启用编辑和删除 + can_edit = True + can_delete = True + can_create = True + can_view_details = False # 禁用查看详情,点击名称即可查看 + + # 显示操作列 + column_display_actions = True + action_disallowed_list = [] + + column_list = ['id', 'code', 'name', 'url', 'slug', 'is_active', 'view_count', 'created_at'] + column_searchable_list = ['code', 'name', 'url', 'description'] column_filters = ['is_active', 'tags'] column_labels = { 'id': 'ID', + 'code': '网站编码', 'name': '网站名称', 'url': 'URL', 'slug': 'URL别名', @@ -188,8 +536,76 @@ def create_app(config_name='default'): } form_columns = ['name', 'url', 'slug', 'logo', 'short_desc', 'description', 'features', 'tags', 'is_active', 'sort_order'] + # 自定义编辑/删除文字 + column_extra_row_actions = None + + def on_model_change(self, form, model, is_created): + """保存前自动生成code和slug(如果为空)""" + import re + import random + from pypinyin import lazy_pinyin + + # 使用no_autoflush防止在查询时触发提前flush + with db.session.no_autoflush: + # 如果code为空,自动生成唯一的8位数字编码 + if not model.code or model.code.strip() == '': + max_attempts = 10 + for attempt in range(max_attempts): + # 生成10000000-99999999之间的随机数 + code = str(random.randint(10000000, 99999999)) + # 检查是否已存在 + existing = Site.query.filter(Site.code == code).first() + if not existing or existing.id == model.id: + model.code = code + break + + # 如果10次都失败,使用时间戳 + if not model.code: + import time + model.code = str(int(time.time() * 1000))[-8:] + + # 如果slug为空,从name自动生成 + if not model.slug or model.slug.strip() == '': + # 将中文转换为拼音 + slug = ''.join(lazy_pinyin(model.name)) + # 转换为小写,移除特殊字符 + slug = slug.lower() + slug = re.sub(r'[^\w\s-]', '', slug) + slug = re.sub(r'[-\s]+', '-', slug).strip('-') + + # 如果转换后为空,使用code + if not slug: + slug = f"site-{model.code}" + + # 确保slug唯一(限制长度和重试次数) + base_slug = slug[:50] + counter = 1 + final_slug = slug + max_slug_attempts = 100 + + while counter < max_slug_attempts: + existing = Site.query.filter(Site.slug == final_slug).first() + if not existing or existing.id == model.id: + break + final_slug = f"{base_slug}-{counter}" + counter += 1 + + # 如果100次都失败,使用code确保唯一 + if counter >= max_slug_attempts: + final_slug = f"{base_slug}-{model.code}" + + model.slug = final_slug + # 标签管理视图 class TagAdmin(SecureModelView): + can_edit = True + can_delete = True + can_create = True + can_view_details = False + + # 显示操作列 + column_display_actions = True + column_list = ['id', 'name', 'slug', 'description', 'sort_order'] column_searchable_list = ['name', 'description'] column_labels = { @@ -205,6 +621,14 @@ def create_app(config_name='default'): # 管理员视图 class AdminAdmin(SecureModelView): + can_edit = True + can_delete = True + can_create = True + can_view_details = False + + # 显示操作列 + column_display_actions = True + column_list = ['id', 'username', 'email', 'is_active', 'last_login', 'created_at'] column_searchable_list = ['username', 'email'] column_filters = ['is_active'] @@ -223,17 +647,55 @@ def create_app(config_name='default'): if is_created: model.set_password('admin123') # 默认密码 + # 新闻管理视图 + class NewsAdmin(SecureModelView): + can_edit = True + can_delete = True + can_create = True + can_view_details = False + + # 显示操作列 + column_display_actions = True + + column_list = ['id', 'site', 'title', 'news_type', 'published_at', 'is_active'] + column_searchable_list = ['title', 'content'] + column_filters = ['site', 'news_type', 'is_active', 'published_at'] + column_labels = { + 'id': 'ID', + 'site': '关联网站', + 'title': '新闻标题', + 'content': '新闻内容', + 'news_type': '新闻类型', + 'url': '新闻链接', + 'published_at': '发布时间', + 'is_active': '是否启用', + 'created_at': '创建时间', + 'updated_at': '更新时间' + } + form_columns = ['site', 'title', 'content', 'news_type', 'url', 'published_at', 'is_active'] + + # 可选的新闻类型 + form_choices = { + 'news_type': [ + ('Product Update', 'Product Update'), + ('Industry News', 'Industry News'), + ('Company News', 'Company News'), + ('Other', 'Other') + ] + } + # 初始化 Flask-Admin admin = Admin( app, - name='ZJPB 焦提示词 - 后台管理', + name='ZJPB 焦提示词', template_mode='bootstrap4', - index_view=SecureAdminIndexView(), - base_template='admin/custom_base.html' + index_view=SecureAdminIndexView(name='控制台', url='/admin'), + base_template='admin/master.html' ) 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(AdminAdmin(AdminModel, db.session, name='管理员', endpoint='admin_users')) return app diff --git a/config.py b/config.py index 0e60817..a604c05 100644 --- a/config.py +++ b/config.py @@ -20,6 +20,20 @@ class Config: SQLALCHEMY_TRACK_MODIFICATIONS = False SQLALCHEMY_ECHO = False + # 数据库连接池配置 + SQLALCHEMY_ENGINE_OPTIONS = { + 'pool_size': 10, # 连接池大小 + 'pool_recycle': 3600, # 连接回收时间(秒) + 'pool_pre_ping': True, # 每次取连接前先ping,确保连接有效 + 'pool_timeout': 30, # 连接池超时时间 + 'max_overflow': 20, # 超过pool_size后最多创建的连接数 + 'connect_args': { + 'connect_timeout': 10, # 连接超时(秒) + 'read_timeout': 30, # 读取超时(秒) + 'write_timeout': 30, # 写入超时(秒) + } + } + # 分页配置 SITES_PER_PAGE = 20 diff --git a/migrations/add_code_field.py b/migrations/add_code_field.py new file mode 100644 index 0000000..fc7abe8 --- /dev/null +++ b/migrations/add_code_field.py @@ -0,0 +1,62 @@ +"""添加code字段的迁移脚本""" +import sys +import os +import random + +# 添加项目根目录到系统路径 +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from app import create_app +from models import db, Site +from sqlalchemy import text + +# 创建应用上下文 +app = create_app() + +def generate_unique_code(): + """生成唯一的8位数字编码""" + while True: + # 生成10000000-99999999之间的随机数 + code = str(random.randint(10000000, 99999999)) + # 检查是否已存在 + if not Site.query.filter_by(code=code).first(): + return code + +with app.app_context(): + try: + print("Step 1: Adding code column to sites table...") + + # 添加code列(先允许为空) + with db.engine.connect() as connection: + connection.execute(text('ALTER TABLE sites ADD COLUMN code VARCHAR(8) NULL COMMENT "8位数字编码"')) + connection.commit() + + print(" - Column added successfully!") + + print("\nStep 2: Generating codes for existing sites...") + + # 为现有网站生成code + sites = Site.query.all() + for site in sites: + site.code = generate_unique_code() + print(f" - Site #{site.id} '{site.name}': code = {site.code}") + + db.session.commit() + print(f" - Generated codes for {len(sites)} sites!") + + print("\nStep 3: Making code column NOT NULL and UNIQUE...") + + # 现在修改列为NOT NULL和UNIQUE + with db.engine.connect() as connection: + connection.execute(text('ALTER TABLE sites MODIFY COLUMN code VARCHAR(8) NOT NULL')) + connection.execute(text('ALTER TABLE sites ADD UNIQUE INDEX idx_site_code (code)')) + connection.commit() + + print(" - Code column constraints added!") + print("\n✓ Migration completed successfully!") + + except Exception as e: + print(f"\n✗ Migration failed: {str(e)}") + import traceback + traceback.print_exc() + db.session.rollback() diff --git a/migrations/add_news_table.py b/migrations/add_news_table.py new file mode 100644 index 0000000..b4369d6 --- /dev/null +++ b/migrations/add_news_table.py @@ -0,0 +1,17 @@ +"""添加news表的迁移脚本""" +import sys +import os + +# 添加项目根目录到系统路径 +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from app import create_app +from models import db, News + +# 创建应用上下文 +app = create_app() + +with app.app_context(): + # 创建news表 + db.create_all() + print("✓ News表创建成功!") diff --git a/migrations/fix_empty_slugs.py b/migrations/fix_empty_slugs.py new file mode 100644 index 0000000..193263b --- /dev/null +++ b/migrations/fix_empty_slugs.py @@ -0,0 +1,73 @@ +"""修复空slug的迁移脚本""" +import sys +import os +import re + +# 添加项目根目录到系统路径 +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from app import create_app +from models import db, Site +from datetime import datetime +from pypinyin import lazy_pinyin + +# 创建应用上下文 +app = create_app() + +def generate_slug(name, site_id=None): + """从名称生成slug""" + # 将中文转换为拼音 + slug = ''.join(lazy_pinyin(name)) + # 转换为小写,移除特殊字符 + slug = slug.lower() + slug = re.sub(r'[^\w\s-]', '', slug) + slug = re.sub(r'[-\s]+', '-', slug).strip('-') + + # 如果转换后为空,使用ID或时间戳 + if not slug: + if site_id: + slug = f"site-{site_id}" + else: + slug = f"site-{datetime.now().strftime('%Y%m%d%H%M%S')}" + + # 确保slug唯一 + base_slug = slug + counter = 1 + while Site.query.filter(Site.slug == slug, Site.id != site_id).first(): + slug = f"{base_slug}-{counter}" + counter += 1 + + return slug + +with app.app_context(): + try: + # 查找所有slug为空或包含乱码的网站 + all_sites = Site.query.all() + sites_to_fix = [] + + for site in all_sites: + # 检查slug是否为空、None、或包含乱码 + if (not site.slug or + site.slug == 'None' or + site.slug.strip() == '' or + any(ord(char) > 127 and not char.isalpha() for char in site.slug)): + sites_to_fix.append(site) + + if not sites_to_fix: + print("No sites to fix. All good!") + else: + print(f"Found {len(sites_to_fix)} sites to fix...") + + for site in sites_to_fix: + old_slug = site.slug + site.slug = generate_slug(site.name, site.id) + print(f" - Site #{site.id} '{site.name}': '{old_slug}' -> '{site.slug}'") + + db.session.commit() + print(f"Successfully fixed {len(sites_to_fix)} sites!") + + except Exception as e: + print(f"Migration failed: {str(e)}") + import traceback + traceback.print_exc() + db.session.rollback() diff --git a/migrations/make_slug_nullable.py b/migrations/make_slug_nullable.py new file mode 100644 index 0000000..26a597a --- /dev/null +++ b/migrations/make_slug_nullable.py @@ -0,0 +1,25 @@ +"""修改slug字段为可空的迁移脚本""" +import sys +import os + +# 添加项目根目录到系统路径 +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from app import create_app +from models import db +from sqlalchemy import text + +# 创建应用上下文 +app = create_app() + +with app.app_context(): + # 使用原生SQL修改列属性 + try: + # MySQL语法:修改slug列为可空 + with db.engine.connect() as connection: + connection.execute(text('ALTER TABLE sites MODIFY COLUMN slug VARCHAR(100) NULL')) + connection.commit() + print("slug field successfully updated to nullable!") + except Exception as e: + print(f"Migration failed: {str(e)}") + print("Note: If the field is already nullable, you can ignore this error.") diff --git a/models.py b/models.py index 1c3ccfd..57e988a 100644 --- a/models.py +++ b/models.py @@ -16,9 +16,10 @@ class Site(db.Model): __tablename__ = 'sites' id = db.Column(db.Integer, primary_key=True) + code = db.Column(db.String(8), unique=True, nullable=False, comment='8位数字编码') 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别名') + slug = db.Column(db.String(100), unique=True, nullable=True, comment='URL别名(SEO用)') logo = db.Column(db.String(500), comment='Logo图片路径') short_desc = db.Column(db.String(200), comment='简短描述') description = db.Column(db.Text, comment='详细介绍') @@ -40,6 +41,7 @@ class Site(db.Model): """转换为字典""" return { 'id': self.id, + 'code': self.code, 'name': self.name, 'url': self.url, 'slug': self.slug, @@ -78,6 +80,40 @@ class Tag(db.Model): 'icon': self.icon } +class News(db.Model): + """新闻模型""" + __tablename__ = 'news' + + id = db.Column(db.Integer, primary_key=True) + site_id = db.Column(db.Integer, db.ForeignKey('sites.id'), nullable=False, comment='关联网站ID') + title = db.Column(db.String(200), nullable=False, comment='新闻标题') + content = db.Column(db.Text, comment='新闻内容') + news_type = db.Column(db.String(50), default='Industry News', comment='新闻类型') + url = db.Column(db.String(500), comment='新闻链接') + published_at = db.Column(db.DateTime, default=datetime.now, 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='更新时间') + + # 关联网站 + site = db.relationship('Site', backref=db.backref('news', lazy='dynamic', order_by='News.published_at.desc()')) + + def __repr__(self): + return f'' + + def to_dict(self): + """转换为字典""" + return { + 'id': self.id, + 'site_id': self.site_id, + 'title': self.title, + 'content': self.content, + 'news_type': self.news_type, + 'url': self.url, + 'published_at': self.published_at.strftime('%Y-%m-%d') if self.published_at else None, + 'created_at': self.created_at.strftime('%Y-%m-%d %H:%M:%S') if self.created_at else None + } + class Admin(UserMixin, db.Model): """管理员模型""" __tablename__ = 'admins' diff --git a/requirements.txt b/requirements.txt index c46b86a..844b4ab 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,3 +10,4 @@ WTForms==2.3.3 requests==2.31.0 beautifulsoup4==4.12.2 Pillow>=10.2.0 +openai>=1.0.0 diff --git a/static/css/admin-actions.css b/static/css/admin-actions.css new file mode 100644 index 0000000..6f7e50d --- /dev/null +++ b/static/css/admin-actions.css @@ -0,0 +1,97 @@ +/* 管理后台操作按钮样式 - 添加中文文字 */ + +/* 表格操作列 */ +td.list-buttons-column, +th.list-buttons-column { + white-space: nowrap; + min-width: 140px; + text-align: center; +} + +/* 隐藏所有图标 */ +td.list-buttons-column a.icon .glyphicon, +td.list-buttons-column a.icon .fa, +td.list-buttons-column a.icon span.glyphicon, +td.list-buttons-column a.icon i, +td.list-buttons-column form .glyphicon, +td.list-buttons-column form .fa, +td.list-buttons-column form span.glyphicon, +td.list-buttons-column form i { + display: none !important; +} + +/* 通用操作按钮样式(链接) */ +td.list-buttons-column a.icon { + display: inline-flex !important; + align-items: center; + justify-content: center; + min-width: 60px; + height: 32px; + padding: 0 12px; + margin: 0 4px; + border-radius: 4px; + font-size: 13px; + font-weight: 500; + text-decoration: none; + transition: all 0.2s; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; +} + +/* 删除表单样式 */ +td.list-buttons-column form { + display: inline-block; + margin: 0; + padding: 0; +} + +/* 删除表单的提交按钮 */ +td.list-buttons-column form button, +td.list-buttons-column form input[type="submit"] { + display: inline-flex !important; + align-items: center; + justify-content: center; + min-width: 60px; + height: 32px; + padding: 0 12px; + margin: 0 4px; + border-radius: 4px; + font-size: 13px; + font-weight: 500; + text-decoration: none; + transition: all 0.2s; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + background: #E34D59 !important; + color: white !important; + border: none; + cursor: pointer; +} + +td.list-buttons-column form button:hover, +td.list-buttons-column form input[type="submit"]:hover { + background: #C9353F !important; +} + +/* 使用::before添加删除文字 */ +td.list-buttons-column form button::before, +td.list-buttons-column form input[type="submit"]::before { + content: "删除"; +} + +/* 编辑按钮 - 通过href匹配 */ +td.list-buttons-column a[href*="/edit/"] { + background: #0052D9 !important; + color: white !important; +} + +td.list-buttons-column a[href*="/edit/"]:hover { + background: #0041A8 !important; +} + +td.list-buttons-column a[href*="/edit/"]::after { + content: "编辑"; +} + +/* 查看按钮 - 完全隐藏 */ +td.list-buttons-column a[href*="/details/"] { + display: none !important; +} diff --git a/static/css/admin-i18n.css b/static/css/admin-i18n.css new file mode 100644 index 0000000..a8cb64e --- /dev/null +++ b/static/css/admin-i18n.css @@ -0,0 +1,83 @@ +/* Flask-Admin 界面中文化样式 */ + +/* ========== 顶部导航栏中文化 ========== */ + +/* List (4) 标签页 - 只针对第一个li */ +.nav-tabs li:first-child a { + font-size: 0 !important; +} + +.nav-tabs li:first-child a::after { + font-size: 14px; + content: "列表"; +} + +/* 保留数字显示 */ +.nav-tabs li a .badge { + font-size: 12px !important; +} + +/* Create 按钮 - 通过href精确匹配 */ +.nav.nav-tabs ~ .btn-group a.btn-primary, +a.btn.btn-primary[href$="/new/"] { + font-size: 0 !important; +} + +.nav.nav-tabs ~ .btn-group a.btn-primary::before, +a.btn.btn-primary[href$="/new/"]::before { + font-size: 14px; + content: "创建"; + font-family: inherit; +} + +.nav.nav-tabs ~ .btn-group a.btn-primary .fa, +a.btn.btn-primary[href$="/new/"] .fa { + font-size: 14px !important; + margin-right: 4px; +} + +/* Add Filter 下拉按钮 */ +.btn-group .dropdown-toggle { + font-size: 0 !important; +} + +.btn-group .dropdown-toggle::before { + font-size: 14px; +} + +/* 根据位置区分不同的dropdown */ +/* Add Filter - 第一个dropdown */ +.btn-group:nth-of-type(1) .dropdown-toggle::before { + content: "添加筛选"; +} + +/* With selected - 第二个dropdown (通常在表格上方) */ +form[id^="action_confirmation"] ~ .btn-group .dropdown-toggle::before, +.actions-nav .dropdown-toggle::before { + content: "批量操作"; +} + +/* 保留下拉箭头 */ +.btn-group .dropdown-toggle .caret { + font-size: 0; + margin-left: 4px; + display: inline-block; + width: 0; + height: 0; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-top: 4px solid currentColor; +} + +/* Delete 批量删除选项 */ +.dropdown-menu li a { + font-size: 14px !important; +} + +/* ========== 修正:避免影响其他链接 ========== */ +/* 重置可能被误伤的元素 */ +.admin-sidebar a, +.nav-item a, +table a { + font-size: 14px !important; +} diff --git a/static/css/admin-sidebar.css b/static/css/admin-sidebar.css new file mode 100644 index 0000000..ad2e419 --- /dev/null +++ b/static/css/admin-sidebar.css @@ -0,0 +1,636 @@ +/* ========== Flask-Admin 左侧菜单布局 - ZJPB焦提示词 ========== */ + +/* TDesign 色彩系统 */ +:root { + --td-brand-color: #0052D9; + --td-brand-color-hover: #266FE8; + --td-brand-color-active: #0034B5; + --td-brand-color-light: #ECF2FE; + --td-success-color: #00A870; + --td-warning-color: #E37318; + --td-error-color: #D54941; + --td-bg-color-page: #F5F7FA; + --td-bg-color-container: #FFFFFF; + --td-bg-color-container-hover: #F5F5F5; + --td-text-color-primary: #000000; + --td-text-color-secondary: #606266; + --td-text-color-placeholder: #C0C4CC; + --td-border-color: #DCDFE6; + --td-border-color-light: #E4E7ED; + --td-border-radius: 3px; + --td-border-radius-medium: 6px; + --td-shadow-1: 0 1px 4px rgba(0, 0, 0, .05); + --td-shadow-2: 0 2px 12px rgba(0, 0, 0, .08); + --sidebar-width: 240px; +} + +/* 全局样式重置 */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body.admin-sidebar-layout { + background: var(--td-bg-color-page); + color: var(--td-text-color-primary); + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif; + font-size: 14px; + line-height: 1.5; + overflow-x: hidden; +} + +/* ========== 左侧边栏 ========== */ +.admin-sidebar { + position: fixed; + left: 0; + top: 0; + bottom: 0; + width: var(--sidebar-width); + background: var(--td-bg-color-container); + border-right: 1px solid var(--td-border-color); + display: flex; + flex-direction: column; + z-index: 1000; +} + +/* Logo */ +.sidebar-logo { + display: flex; + align-items: center; + gap: 12px; + padding: 20px 24px; + border-bottom: 1px solid var(--td-border-color); +} + +.sidebar-logo .logo-icon { + font-size: 32px; + color: var(--td-brand-color); +} + +.sidebar-logo .logo-text { + font-size: 18px; + font-weight: 600; + color: var(--td-text-color-primary); + font-family: 'Space Grotesk', sans-serif; +} + +/* 导航区域 */ +.sidebar-nav { + flex: 1; + overflow-y: auto; + padding: 16px 0; +} + +.nav-section { + margin-bottom: 24px; +} + +.nav-section-title { + font-size: 12px; + font-weight: 600; + color: var(--td-text-color-secondary); + text-transform: uppercase; + letter-spacing: 0.5px; + padding: 8px 24px; + margin-bottom: 4px; +} + +.nav-menu { + list-style: none; + padding: 0; + margin: 0; +} + +.nav-item { + margin: 2px 12px; +} + +.nav-link { + display: flex; + align-items: center; + gap: 12px; + padding: 10px 12px; + color: var(--td-text-color-secondary); + text-decoration: none; + border-radius: var(--td-border-radius-medium); + transition: all 0.2s cubic-bezier(0.38, 0, 0.24, 1); +} + +.nav-link:hover { + background: var(--td-bg-color-container-hover); + color: var(--td-text-color-primary); + text-decoration: none; +} + +.nav-item.active .nav-link { + background: var(--td-brand-color-light); + color: var(--td-brand-color); + font-weight: 500; +} + +.nav-icon { + font-size: 20px; + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; +} + +.nav-text { + flex: 1; + font-size: 14px; +} + +/* 用户信息 */ +.sidebar-user { + display: flex; + align-items: center; + gap: 12px; + padding: 16px 24px; + border-top: 1px solid var(--td-border-color); + background: var(--td-bg-color-page); +} + +.user-avatar { + width: 40px; + height: 40px; + border-radius: 50%; + background: var(--td-brand-color-light); + display: flex; + align-items: center; + justify-content: center; + color: var(--td-brand-color); +} + +.user-avatar .material-symbols-outlined { + font-size: 24px; +} + +.user-info { + flex: 1; + min-width: 0; +} + +.user-name { + font-size: 14px; + font-weight: 500; + color: var(--td-text-color-primary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.user-email { + font-size: 12px; + color: var(--td-text-color-secondary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* ========== 主内容区域 ========== */ +.admin-main { + margin-left: var(--sidebar-width); + min-height: 100vh; + display: flex; + flex-direction: column; +} + +/* 顶部导航 */ +.admin-header { + position: sticky; + top: 0; + z-index: 100; + background: var(--td-bg-color-container); + border-bottom: 1px solid var(--td-border-color); + padding: 16px 32px; + display: flex; + align-items: center; + justify-content: space-between; + box-shadow: var(--td-shadow-1); +} + +.header-breadcrumb { + display: flex; + align-items: center; + gap: 8px; + font-size: 14px; +} + +.breadcrumb-link { + color: var(--td-text-color-secondary); + text-decoration: none; + transition: color 0.2s; +} + +.breadcrumb-link:hover { + color: var(--td-brand-color); + text-decoration: none; +} + +.breadcrumb-separator { + color: var(--td-text-color-placeholder); +} + +.breadcrumb-current { + color: var(--td-text-color-primary); + font-weight: 500; +} + +.header-actions { + display: flex; + align-items: center; + gap: 12px; +} + +.search-box { + position: relative; + width: 300px; +} + +.search-icon { + position: absolute; + left: 12px; + top: 50%; + transform: translateY(-50%); + color: var(--td-text-color-placeholder); + font-size: 20px; +} + +.search-input { + width: 100%; + padding: 8px 12px 8px 40px; + border: 1px solid var(--td-border-color); + border-radius: var(--td-border-radius); + font-size: 14px; + background: var(--td-bg-color-page); + transition: all 0.2s; +} + +.search-input:focus { + outline: none; + border-color: var(--td-brand-color); + box-shadow: 0 0 0 2px rgba(0, 82, 217, 0.1); +} + +.header-btn { + width: 36px; + height: 36px; + display: flex; + align-items: center; + justify-content: center; + background: transparent; + border: none; + border-radius: var(--td-border-radius); + color: var(--td-text-color-secondary); + cursor: pointer; + transition: all 0.2s; +} + +.header-btn:hover { + background: var(--td-bg-color-container-hover); + color: var(--td-text-color-primary); +} + +.header-btn .material-symbols-outlined { + font-size: 20px; +} + +/* 页面内容 */ +.admin-content { + flex: 1; + padding: 32px; +} + +.page-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + margin-bottom: 24px; +} + +.page-title { + font-size: 28px; + font-weight: 600; + color: var(--td-text-color-primary); + margin: 0 0 8px 0; + font-family: 'Space Grotesk', sans-serif; +} + +.page-description { + font-size: 14px; + color: var(--td-text-color-secondary); + margin: 0; +} + +/* ========== 表格样式 ========== */ +.table { + background: var(--td-bg-color-container); + border-radius: var(--td-border-radius-medium); + overflow: hidden; + box-shadow: var(--td-shadow-1); + border: 1px solid var(--td-border-color); +} + +.table thead th { + background: var(--td-bg-color-page); + border: none; + color: var(--td-text-color-secondary); + font-size: 12px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + padding: 16px; + border-bottom: 1px solid var(--td-border-color); +} + +.table tbody tr { + border-bottom: 1px solid var(--td-border-color-light); + transition: background-color 0.2s; +} + +.table tbody tr:hover { + background: var(--td-bg-color-page); +} + +.table tbody tr:last-child { + border-bottom: none; +} + +.table td { + padding: 16px; + vertical-align: middle; + color: var(--td-text-color-primary); + border: none; +} + +/* ========== 表单样式 ========== */ +.form-control { + background: var(--td-bg-color-container); + border: 1px solid var(--td-border-color); + border-radius: var(--td-border-radius); + padding: 8px 12px; + font-size: 14px; + color: var(--td-text-color-primary); + transition: all 0.2s cubic-bezier(0.38, 0, 0.24, 1); +} + +.form-control:focus { + border-color: var(--td-brand-color); + box-shadow: 0 0 0 2px rgba(0, 82, 217, 0.1); + outline: none; +} + +.form-control::placeholder { + color: var(--td-text-color-placeholder); +} + +label, .form-label { + color: var(--td-text-color-primary); + font-weight: 500; + font-size: 14px; + margin-bottom: 8px; +} + +/* ========== 按钮样式 ========== */ +.btn { + padding: 8px 16px; + border-radius: var(--td-border-radius); + font-size: 14px; + font-weight: 500; + border: none; + cursor: pointer; + transition: all 0.2s cubic-bezier(0.38, 0, 0.24, 1); + display: inline-flex; + align-items: center; + gap: 6px; +} + +.btn-primary { + background: var(--td-brand-color); + color: #FFFFFF; +} + +.btn-primary:hover { + background: var(--td-brand-color-hover); + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(0, 82, 217, 0.2); +} + +.btn-success { + background: var(--td-success-color); + color: #FFFFFF; +} + +.btn-warning { + background: var(--td-warning-color); + color: #FFFFFF; +} + +.btn-danger { + background: var(--td-error-color); + color: #FFFFFF; +} + +.btn-secondary, .btn-default { + background: var(--td-bg-color-container); + border: 1px solid var(--td-border-color); + color: var(--td-text-color-primary); +} + +.btn-secondary:hover, .btn-default:hover { + background: var(--td-bg-color-container-hover); +} + +/* ========== 卡片样式 ========== */ +.card, .panel { + background: var(--td-bg-color-container); + border: 1px solid var(--td-border-color); + border-radius: var(--td-border-radius-medium); + box-shadow: var(--td-shadow-1); + margin-bottom: 16px; +} + +.card-header, .panel-heading { + background: transparent; + border-bottom: 1px solid var(--td-border-color); + padding: 16px 20px; + font-weight: 600; + font-size: 16px; +} + +.card-body, .panel-body { + padding: 20px; +} + +/* ========== 警告框 ========== */ +.alert { + border-radius: var(--td-border-radius-medium); + border: none; + padding: 12px 16px; + margin-bottom: 16px; +} + +.alert-success { + background: rgba(0, 168, 112, 0.1); + color: var(--td-success-color); +} + +.alert-danger { + background: rgba(213, 73, 65, 0.1); + color: var(--td-error-color); +} + +.alert-info { + background: rgba(0, 82, 217, 0.1); + color: var(--td-brand-color); +} + +.alert-warning { + background: rgba(227, 115, 24, 0.1); + color: var(--td-warning-color); +} + +/* ========== 分页 ========== */ +.pagination { + display: flex; + gap: 4px; + margin: 20px 0; +} + +.pagination .page-link { + background: var(--td-bg-color-container); + border: 1px solid var(--td-border-color); + color: var(--td-text-color-primary); + border-radius: var(--td-border-radius); + padding: 6px 12px; + transition: all 0.2s; +} + +.pagination .page-link:hover { + background: var(--td-brand-color-light); + border-color: var(--td-brand-color); + color: var(--td-brand-color); + text-decoration: none; +} + +.pagination .page-item.active .page-link { + background: var(--td-brand-color); + border-color: var(--td-brand-color); + color: #FFFFFF; +} + +.pagination .page-item.disabled .page-link { + background: var(--td-bg-color-page); + border-color: var(--td-border-color); + color: var(--td-text-color-placeholder); + cursor: not-allowed; +} + +/* ========== 模态框 ========== */ +.modal-content { + background: var(--td-bg-color-container); + border: 1px solid var(--td-border-color); + border-radius: var(--td-border-radius-medium); + box-shadow: var(--td-shadow-2); +} + +.modal-header { + border-bottom: 1px solid var(--td-border-color); + padding: 20px 24px; +} + +.modal-title { + font-size: 18px; + font-weight: 600; + color: var(--td-text-color-primary); +} + +.modal-body { + padding: 24px; +} + +.modal-footer { + border-top: 1px solid var(--td-border-color); + padding: 16px 24px; +} + +/* ========== 下拉菜单 ========== */ +.dropdown-menu { + background: var(--td-bg-color-container); + border: 1px solid var(--td-border-color); + border-radius: var(--td-border-radius-medium); + box-shadow: var(--td-shadow-2); +} + +.dropdown-item { + color: var(--td-text-color-primary); + padding: 8px 16px; + font-size: 14px; +} + +.dropdown-item:hover { + background: var(--td-brand-color-light); + color: var(--td-brand-color); +} + +/* ========== Select2 ========== */ +.select2-container--bootstrap4 .select2-selection { + background: var(--td-bg-color-container); + border: 1px solid var(--td-border-color); + border-radius: var(--td-border-radius); +} + +.select2-dropdown { + background: var(--td-bg-color-container); + border: 1px solid var(--td-border-color); + border-radius: var(--td-border-radius-medium); + box-shadow: var(--td-shadow-2); +} + +.select2-results__option { + color: var(--td-text-color-primary); + padding: 8px 12px; +} + +.select2-results__option--highlighted { + background: var(--td-brand-color-light); + color: var(--td-brand-color); +} + +/* ========== 滚动条 ========== */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: var(--td-bg-color-page); +} + +::-webkit-scrollbar-thumb { + background: var(--td-border-color); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--td-text-color-placeholder); +} + +/* ========== 响应式 ========== */ +@media (max-width: 768px) { + .admin-sidebar { + transform: translateX(-100%); + transition: transform 0.3s; + } + + .admin-main { + margin-left: 0; + } + + .search-box { + display: none; + } +} diff --git a/static/css/admin-theme.css b/static/css/admin-theme.css index 0a32977..dd75cc6 100644 --- a/static/css/admin-theme.css +++ b/static/css/admin-theme.css @@ -1,295 +1,459 @@ -/* ========== Flask-Admin 后台科技感主题 - ZJPB焦提示词 ========== */ +/* ========== Flask-Admin TDesign主题 - 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; +/* TDesign 色彩系统 */ +:root { + --td-brand-color: #0052D9; + --td-brand-color-hover: #266FE8; + --td-brand-color-active: #0034B5; + --td-brand-color-light: #ECF2FE; + --td-success-color: #00A870; + --td-warning-color: #E37318; + --td-error-color: #D54941; + --td-bg-color-page: #F3F3F3; + --td-bg-color-container: #FFFFFF; + --td-bg-color-container-hover: #F5F5F5; + --td-text-color-primary: #000000; + --td-text-color-secondary: #606266; + --td-text-color-placeholder: #C0C4CC; + --td-border-color: #DCDFE6; + --td-border-color-light: #E4E7ED; + --td-border-radius: 3px; + --td-border-radius-medium: 6px; + --td-shadow-1: 0 1px 10px rgba(0, 0, 0, .05); + --td-shadow-2: 0 2px 20px rgba(0, 0, 0, .08); +} + +/* 亮色模式 - 全局覆盖 */ +body, body.admin-theme, .admin-theme { + background: var(--td-bg-color-page) !important; + color: var(--td-text-color-primary) !important; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif !important; +} + +/* 主容器 */ +.container-fluid, .container { + background: transparent !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, .navbar-default, .navbar-inverse, .navbar-admin { + background: #FFFFFF !important; + border-bottom: 1px solid var(--td-border-color) !important; + box-shadow: var(--td-shadow-1) !important; + border: none !important; + min-height: 64px !important; } -.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; +.navbar-brand, .navbar .navbar-brand { + color: var(--td-brand-color) !important; + font-weight: 600 !important; + font-size: 18px !important; +} + +.navbar-nav .nav-link, .navbar-nav > li > a { + color: var(--td-text-color-secondary) !important; + font-size: 14px !important; + padding: 8px 16px !important; + border-radius: var(--td-border-radius) !important; + transition: all 0.2s ease !important; +} + +.navbar-nav .nav-link:hover, .navbar-nav > li > a:hover, +.navbar-nav .nav-link:focus, .navbar-nav > li > a:focus { + color: var(--td-text-color-primary) !important; + background: var(--td-bg-color-container-hover) !important; } /* 侧边栏 */ -.nav-sidebar { - background: rgba(27, 36, 39, 0.8) !important; +.nav-sidebar, .nav.nav-pills { + background: #FFFFFF !important; + border-right: 1px solid var(--td-border-color) !important; } -.nav-sidebar .nav-link { - color: #9cb2ba !important; - transition: all 0.3s ease; +.nav-sidebar .nav-link, .nav-sidebar > li > a, +.nav-pills > li > a { + color: var(--td-text-color-secondary) !important; + padding: 12px 16px !important; + margin: 4px 8px !important; + border-radius: var(--td-border-radius-medium) !important; + font-size: 14px !important; + transition: all 0.2s cubic-bezier(0.38, 0, 0.24, 1) !important; + background: transparent !important; } -.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; +.nav-sidebar .nav-link:hover, .nav-sidebar > li > a:hover, +.nav-pills > li > a:hover { + color: var(--td-text-color-primary) !important; + background: var(--td-brand-color-light) !important; +} + +.nav-sidebar .nav-link.active, .nav-sidebar > li.active > a, +.nav-pills > li.active > a { + color: var(--td-brand-color) !important; + background: var(--td-brand-color-light) !important; + font-weight: 500 !important; } /* 卡片和面板 */ .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); + background: var(--td-bg-color-container) !important; + border: 1px solid var(--td-border-color) !important; + border-radius: var(--td-border-radius-medium) !important; + box-shadow: var(--td-shadow-1); + margin-bottom: 16px; } .card-header, .panel-heading { - background: rgba(37, 192, 244, 0.08) !important; - border-bottom: 1px solid #283539 !important; - color: #ffffff !important; + background: transparent !important; + border-bottom: 1px solid var(--td-border-color) !important; + color: var(--td-text-color-primary) !important; font-weight: 600; + font-size: 16px; + padding: 16px !important; } /* 表格 */ .table { - color: #ffffff !important; + color: var(--td-text-color-primary) !important; + font-size: 14px; + border-collapse: separate; + border-spacing: 0; } .table thead th { - background: rgba(30, 39, 44, 0.8) !important; - border-color: #283539 !important; - color: #9cb2ba !important; - font-size: 11px; + background: var(--td-bg-color-container-hover) !important; + border: none !important; + color: var(--td-text-color-secondary) !important; + font-size: 12px; font-weight: 600; text-transform: uppercase; - letter-spacing: 0.05em; + letter-spacing: 0.5px; + padding: 12px 16px !important; + height: 48px; +} + +.table thead th:first-child { + border-top-left-radius: var(--td-border-radius); +} + +.table thead th:last-child { + border-top-right-radius: var(--td-border-radius); } .table tbody tr { background: transparent !important; - border-color: #283539 !important; - transition: all 0.2s ease; + border-bottom: 1px solid var(--td-border-color-light) !important; + transition: background-color 0.2s ease; } .table tbody tr:hover { - background: rgba(30, 39, 44, 0.5) !important; + background: var(--td-brand-color-light) !important; } -.table td, .table th { - border-color: #283539 !important; - color: #ffffff !important; +.table tbody tr:last-child { + border-bottom: none !important; +} + +.table td { + border: none !important; + color: var(--td-text-color-primary) !important; + padding: 16px !important; + vertical-align: middle; +} + +.table th { + border: none !important; } /* 表单 */ .form-control { - background: #111618 !important; - border: 1px solid #283539 !important; - color: #ffffff !important; - border-radius: 8px !important; - transition: all 0.3s ease; + background: var(--td-bg-color-container) !important; + border: 1px solid var(--td-border-color) !important; + color: var(--td-text-color-primary) !important; + border-radius: var(--td-border-radius) !important; + padding: 8px 12px !important; + font-size: 14px; + line-height: 22px; + transition: all 0.2s cubic-bezier(0.38, 0, 0.24, 1); } .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; + background: var(--td-bg-color-container) !important; + border-color: var(--td-brand-color) !important; + box-shadow: 0 0 0 2px rgba(0, 82, 217, 0.1) !important; + outline: none; } .form-control::placeholder { - color: #9cb2ba !important; + color: var(--td-text-color-placeholder) !important; } .form-label, label { - color: #9cb2ba !important; + color: var(--td-text-color-primary) !important; font-weight: 500; - font-size: 13px; + font-size: 14px; + margin-bottom: 8px; +} + +.form-text { + color: var(--td-text-color-secondary) !important; + font-size: 12px; } /* 按钮 */ +.btn { + border-radius: var(--td-border-radius) !important; + font-size: 14px; + font-weight: 500; + padding: 8px 16px !important; + line-height: 22px; + transition: all 0.2s cubic-bezier(0.38, 0, 0.24, 1); + border: none; +} + .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; + background: var(--td-brand-color) !important; + color: #FFFFFF !important; + box-shadow: 0 2px 4px rgba(0, 82, 217, 0.2); } .btn-primary:hover { - background: #1fa8d8 !important; - transform: translateY(-2px); - box-shadow: 0 0 30px rgba(37, 192, 244, 0.5); + background: var(--td-brand-color-hover) !important; + box-shadow: 0 4px 8px rgba(0, 82, 217, 0.3); + transform: translateY(-1px); +} + +.btn-primary:active { + background: var(--td-brand-color-active) !important; + transform: translateY(0); +} + +.btn-success { + background: var(--td-success-color) !important; + color: #FFFFFF !important; +} + +.btn-warning { + background: var(--td-warning-color) !important; + color: #FFFFFF !important; +} + +.btn-danger { + background: var(--td-error-color) !important; + color: #FFFFFF !important; } .btn-info { - background: linear-gradient(135deg, #25c0f4 0%, #00f2fe 100%) !important; - border: none !important; - color: #111618 !important; - font-weight: 600; + background: #029CD4 !important; + color: #FFFFFF !important; } .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; + background: #FFFFFF !important; + border: 1px solid var(--td-border-color) !important; + color: var(--td-text-color-primary) !important; } .btn-secondary:hover, .btn-default:hover { - background: rgba(52, 66, 71, 0.8) !important; - border-color: #4a5a60 !important; + background: var(--td-bg-color-container-hover) !important; + border-color: var(--td-border-color) !important; } /* 模态框 */ .modal-content { - background: rgba(27, 36, 39, 0.95) !important; - backdrop-filter: blur(20px); - border: 1px solid #283539 !important; - border-radius: 12px !important; + background: var(--td-bg-color-container) !important; + border: 1px solid var(--td-border-color) !important; + border-radius: var(--td-border-radius-medium) !important; + box-shadow: var(--td-shadow-2); } .modal-header { - border-bottom-color: #283539 !important; - background: rgba(37, 192, 244, 0.05); + border-bottom: 1px solid var(--td-border-color) !important; + padding: 20px 24px; +} + +.modal-title { + color: var(--td-text-color-primary) !important; + font-size: 18px; + font-weight: 600; +} + +.modal-body { + padding: 24px; } .modal-footer { - border-top-color: #283539 !important; + border-top: 1px solid var(--td-border-color) !important; + padding: 16px 24px; } /* 分页 */ +.pagination { + gap: 4px; +} + .pagination .page-link { - background: rgba(27, 36, 39, 0.6) !important; - border-color: #283539 !important; - color: #9cb2ba !important; + background: var(--td-bg-color-container) !important; + border: 1px solid var(--td-border-color) !important; + color: var(--td-text-color-primary) !important; + border-radius: var(--td-border-radius) !important; + padding: 6px 12px; + margin: 0; transition: all 0.2s ease; } .pagination .page-link:hover { - background: rgba(37, 192, 244, 0.1) !important; - border-color: #25c0f4 !important; - color: #ffffff !important; + background: var(--td-brand-color-light) !important; + border-color: var(--td-brand-color) !important; + color: var(--td-brand-color) !important; } .pagination .page-item.active .page-link { - background: #25c0f4 !important; - border-color: #25c0f4 !important; - color: #111618 !important; + background: var(--td-brand-color) !important; + border-color: var(--td-brand-color) !important; + color: #FFFFFF !important; +} + +.pagination .page-item.disabled .page-link { + background: var(--td-bg-color-container) !important; + border-color: var(--td-border-color) !important; + color: var(--td-text-color-placeholder) !important; + cursor: not-allowed; } /* 警告框 */ .alert { - background: rgba(27, 36, 39, 0.8) !important; - border: 1px solid #283539 !important; - color: #ffffff !important; - border-radius: 8px !important; + border-radius: var(--td-border-radius-medium) !important; + border: none !important; + padding: 12px 16px; + font-size: 14px; } .alert-success { - background: rgba(34, 197, 94, 0.1) !important; - border-color: rgba(34, 197, 94, 0.3) !important; - color: #4ade80 !important; + background: rgba(0, 168, 112, 0.1) !important; + color: #00A870 !important; } .alert-danger { - background: rgba(239, 68, 68, 0.1) !important; - border-color: rgba(239, 68, 68, 0.3) !important; - color: #f87171 !important; + background: rgba(213, 73, 65, 0.1) !important; + color: #D54941 !important; } .alert-info { - background: rgba(37, 192, 244, 0.1) !important; - border-color: rgba(37, 192, 244, 0.3) !important; - color: #25c0f4 !important; + background: rgba(2, 156, 212, 0.1) !important; + color: #029CD4 !important; } .alert-warning { - background: rgba(251, 191, 36, 0.1) !important; - border-color: rgba(251, 191, 36, 0.3) !important; - color: #fbbf24 !important; + background: rgba(227, 115, 24, 0.1) !important; + color: #E37318 !important; } /* 链接 */ a { - color: #25c0f4 !important; + color: var(--td-brand-color) !important; text-decoration: none; transition: color 0.2s ease; } a:hover { - color: #1fa8d8 !important; + color: var(--td-brand-color-hover) !important; } /* 文本颜色 */ .text-muted { - color: #9cb2ba !important; + color: var(--td-text-color-secondary) !important; } /* 输入组 */ .input-group-text { - background: rgba(27, 36, 39, 0.6) !important; - border-color: #283539 !important; - color: #9cb2ba !important; + background: var(--td-bg-color-container-hover) !important; + border: 1px solid var(--td-border-color) !important; + color: var(--td-text-color-secondary) !important; } /* Select2 下拉框 */ .select2-container--bootstrap4 .select2-selection { - background: #111618 !important; - border-color: #283539 !important; - color: #ffffff !important; + background: var(--td-bg-color-container) !important; + border: 1px solid var(--td-border-color) !important; + color: var(--td-text-color-primary) !important; + border-radius: var(--td-border-radius) !important; } .select2-dropdown { - background: rgba(27, 36, 39, 0.95) !important; - border-color: #283539 !important; - backdrop-filter: blur(10px); + background: var(--td-bg-color-container) !important; + border: 1px solid var(--td-border-color) !important; + border-radius: var(--td-border-radius-medium) !important; + box-shadow: var(--td-shadow-2); } .select2-results__option { - color: #ffffff !important; + color: var(--td-text-color-primary) !important; + padding: 8px 12px; } .select2-results__option--highlighted { - background: rgba(37, 192, 244, 0.2) !important; + background: var(--td-brand-color-light) !important; + color: var(--td-brand-color) !important; } /* 徽章 */ +.badge { + border-radius: 2px !important; + font-size: 12px; + font-weight: 500; + padding: 2px 8px; +} + .badge-primary { - background: #25c0f4 !important; - color: #111618 !important; + background: var(--td-brand-color) !important; + color: #FFFFFF !important; +} + +.badge-success { + background: var(--td-success-color) !important; + color: #FFFFFF !important; +} + +.badge-warning { + background: var(--td-warning-color) !important; + color: #FFFFFF !important; +} + +.badge-danger { + background: var(--td-error-color) !important; + color: #FFFFFF !important; } .badge-secondary { - background: #283539 !important; - color: #9cb2ba !important; + background: var(--td-bg-color-container-hover) !important; + color: var(--td-text-color-secondary) !important; } /* 进度条 */ .progress { - background: rgba(27, 36, 39, 0.6) !important; + background: var(--td-bg-color-container-hover) !important; + border-radius: 2px !important; + height: 8px; } .progress-bar { - background: #25c0f4 !important; + background: var(--td-brand-color) !important; } -/* 额外优化 */ -.navbar-nav .nav-link { - color: #9cb2ba !important; +/* 复选框和单选框 */ +.form-check-input { + background-color: var(--td-bg-color-container) !important; + border: 1px solid var(--td-border-color) !important; } -.navbar-nav .nav-link:hover { - color: #ffffff !important; +.form-check-input:checked { + background-color: var(--td-brand-color) !important; + border-color: var(--td-brand-color) !important; +} + +.form-check-input:focus { + box-shadow: 0 0 0 2px rgba(0, 82, 217, 0.1) !important; } /* 自定义滚动条 */ @@ -299,15 +463,123 @@ a:hover { } ::-webkit-scrollbar-track { - background: #111618; + background: var(--td-bg-color-page); } ::-webkit-scrollbar-thumb { - background: #283539; + background: var(--td-border-color); border-radius: 4px; } ::-webkit-scrollbar-thumb:hover { - background: #3a4b50; + background: var(--td-text-color-placeholder); +} + +/* 工具提示 */ +.tooltip-inner { + background: var(--td-text-color-primary) !important; + color: #FFFFFF !important; + border-radius: var(--td-border-radius) !important; + padding: 6px 12px; + font-size: 12px; +} + +/* 面包屑 */ +.breadcrumb { + background: transparent !important; + padding: 0; + margin-bottom: 16px; +} + +.breadcrumb-item { + color: var(--td-text-color-secondary) !important; + font-size: 14px; +} + +.breadcrumb-item.active { + color: var(--td-text-color-primary) !important; +} + +.breadcrumb-item + .breadcrumb-item::before { + color: var(--td-text-color-placeholder) !important; +} + +/* 标签页 */ +.nav-tabs { + border-bottom: 1px solid var(--td-border-color) !important; +} + +.nav-tabs .nav-link { + border: none !important; + color: var(--td-text-color-secondary) !important; + padding: 12px 24px; + margin-bottom: -1px; + border-bottom: 2px solid transparent; +} + +.nav-tabs .nav-link:hover { + color: var(--td-text-color-primary) !important; + border-bottom-color: var(--td-border-color) !important; +} + +.nav-tabs .nav-link.active { + color: var(--td-brand-color) !important; + background: transparent !important; + border-bottom-color: var(--td-brand-color) !important; +} + +/* 下拉菜单 */ +.dropdown-menu { + background: var(--td-bg-color-container) !important; + border: 1px solid var(--td-border-color) !important; + border-radius: var(--td-border-radius-medium) !important; + box-shadow: var(--td-shadow-2); +} + +.dropdown-item { + color: var(--td-text-color-primary) !important; + padding: 8px 16px; + font-size: 14px; +} + +.dropdown-item:hover { + background: var(--td-brand-color-light) !important; + color: var(--td-brand-color) !important; +} + +/* 表格操作按钮 */ +.table .btn-sm { + padding: 4px 12px !important; + font-size: 12px; +} + +/* 空状态 */ +.empty-state { + text-align: center; + padding: 48px 24px; + color: var(--td-text-color-secondary); +} + +.empty-state-icon { + font-size: 48px; + color: var(--td-text-color-placeholder); + margin-bottom: 16px; +} + +/* 加载状态 */ +.spinner-border { + border-color: var(--td-brand-color); + border-right-color: transparent; +} + +/* 响应式优化 */ +@media (max-width: 768px) { + .table { + font-size: 12px; + } + + .table td, .table th { + padding: 8px !important; + } } diff --git a/stitch_ai_tool_detail_page/admin_login_page/code.html b/stitch_ai_tool_detail_page/admin_login_page/code.html new file mode 100644 index 0000000..1a3ce5d --- /dev/null +++ b/stitch_ai_tool_detail_page/admin_login_page/code.html @@ -0,0 +1,108 @@ + + + + +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/admin_login_page/screen.png b/stitch_ai_tool_detail_page/admin_login_page/screen.png new file mode 100644 index 0000000..1196c94 Binary files /dev/null and b/stitch_ai_tool_detail_page/admin_login_page/screen.png differ diff --git a/stitch_ai_tool_detail_page/admin_management_interface/code.html b/stitch_ai_tool_detail_page/admin_management_interface/code.html new file mode 100644 index 0000000..444c7e6 --- /dev/null +++ b/stitch_ai_tool_detail_page/admin_management_interface/code.html @@ -0,0 +1,365 @@ + + + + +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/admin_management_interface/screen.png b/stitch_ai_tool_detail_page/admin_management_interface/screen.png new file mode 100644 index 0000000..e7acc33 Binary files /dev/null and b/stitch_ai_tool_detail_page/admin_management_interface/screen.png differ diff --git a/stitch_ai_tool_detail_page/ai_tool_detail_page/code.html b/stitch_ai_tool_detail_page/ai_tool_detail_page/code.html new file mode 100644 index 0000000..082a562 --- /dev/null +++ b/stitch_ai_tool_detail_page/ai_tool_detail_page/code.html @@ -0,0 +1,351 @@ + + + + +AI Tool Detail Page (Light Theme) + + + + + + + + + +
+
+
+
+
+
+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. +

+
+
+
+

+newspaper + Related News +

+ +
+
+

+auto_awesome + Similar Recommendations +

+ +
+
+
+
+
+
+Try it now +ONLINE +
+ +
+ + 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/ai_tool_detail_page/screen.png b/stitch_ai_tool_detail_page/ai_tool_detail_page/screen.png new file mode 100644 index 0000000..13bbea4 Binary files /dev/null and b/stitch_ai_tool_detail_page/ai_tool_detail_page/screen.png differ diff --git a/stitch_ai_tool_detail_page/ai_tool_home_page/code.html b/stitch_ai_tool_detail_page/ai_tool_home_page/code.html new file mode 100644 index 0000000..689f668 --- /dev/null +++ b/stitch_ai_tool_detail_page/ai_tool_home_page/code.html @@ -0,0 +1,357 @@ + + + + +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/ai_tool_home_page/screen.png b/stitch_ai_tool_detail_page/ai_tool_home_page/screen.png new file mode 100644 index 0000000..6f44ea5 Binary files /dev/null and b/stitch_ai_tool_detail_page/ai_tool_home_page/screen.png differ 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 deleted file mode 100644 index 4cd9494..0000000 --- a/stitch_ai_tool_detail_page/stitch_ai_tool_detail_page/admin_login_page/code.html +++ /dev/null @@ -1,126 +0,0 @@ - - - - - -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 deleted file mode 100644 index e666f9e..0000000 Binary files a/stitch_ai_tool_detail_page/stitch_ai_tool_detail_page/admin_login_page/screen.png and /dev/null 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 deleted file mode 100644 index c42420c..0000000 --- a/stitch_ai_tool_detail_page/stitch_ai_tool_detail_page/admin_management_interface/code.html +++ /dev/null @@ -1,394 +0,0 @@ - - - - - -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 deleted file mode 100644 index 9dc84d7..0000000 Binary files a/stitch_ai_tool_detail_page/stitch_ai_tool_detail_page/admin_management_interface/screen.png and /dev/null 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 deleted file mode 100644 index 59939eb..0000000 --- a/stitch_ai_tool_detail_page/stitch_ai_tool_detail_page/ai_tool_detail_page/code.html +++ /dev/null @@ -1,270 +0,0 @@ - - - - - -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 deleted file mode 100644 index 24fc60f..0000000 Binary files a/stitch_ai_tool_detail_page/stitch_ai_tool_detail_page/ai_tool_detail_page/screen.png and /dev/null 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 deleted file mode 100644 index 561dce5..0000000 --- a/stitch_ai_tool_detail_page/stitch_ai_tool_detail_page/ai_tool_home_page/code.html +++ /dev/null @@ -1,383 +0,0 @@ - - - - - -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 deleted file mode 100644 index 8256848..0000000 Binary files a/stitch_ai_tool_detail_page/stitch_ai_tool_detail_page/ai_tool_home_page/screen.png and /dev/null differ diff --git a/templates/admin/batch_import.html b/templates/admin/batch_import.html new file mode 100644 index 0000000..b09d75c --- /dev/null +++ b/templates/admin/batch_import.html @@ -0,0 +1,363 @@ + + + + + + 批量导入 - ZJPB 焦提示词 + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ 控制台 + / + 批量导入 +
+
+ + + +
+
+ + +
+ + + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} + + {% endfor %} + {% endif %} + {% endwith %} + +
+
+ + +
+ +
+
+ + +
+ + + + 每行输入一个网站URL,系统将自动抓取网站名称、描述和Logo + +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ 如何导出Chrome书签? +
    +
  1. 打开Chrome浏览器
  2. +
  3. Ctrl + Shift + O 打开书签管理器
  4. +
  5. 点击右上角的 菜单
  6. +
  7. 选择 导出书签
  8. +
  9. 保存为HTML文件
  10. +
+
+ +
+ +
+ + +
+ + 仅支持HTML格式的Chrome书签导出文件 + +
+ +
+ + + + 留空则导入所有书签,填写文件夹名称则只导入该文件夹下的书签 + +
+ +
+
+ + +
+
+ + +
+
+
+
+
+ + {% if results %} +
+
+
导入结果
+
+
+
+ 导入完成! + 成功: {{ results.success_count }}, + 失败: {{ results.failed_count }}, + 总计: {{ results.total_count }} +
+ + {% if results.success_list %} +
成功导入 ({{ results.success_count }})
+
    + {% for item in results.success_list %} +
  • + check_circle + {{ item.name }} - {{ item.url }} +
  • + {% endfor %} +
+ {% endif %} + + {% if results.failed_list %} +
导入失败 ({{ results.failed_count }})
+
+ 提示:失败的URL不会影响其他URL的导入,您可以稍后手动添加这些网站。 +
+
+ + + + + + + + + + + {% for item in results.failed_list %} + + + + + + + {% endfor %} + +
#网站名称URL失败原因
+ cancel + + {{ item.name or '未知' }} + + {{ item.url }} + + {{ item.error }} +
+
+ {% endif %} +
+
+ {% endif %} +
+
+ + + + + + + + + + diff --git a/templates/admin/custom_base.html b/templates/admin/custom_base.html index 090e5ce..cccc5de 100644 --- a/templates/admin/custom_base.html +++ b/templates/admin/custom_base.html @@ -8,10 +8,14 @@ + {% endblock %} -{% block body %} - -{{ super() }} - -{% endblock %} +{% block body_class %}admin-theme{% endblock %} diff --git a/templates/admin/index.html b/templates/admin/index.html new file mode 100644 index 0000000..002160b --- /dev/null +++ b/templates/admin/index.html @@ -0,0 +1,239 @@ +{% extends 'admin/master.html' %} + +{% block body %} +
+
+ +
+
+
+ public +
+
+
{{ stats.sites_count or 0 }}
+
AI工具总数
+
+
+
+ +
+
+
+ label +
+
+
{{ stats.tags_count or 0 }}
+
标签分类
+
+
+
+ +
+
+
+ newspaper +
+
+
{{ stats.news_count or 0 }}
+
新闻动态
+
+
+
+ +
+
+
+ visibility +
+
+
{{ stats.total_views or 0 }}
+
总浏览量
+
+
+
+
+ + + + + +
+
+
+
+
最近添加的工具
+
+
+ {% if recent_sites %} +
+ + + + + + + + + + + + {% for site in recent_sites %} + + + + + + + + {% endfor %} + +
名称URL浏览量状态添加时间
+
+ {% if site.logo %} + {{ site.name }} + {% endif %} + {{ site.name }} +
+
+ + {{ site.url[:50] }}... + + {{ site.view_count }} + {% if site.is_active %} + 已启用 + {% else %} + 已禁用 + {% endif %} + {{ site.created_at.strftime('%Y-%m-%d %H:%M') }}
+
+ {% else %} +

暂无数据

+ {% endif %} +
+
+
+
+
+ + +{% endblock %} diff --git a/templates/admin/master.html b/templates/admin/master.html new file mode 100644 index 0000000..e499e4a --- /dev/null +++ b/templates/admin/master.html @@ -0,0 +1,176 @@ +{% import 'admin/layout.html' as layout with context -%} +{% import 'admin/static.html' as admin_static with context %} + + + + + + {% block title %}{% if admin_view.category %}{{ admin_view.category }} - {% endif %}{{ admin_view.name }} - {{ admin_view.admin.name }}{% endblock %} + + + + + + + + + + + + + + + + + + + {% block head_css %}{% endblock %} + + + + + + +
+ +
+
+ 控制台 + / + {{ admin_view.name }} +
+
+ + + +
+
+ + +
+ {% block page_body %} + + + {% block messages %} + {{ layout.messages() }} + {% endblock %} + + {% block body %}{% endblock %} + {% endblock %} +
+
+ + + + + + {% block tail_js %}{% endblock %} + {% block tail %}{% endblock %} + + diff --git a/templates/admin/site/create.html b/templates/admin/site/create.html index 6b87516..f32bed6 100644 --- a/templates/admin/site/create.html +++ b/templates/admin/site/create.html @@ -7,30 +7,40 @@ margin-top: 10px; margin-bottom: 15px; } + .generate-tags-btn { + margin-top: 10px; + margin-bottom: 15px; + } .fetch-status { margin-top: 10px; padding: 10px; border-radius: 8px; display: none; } - .fetch-status.success { + .tags-status { + margin-top: 10px; + padding: 10px; + border-radius: 8px; + display: none; + } + .fetch-status.success, .tags-status.success { background-color: rgba(34, 197, 94, 0.1); border: 1px solid rgba(34, 197, 94, 0.3); color: #4ade80; } - .fetch-status.error { + .fetch-status.error, .tags-status.error { background-color: rgba(239, 68, 68, 0.1); border: 1px solid rgba(239, 68, 68, 0.3); color: #f87171; } - .auto-fetch-btn .loading-icon { + .auto-fetch-btn .loading-icon, .generate-tags-btn .loading-icon { display: none; } - .auto-fetch-btn.loading .loading-icon { + .auto-fetch-btn.loading .loading-icon, .generate-tags-btn.loading .loading-icon { display: inline-block; animation: spin 1s linear infinite; } - .auto-fetch-btn.loading .normal-icon { + .auto-fetch-btn.loading .normal-icon, .generate-tags-btn.loading .normal-icon { display: none; } @keyframes spin { @@ -119,6 +129,75 @@ document.addEventListener('DOMContentLoaded', function() { statusDiv.style.display = 'block'; } } + + // 在标签字段后添加"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 = ' AI生成标签'; + + const tagsStatusDiv = document.createElement('div'); + tagsStatusDiv.className = 'tags-status'; + + tagsField.parentNode.appendChild(generateBtn); + tagsField.parentNode.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) { + // 显示生成的标签 + const tagsText = data.tags.join(', '); + showTagsStatus('✓ AI生成的标签建议: ' + tagsText + '\n(请在标签字段中手动选择或创建这些标签)', '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'; + } + } }); {% endblock %} diff --git a/templates/admin/site/edit.html b/templates/admin/site/edit.html index 355867d..aef6d86 100644 --- a/templates/admin/site/edit.html +++ b/templates/admin/site/edit.html @@ -7,30 +7,40 @@ margin-top: 10px; margin-bottom: 15px; } + .generate-tags-btn { + margin-top: 10px; + margin-bottom: 15px; + } .fetch-status { margin-top: 10px; padding: 10px; border-radius: 8px; display: none; } - .fetch-status.success { + .tags-status { + margin-top: 10px; + padding: 10px; + border-radius: 8px; + display: none; + } + .fetch-status.success, .tags-status.success { background-color: rgba(34, 197, 94, 0.1); border: 1px solid rgba(34, 197, 94, 0.3); color: #4ade80; } - .fetch-status.error { + .fetch-status.error, .tags-status.error { background-color: rgba(239, 68, 68, 0.1); border: 1px solid rgba(239, 68, 68, 0.3); color: #f87171; } - .auto-fetch-btn .loading-icon { + .auto-fetch-btn .loading-icon, .generate-tags-btn .loading-icon { display: none; } - .auto-fetch-btn.loading .loading-icon { + .auto-fetch-btn.loading .loading-icon, .generate-tags-btn.loading .loading-icon { display: inline-block; animation: spin 1s linear infinite; } - .auto-fetch-btn.loading .normal-icon { + .auto-fetch-btn.loading .normal-icon, .generate-tags-btn.loading .normal-icon { display: none; } @keyframes spin { @@ -119,6 +129,75 @@ document.addEventListener('DOMContentLoaded', function() { statusDiv.style.display = 'block'; } } + + // 在标签字段后添加"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 = ' AI生成标签'; + + const tagsStatusDiv = document.createElement('div'); + tagsStatusDiv.className = 'tags-status'; + + tagsField.parentNode.appendChild(generateBtn); + tagsField.parentNode.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) { + // 显示生成的标签 + const tagsText = data.tags.join(', '); + showTagsStatus('✓ AI生成的标签建议: ' + tagsText + '\n(请在标签字段中手动选择或创建这些标签)', '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'; + } + } }); {% endblock %} diff --git a/templates/base.html b/templates/base.html index bd94db0..9cca75c 100644 --- a/templates/base.html +++ b/templates/base.html @@ -56,6 +56,12 @@ box-shadow: 0 10px 30px -10px rgba(37, 192, 244, 0.15); border-color: #25c0f4; } + .line-clamp-2 { + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + } {% block extra_css %}{% endblock %} diff --git a/templates/base_new.html b/templates/base_new.html new file mode 100644 index 0000000..627d8e6 --- /dev/null +++ b/templates/base_new.html @@ -0,0 +1,278 @@ + + + + + + {% block title %}ZJPB - 焦提示词 | AI工具导航{% endblock %} + + + + + + + + + + + + + + + + + {% block content %}{% endblock %} + + + + + {% block extra_js %}{% endblock %} + + diff --git a/templates/detail.html b/templates/detail.html index 62cd79a..c7c12d3 100644 --- a/templates/detail.html +++ b/templates/detail.html @@ -106,6 +106,82 @@ {% endif %} + + + {% if news_list %} +
+

+ newspaper + 相关新闻 +

+ +
+ {% endif %} + + + {% if recommended_sites %} +
+

+ auto_awesome + 同类工具推荐 +

+ +
+ {% endif %} diff --git a/templates/detail_new.html b/templates/detail_new.html new file mode 100644 index 0000000..1033f7c --- /dev/null +++ b/templates/detail_new.html @@ -0,0 +1,577 @@ +{% extends 'base_new.html' %} + +{% block title %}{{ site.name }} - ZJPB AI工具导航{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block content %} +
+ +
+ + + + arrow_back + 返回首页 + + + +
+ + +
+ +
+
+ + {% if site.logo %} + {{ site.name }} + {% else %} +
+ {% endif %} + + +
+

{{ site.name }}

+ + {{ site.url }} + open_in_new + + +
+
+ visibility + {{ site.view_count | default(0) }} 次浏览 +
+
+ calendar_today + 添加于 {{ site.created_at.strftime('%Y年%m月%d日') }} +
+
+ +
+ {% for tag in site.tags %} + {{ tag.name }} + {% endfor %} + 免费试用 +
+
+
+
+ + +
+
+

立即访问

+ 在线 +
+ + 访问网站 + north_east + +

在新标签页打开 • {{ site.url.split('/')[2] if site.url else '' }}

+
+
+ + +
+ +
+ +
+

+ info + 产品概述 +

+

{{ site.description }}

+
+ + + {% if site.features %} +
+

+ description + 详细描述 +

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

+ newspaper + 相关新闻 +

+ {% for news in news_list %} +
+ {{ news.news_type }} +

{{ news.title }}

+

{{ news.content[:200] }}...

+
{{ news.published_at.strftime('%b %d, %Y') }}
+
+ {% endfor %} +
+ {% endif %} + + + {% if recommended_sites %} + + {% endif %} +
+ + + +
+
+{% endblock %} diff --git a/templates/index_new.html b/templates/index_new.html new file mode 100644 index 0000000..79f208a --- /dev/null +++ b/templates/index_new.html @@ -0,0 +1,412 @@ +{% extends 'base_new.html' %} + +{% block title %}ZJPB - 焦提示词 | 发现最新最好用的AI工具和产品{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block content %} + +
+
+
+

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

+

Discover the best AI tools tailored for your workflow

+
+
+ +
+ + + + + +
+ {% if search_query %} +
+

+ search + 搜索 "{{ search_query }}" 的结果:找到 {{ pagination.total if pagination else sites|length }} 个工具 + 清除搜索 +

+
+ {% endif %} + {% if sites %} + + + + {% if pagination and pagination.pages > 1 %} + + {% endif %} + {% else %} +
+ search_off +

暂无工具

+

{% if selected_tag %}该分类下还没有工具{% else %}还没有添加任何工具{% endif %}

+
+ {% endif %} +
+
+{% endblock %} diff --git a/temt/OneNav_Export_2025.12.28.html b/temt/OneNav_Export_2025.12.28.html new file mode 100644 index 0000000..12ffefe --- /dev/null +++ b/temt/OneNav_Export_2025.12.28.html @@ -0,0 +1,125 @@ + + + +Bookmarks +

Bookmarks

+

+

OneNav

+

+

OneNav默认分类

+

+

Stitch +
NewsNow +
BoxsHub +
AIPMClub +
ImgURL免费图床 +

+

AGI大模型

+

+

Perplexity +
硅基流动 SiliconFlow - 致力于成为全球领先的 AI 能力提供商 +
DeepSeek | 深度求索 +
Grok +
ChatGPT +
Claude +
Kimi +

+

API资源

+

+

「简米|Ping++」聚合支付系统-支付分账接口-B2B 支付解决方案-二清支付公司 +
OpenRouter +
AIHubMix - One Interface, Router All LLMs +
NewsAPI +
云雾API +
SerpAPI +

+

服务器管理

+

+

Connect, protect, and build everywhere | Cloudflare +
Hostinger - Bring Your Idea Online With a Website +
Semrush:数据驱动的营销工具,助您拓展业务 +
Pexels +
Zeabur - Your AI DevOps Engineer +
Railway +
GitHub · Change is constant. GitHub keeps you ahead. · GitHub +
宝塔面板(bt.cn) 简单好用的Linux/Windows服务器运维管理面板 +
1Panel - 现代化、开源的Linux服务器运维管理面板 +

+

AI工具

+

+

Gamma +
JetBrains: 软件开发者和团队的必备工具 +
shodan +
Similarweb: AI-Powered Digital Data Intelligence Solutions +
Trading View +
Sierra | Better customer experiences +
Product Hunt +
QuillBot: Your complete writing solution +
News Minimalist +
Fashion AI Model &amp; AI Generated Product Images for eCcommerce | Pic Copilot +
Canva +
BuildingAI | 开源AI框架 | FastBuildAI +
NotebookLLM +
IPRSS +

+

Agent平台

+

+

轻灵 +
Dify: Leading Agentic Workflow Builder +
flowith +
FastGPT - 企业级 AI Agent 搭建平台 +
AI Workflow Automation Platform &amp; Tools - n8n +

+

AI视频

+

+

Fogsight 雾象 | AI Animation Engine - Transform Concepts into Cinematic Visuals +
模力视频 - AIGC视频制作平台 | AI剪辑 | 云剪辑 | 海量模板 +
米壳AI - Medio.cool 助力企业海外视频推广 +
Relume — Websites designed &amp; built faster with AI | AI website builder +
fliki +
vidiq +
Creatify +

+

AI图像

+

+

AI Wind +
UI Design Made Easy, Powered By AI | Uizard +
AI设计生成代码:快速构建网站与App|UXbot +
AI Logo Maker | Logo Maker | Logo Diffusion +

+

AI建站

+

+

Stock Sentinel +
Appwrite - The developers&#039; cloud +
GitCode - 全球开发者的开源社区,开源代码托管平台 +
Lovable +
Base44 +

+

Youtube大神

+

+

技术爬爬虾 +

+

GitHub大神

+

+

Ramond +
Selei1983 +

+

神奇必推

+

+

LMArena +
PlayPhrase.me: Site for cinema archaeologists. +

+

其他收藏

+

+

Y Combinator +
Trade Map - Trade statistics for international business development +

+

本地服务

+

+

Aster - The next-gen perp DEX for all traders +
210飞牛 +

+

+

diff --git a/test_batch_import.py b/test_batch_import.py new file mode 100644 index 0000000..d641695 --- /dev/null +++ b/test_batch_import.py @@ -0,0 +1,145 @@ +"""测试批量导入功能的错误处理""" +import sys +import os + +# 设置UTF-8编码 +if sys.platform == 'win32': + import io + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') + +from utils.bookmark_parser import BookmarkParser +from utils.website_fetcher import WebsiteFetcher + +def test_url_parsing(): + """测试URL列表解析""" + print("=" * 50) + print("测试1: URL列表解析") + print("=" * 50) + + parser = BookmarkParser() + test_urls = """ +https://www.google.com +https://github.com +https://invalid-url-that-does-not-exist-12345.com +http://example.com +# 这是注释,应该被忽略 +https://www.python.org +""" + + urls = parser.parse_url_list(test_urls) + print(f"成功解析 {len(urls)} 个URL:") + for idx, url_info in enumerate(urls, 1): + print(f" {idx}. {url_info['url']}") + print() + + +def test_website_fetching(): + """测试网站信息抓取""" + print("=" * 50) + print("测试2: 网站信息抓取(含错误处理)") + print("=" * 50) + + fetcher = WebsiteFetcher(timeout=10) + + test_cases = [ + "https://www.google.com", + "https://github.com", + "https://invalid-url-that-does-not-exist-12345.com", # 这个应该失败 + "https://httpbin.org", + ] + + for idx, url in enumerate(test_cases, 1): + print(f"\n[{idx}/{len(test_cases)}] 正在抓取: {url}") + try: + info = fetcher.fetch_website_info(url) + if info: + print(f" [OK] 成功:") + print(f" 名称: {info.get('name', 'N/A')[:50]}") + print(f" 描述: {info.get('description', 'N/A')[:80]}") + print(f" Logo: {info.get('logo_url', 'N/A')[:60]}") + else: + print(f" [FAIL] 失败: 无法获取网站信息") + except Exception as e: + print(f" [ERROR] 异常: {str(e)[:100]}") + print() + + +def test_error_isolation(): + """测试错误隔离 - 确保一个失败不影响其他""" + print("=" * 50) + print("测试3: 错误隔离验证") + print("=" * 50) + + fetcher = WebsiteFetcher(timeout=5) + + urls = [ + {"url": "https://www.google.com", "name": "Google"}, + {"url": "https://invalid-url-12345.com", "name": "Invalid"}, # 这个会失败 + {"url": "https://github.com", "name": "GitHub"}, + {"url": "https://another-invalid-url.xyz", "name": "Invalid2"}, # 这个也会失败 + {"url": "https://www.python.org", "name": "Python"}, + ] + + success_count = 0 + failed_count = 0 + + for idx, item in enumerate(urls, 1): + url = item['url'] + name = item['name'] + + print(f"\n[{idx}/{len(urls)}] 处理: {name} ({url})") + + try: + # 模拟批量导入的错误处理逻辑 + info = None + try: + info = fetcher.fetch_website_info(url) + except Exception as e: + print(f" ⚠ 抓取失败: {str(e)[:50]}") + + if not info or not info.get('name'): + if name: + info = {'name': name, 'description': '', 'logo_url': ''} + print(f" [FALLBACK] 使用备用名称: {name}") + else: + failed_count += 1 + print(f" [SKIP] 跳过: 无法获取信息且无备用名称") + continue + + success_count += 1 + print(f" [SUCCESS] 成功: {info.get('name', 'N/A')[:50]}") + + except Exception as e: + failed_count += 1 + print(f" [ERROR] 未知错误: {str(e)[:50]}") + continue # 继续处理下一个 + + print(f"\n{'=' * 50}") + print(f"测试完成:") + print(f" 总计: {len(urls)}") + print(f" 成功: {success_count}") + print(f" 失败: {failed_count}") + print(f" 成功率: {success_count / len(urls) * 100:.1f}%") + print(f"{'=' * 50}\n") + + +if __name__ == "__main__": + try: + test_url_parsing() + test_website_fetching() + test_error_isolation() + + print("\n" + "=" * 50) + print("所有测试完成!") + print("=" * 50) + print("\n关键验证点:") + print("1. [OK] URL解析正常工作") + print("2. [OK] 网站信息抓取有错误处理") + print("3. [OK] 单个失败不影响其他URL处理") + print("4. [OK] 提供了备用方案(使用书签名称)") + + except Exception as e: + print(f"\n测试过程中出现错误: {str(e)}") + import traceback + traceback.print_exc() + sys.exit(1) diff --git a/test_site_creation.py b/test_site_creation.py new file mode 100644 index 0000000..1c386a9 --- /dev/null +++ b/test_site_creation.py @@ -0,0 +1,184 @@ +"""测试网站创建功能 - 验证code和slug自动生成""" +import sys +import os + +# 设置UTF-8编码 +if sys.platform == 'win32': + import io + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') + +from app import create_app +from models import db, Site + +def test_site_creation_with_autoflush(): + """测试通过Flask-Admin添加网站时的autoflush问题""" + print("=" * 60) + print("测试:验证on_model_change中的no_autoflush修复") + print("=" * 60) + + app = create_app() + + with app.app_context(): + try: + # 模拟Flask-Admin创建网站的过程 + print("\n1. 创建新网站对象(code和slug为空)...") + site = Site( + name='测试网站WaytoAGI', + url='https://test.waytoagi.com', + short_desc='这是一个测试网站', + description='用于测试code和slug自动生成功能', + is_active=True + ) + + print(f" 初始状态: code={site.code}, slug={site.slug}") + + # 添加到session + print("\n2. 添加到数据库session...") + db.session.add(site) + + # 模拟on_model_change的调用 + print("\n3. 执行on_model_change逻辑(生成code和slug)...") + import re + import random + from pypinyin import lazy_pinyin + + # 使用no_autoflush(这是修复的关键) + with db.session.no_autoflush: + # 生成code + if not site.code: + for _ in range(10): + code = str(random.randint(10000000, 99999999)) + existing = Site.query.filter(Site.code == code).first() + if not existing: + site.code = code + print(f" 生成code: {code}") + break + + # 生成slug + if not site.slug: + slug = ''.join(lazy_pinyin(site.name)) + slug = slug.lower() + slug = re.sub(r'[^\w\s-]', '', slug) + slug = re.sub(r'[-\s]+', '-', slug).strip('-') + + if not slug: + slug = f"site-{site.code}" + + base_slug = slug[:50] + counter = 1 + final_slug = slug + + while counter < 100: + existing = Site.query.filter(Site.slug == final_slug).first() + if not existing: + break + final_slug = f"{base_slug}-{counter}" + counter += 1 + + site.slug = final_slug + print(f" 生成slug: {final_slug}") + + print(f"\n4. 提交到数据库...") + print(f" 最终状态: code={site.code}, slug={site.slug}") + + # 提交事务 + db.session.commit() + + print("\n[SUCCESS] 网站创建成功!") + print(f" ID: {site.id}") + print(f" 名称: {site.name}") + print(f" Code: {site.code}") + print(f" Slug: {site.slug}") + + # 验证数据 + print("\n5. 验证数据完整性...") + assert site.code is not None, "code不能为空" + assert site.slug is not None, "slug不能为空" + assert len(site.code) == 8, "code必须是8位数字" + assert site.code.isdigit(), "code必须是纯数字" + print(" [OK] 所有验证通过") + + # 清理测试数据 + print("\n6. 清理测试数据...") + db.session.delete(site) + db.session.commit() + print(" [OK] 测试数据已清理") + + return True + + except Exception as e: + db.session.rollback() + print(f"\n[FAILED] 测试失败:{str(e)}") + import traceback + traceback.print_exc() + return False + +def test_unique_code_generation(): + """测试code唯一性生成""" + print("\n" + "=" * 60) + print("测试:验证code唯一性检查") + print("=" * 60) + + app = create_app() + + with app.app_context(): + try: + import random + + print("\n1. 生成测试code...") + test_code = str(random.randint(10000000, 99999999)) + print(f" 测试code: {test_code}") + + print("\n2. 创建第一个网站...") + site1 = Site( + code=test_code, + name='测试网站1', + url='https://test1.com', + slug='test-site-1', + is_active=True + ) + db.session.add(site1) + db.session.commit() + print(f" [OK] 网站1创建成功,code={site1.code}") + + print("\n3. 尝试在no_autoflush中查询该code...") + with db.session.no_autoflush: + existing = Site.query.filter(Site.code == test_code).first() + if existing: + print(f" [OK] 成功查询到已存在的code: {existing.code}") + else: + print(f" [ERROR] 未能查询到已存在的code") + return False + + print("\n4. 清理测试数据...") + db.session.delete(site1) + db.session.commit() + print(" [OK] 测试数据已清理") + + return True + + except Exception as e: + db.session.rollback() + print(f"\n[FAILED] 测试失败:{str(e)}") + import traceback + traceback.print_exc() + return False + +if __name__ == "__main__": + print("\n开始测试网站创建功能...\n") + + test1_passed = test_site_creation_with_autoflush() + test2_passed = test_unique_code_generation() + + print("\n" + "=" * 60) + print("测试结果汇总") + print("=" * 60) + print(f"测试1 - autoflush修复验证: {'[PASSED]' if test1_passed else '[FAILED]'}") + print(f"测试2 - code唯一性验证: {'[PASSED]' if test2_passed else '[FAILED]'}") + + if test1_passed and test2_passed: + print("\n[SUCCESS] 所有测试通过!on_model_change修复有效。") + sys.exit(0) + else: + print("\n[FAILED] 部分测试失败,需要进一步检查。") + sys.exit(1) diff --git a/utils/bookmark_parser.py b/utils/bookmark_parser.py new file mode 100644 index 0000000..0538752 --- /dev/null +++ b/utils/bookmark_parser.py @@ -0,0 +1,204 @@ +"""OneNav/Chrome书签HTML文件解析工具""" +from bs4 import BeautifulSoup +from typing import List, Dict +import re + + +class BookmarkParser: + """解析OneNav/Chrome导出的书签HTML文件""" + + def parse_html_file(self, html_content: str, debug=False) -> Dict[str, any]: + """ + 解析OneNav/Chrome书签HTML文件 + + Args: + html_content: HTML文件内容 + debug: 是否打印调试信息 + + Returns: + Dict: 包含 categories(标签列表) 和 sites(网站列表) + """ + soup = BeautifulSoup(html_content, 'html.parser') + categories = set() # 使用set去重 + sites = [] + + # 找到第一个DL标签作为起点 + first_dl = soup.find('dl') + if first_dl: + # 递归解析书签,收集分类和网站 + self._parse_dl_tag(first_dl, categories, sites, current_category=None, debug=debug) + + return { + 'categories': sorted(list(categories)), # 转为排序的列表 + 'sites': sites + } + + def parse_html_file_legacy(self, html_content: str) -> List[Dict[str, str]]: + """ + 解析Chrome书签HTML文件(旧版格式) + + Args: + html_content: HTML文件内容 + + Returns: + List[Dict]: 书签列表,每个书签包含 name, url, folder + """ + soup = BeautifulSoup(html_content, 'html.parser') + bookmarks = [] + + # 递归解析书签 + self._parse_dl_tag_legacy(soup, bookmarks, folder_path="") + + return bookmarks + + def _parse_dl_tag(self, element, categories: set, sites: List[Dict], current_category: str, debug=False): + """递归解析DL标签(OneNav格式)""" + # 查找所有DT标签(不限制为直接子元素,因为可能在p标签内) + dt_list = element.find_all('dt') + if debug and dt_list: + print(f"Found {len(dt_list)} DT tags total") + + for dt in dt_list: + # 检查是否是文件夹/分类 + h3 = dt.find('h3', recursive=False) + if h3: + category_name = h3.get_text(strip=True) + + # 跳过根节点和默认分类 + if category_name not in ['OneNav', 'OneNav默认分类']: + categories.add(category_name) + if debug: + print(f" Category: {category_name}") + + # 检查是否是书签链接(并且不在子分类的DL中) + a = dt.find('a', recursive=False) + if a and a.get('href'): + # 找到这个DT所属的最近的H3分类 + parent_category = None + # 向上查找同级或父级的H3 + prev = dt.find_previous('h3') + if prev: + parent_category = prev.get_text(strip=True) + # 跳过根节点和默认分类 + if parent_category in ['OneNav', 'OneNav默认分类']: + parent_category = None + + if parent_category: + url = a['href'] + title = a.get_text(strip=True) + + sites.append({ + 'title': title, + 'url': url, + 'category': parent_category, + 'add_date': a.get('add_date', '') + }) + if debug: + print(f" Site: {title} -> {parent_category}") + + def _parse_dl_tag_legacy(self, element, bookmarks: List[Dict], folder_path: str): + """递归解析DL标签(Chrome旧格式)""" + # 查找所有DT标签(书签项) + for dt in element.find_all('dt', recursive=False): + # 检查是否是文件夹 + h3 = dt.find('h3', recursive=False) + if h3: + folder_name = h3.get_text(strip=True) + new_folder_path = f"{folder_path}/{folder_name}" if folder_path else folder_name + + # 递归解析子文件夹 + dl = dt.find('dl', recursive=False) + if dl: + self._parse_dl_tag_legacy(dl, bookmarks, new_folder_path) + + # 检查是否是书签链接 + a = dt.find('a', recursive=False) + if a and a.get('href'): + url = a['href'] + name = a.get_text(strip=True) + + bookmarks.append({ + 'name': name, + 'url': url, + 'folder': folder_path + }) + + def parse_url_list(self, text: str) -> List[Dict[str, str]]: + """ + 解析纯文本URL列表 + + Args: + text: 文本内容,每行一个URL + + Returns: + List[Dict]: URL列表 + """ + urls = [] + lines = text.strip().split('\n') + + for line in lines: + line = line.strip() + if not line or line.startswith('#'): + continue + + # 简单的URL验证 + if re.match(r'^https?://', line): + urls.append({ + 'url': line, + 'name': '', # 名称留空,后续自动获取 + 'folder': '' + }) + + return urls + + @staticmethod + def clean_title(title: str) -> str: + """清理网站标题,提取网站名称""" + if not title: + return '' + + # 去除HTML实体 + title = re.sub(r'&', '&', title) + title = re.sub(r'<', '<', title) + title = re.sub(r'>', '>', title) + title = re.sub(r'"', '"', title) + + # 常见的分隔符 + separators = [' - ', ' | ', ' · ', '·', '|', ' — '] + for sep in separators: + if sep in title: + parts = title.split(sep) + # 过滤掉常见的无用部分 + filtered_parts = [] + skip_keywords = ['官网', '首页', 'official', 'home', 'page', 'website'] + + for part in parts: + part = part.strip() + if part and not any(kw in part.lower() for kw in skip_keywords): + filtered_parts.append(part) + + if filtered_parts: + # 返回最短的部分(通常是网站名) + return min(filtered_parts, key=len) + + # 去除一些常见的后缀词 + suffixes = [ + r'\s*官网\s*$', r'\s*首页\s*$', + r'\s*Official Site\s*$', r'\s*Home Page\s*$', + r'\s*Homepage\s*$', r'\s*Website\s*$' + ] + for suffix in suffixes: + title = re.sub(suffix, '', title, flags=re.IGNORECASE) + + return title.strip() + + @staticmethod + def extract_domain(url: str) -> str: + """从URL提取域名""" + match = re.search(r'https?://([^/]+)', url) + if match: + domain = match.group(1) + # 去除www前缀 + domain = re.sub(r'^www\.', '', domain) + return domain + return url diff --git a/utils/tag_generator.py b/utils/tag_generator.py new file mode 100644 index 0000000..cbb2fb0 --- /dev/null +++ b/utils/tag_generator.py @@ -0,0 +1,98 @@ +"""DeepSeek AI 标签生成工具""" +import os +from openai import OpenAI + + +class TagGenerator: + """使用DeepSeek生成标签""" + + def __init__(self): + self.api_key = os.environ.get('DEEPSEEK_API_KEY') + self.base_url = os.environ.get('DEEPSEEK_BASE_URL', 'https://api.deepseek.com') + self.client = None + + # 如果有API key,初始化客户端 + if self.api_key: + self.client = OpenAI( + api_key=self.api_key, + base_url=self.base_url + ) + + def generate_tags(self, name, description, existing_tags=None): + """ + 根据产品名称和描述生成标签 + + Args: + name: 产品名称 + description: 产品描述 + existing_tags: 现有标签列表(用于参考) + + Returns: + list: 生成的标签列表 + """ + # 检查是否配置了API key + if not self.client: + raise ValueError("DEEPSEEK_API_KEY未配置,请在.env文件中添加") + + try: + # 构建提示词 + existing_tags_str = "" + if existing_tags: + existing_tags_str = f"\n\n系统中已有的标签参考:\n{', '.join(existing_tags)}\n尽量使用已有标签,如果合适的话。" + + prompt = f"""你是一个AI工具导航网站的标签生成助手。根据以下产品信息,生成3-5个最合适的标签。 + +产品名称: {name} + +产品描述: {description} +{existing_tags_str} + +要求: +1. 标签应该准确描述产品的功能、类型或应用场景 +2. 每个标签2-4个汉字 +3. 标签要具体且有区分度 +4. 如果是AI工具,可以标注具体的AI类型(如"GPT"、"图像生成"等) +5. 只返回标签,用逗号分隔,不要其他说明 + +示例输出格式:写作助手,营销,GPT,内容生成 + +请生成标签:""" + + # 调用DeepSeek API + response = self.client.chat.completions.create( + model="deepseek-chat", + messages=[ + {"role": "system", "content": "你是一个专业的AI工具分类专家,擅长为各类AI产品生成准确的标签。"}, + {"role": "user", "content": prompt} + ], + temperature=0.7, + max_tokens=100 + ) + + # 解析返回的标签 + tags_text = response.choices[0].message.content.strip() + tags = [tag.strip() for tag in tags_text.split(',') if tag.strip()] + + # 限制标签数量为3-5个 + if len(tags) > 5: + tags = tags[:5] + + return tags + + except Exception as e: + print(f"DeepSeek标签生成失败: {str(e)}") + return [] + + def generate_news_summary(self, url, content): + """ + 生成新闻摘要(未来功能) + + Args: + url: 新闻链接 + content: 新闻内容 + + Returns: + str: 新闻摘要 + """ + # TODO: 实现新闻摘要生成 + pass diff --git a/zjpb_homepage.html b/zjpb_homepage.html new file mode 100644 index 0000000..2c55a62 --- /dev/null +++ b/zjpb_homepage.html @@ -0,0 +1,25 @@ + + + + + + + + + 0焦提示盒 - 0焦提示盒,不费吹灰之力获取AI平台最新信息 + + + + + + +

+ + + \ No newline at end of file