diff --git a/app.py b/app.py index 1a0ec9b..0dbf048 100644 --- a/app.py +++ b/app.py @@ -11,7 +11,7 @@ from flask_admin.contrib.sqla import ModelView from wtforms import fields from datetime import datetime, timedelta from config import config -from models import db, Site, Tag, Admin as AdminModel, News, site_tags, PromptTemplate, User, Folder, Collection, ApiKey +from models import db, Site, Tag, Admin as AdminModel, News, site_tags, PromptTemplate, User, Folder, Collection, ApiKey, Skill from utils.website_fetcher import WebsiteFetcher from utils.tag_generator import TagGenerator from utils.news_searcher import NewsSearcher @@ -173,6 +173,41 @@ def create_app(config_name='default'): pagination=pagination, tag_counts=tag_counts, current_tab=current_tab) + # ========== Skills 前台路由 ========== + @app.route('/skills') + def skills_list(): + """Skills 列表页""" + page = request.args.get('page', 1, type=int) + per_page = 20 + + # 获取推荐的和所有的 Skills + featured_skills = Skill.query.filter_by(is_active=True, is_featured=True).order_by(Skill.sort_order.desc()).all() + pagination = Skill.query.filter_by(is_active=True).order_by(Skill.sort_order.desc(), Skill.created_at.desc()).paginate( + page=page, per_page=per_page, error_out=False + ) + + return render_template('skills/list.html', + featured_skills=featured_skills, + skills=pagination.items, + pagination=pagination) + + @app.route('/skills/') + def skill_detail(slug): + """Skill 详情页""" + skill = Skill.query.filter_by(slug=slug, is_active=True).first_or_404() + + # 更新浏览次数 + try: + db.session.execute( + db.text("UPDATE skills SET view_count = view_count + 1 WHERE id = :id"), + {"id": skill.id} + ) + db.session.commit() + except: + db.session.rollback() + + return render_template('skills/detail.html', skill=skill) + @app.route('/site/') def site_detail(code): """网站详情页 - v3.1性能优化""" @@ -3380,6 +3415,71 @@ Sitemap: {}sitemap.xml db.orm.joinedload(News.site) ) + # Skills管理视图 + class SkillAdmin(SecureModelView): + can_edit = True + can_delete = True + can_create = True + can_view_details = False + can_set_page_size = True + page_size = 20 + + column_display_actions = True + + column_list = ['id', 'name', 'slug', 'short_desc', 'source_type', 'is_active', 'is_featured', 'view_count', 'created_at'] + column_default_sort = ('created_at', True) + column_searchable_list = ['name', 'slug', 'description'] + column_filters = ['is_active', 'is_featured', 'source_type'] + column_labels = { + 'id': 'ID', + 'name': 'Skill名称', + 'slug': 'URL别名', + 'short_desc': '简短描述', + 'description': '详细介绍', + 'logo': 'Logo', + 'github_url': 'GitHub链接', + 'source_type': '来源类型', + 'source_repo': '来源仓库', + 'usage': '使用方法', + 'examples': '使用示例', + 'is_active': '是否启用', + 'is_featured': '是否推荐', + 'view_count': '浏览次数', + 'sort_order': '排序权重', + 'created_at': '创建时间', + 'updated_at': '更新时间' + } + form_columns = ['name', 'slug', 'short_desc', 'description', 'logo', 'github_url', 'source_type', 'source_repo', 'usage', 'examples', 'is_active', 'is_featured', 'sort_order'] + + def on_model_change(self, form, model, is_created): + """保存前自动生成slug(如果为空)""" + import re + from pypinyin import lazy_pinyin + + # 如果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('-') + + if not slug: + slug = f"skill-{model.id}" + + # 确保slug唯一 + base_slug = slug[:50] + counter = 1 + final_slug = slug + + while counter < 100: + existing = Skill.query.filter(Skill.slug == final_slug).first() + if not existing or existing.id == model.id: + break + final_slug = f"{base_slug}-{counter}" + counter += 1 + + model.slug = final_slug + # Prompt模板管理视图 class PromptAdmin(SecureModelView): can_edit = True @@ -3436,6 +3536,7 @@ Sitemap: {}sitemap.xml admin.add_view(SiteAdmin(Site, db.session, name='网站管理')) admin.add_view(ApiKeyAdmin(ApiKey, db.session, name='API密钥', endpoint='api_keys')) + admin.add_view(SkillAdmin(Skill, db.session, name='Skills管理', endpoint='skills')) admin.add_view(TagAdmin(Tag, db.session, name='标签管理')) admin.add_view(NewsAdmin(News, db.session, name='新闻管理')) admin.add_view(PromptAdmin(PromptTemplate, db.session, name='Prompt管理')) diff --git a/migrations/add_skills_table.py b/migrations/add_skills_table.py new file mode 100644 index 0000000..ee83468 --- /dev/null +++ b/migrations/add_skills_table.py @@ -0,0 +1,26 @@ +""" +Skills 表迁移脚本 + +执行方式: python migrations/add_skills_table.py +""" +import os +import sys + +# 添加项目根目录到路径 +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from app import create_app, db +from models import Skill + + +def create_skills_table(): + """创建 skills 表""" + app = create_app('development') + + with app.app_context(): + db.create_all() + print('skills 表创建成功') + + +if __name__ == '__main__': + create_skills_table() \ No newline at end of file diff --git a/models.py b/models.py index 868a434..db95bc7 100644 --- a/models.py +++ b/models.py @@ -311,3 +311,51 @@ class Collection(db.Model): def __repr__(self): return f'' + +class Skill(db.Model): + """Skills 模型""" + __tablename__ = 'skills' + + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(100), nullable=False, comment='Skill名称') + slug = db.Column(db.String(100), unique=True, nullable=False, comment='URL别名') + description = db.Column(db.Text, comment='详细介绍') + short_desc = db.Column(db.String(200), comment='简短描述') + logo = db.Column(db.String(500), comment='Logo图片路径') + github_url = db.Column(db.String(500), comment='GitHub 链接') + source_type = db.Column(db.String(20), default='manual', comment='来源类型: manual/github') + source_repo = db.Column(db.String(200), comment='来源仓库') + usage = db.Column(db.Text, comment='使用方法') + examples = db.Column(db.Text, comment='使用示例') + is_active = db.Column(db.Boolean, default=True, comment='是否启用') + is_featured = db.Column(db.Boolean, default=False, comment='是否推荐') + view_count = db.Column(db.Integer, default=0, comment='浏览次数') + sort_order = db.Column(db.Integer, default=0, comment='排序权重') + created_at = db.Column(db.DateTime, default=datetime.now, comment='创建时间') + updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now, comment='更新时间') + + def __repr__(self): + return f'' + + def to_dict(self): + """转换为字典""" + return { + 'id': self.id, + 'name': self.name, + 'slug': self.slug, + 'description': self.description, + 'short_desc': self.short_desc, + 'logo': self.logo, + 'github_url': self.github_url, + 'source_type': self.source_type, + 'source_repo': self.source_repo, + 'usage': self.usage, + 'examples': self.examples, + 'is_active': self.is_active, + 'is_featured': self.is_featured, + 'view_count': self.view_count, + 'sort_order': self.sort_order, + 'created_at': self.created_at.strftime('%Y-%m-%d %H:%M:%S') if self.created_at else None, + 'updated_at': self.updated_at.strftime('%Y-%m-%d %H:%M:%S') if self.updated_at else None + } + diff --git a/templates/admin/components/sidebar.html b/templates/admin/components/sidebar.html index 3877f40..de545c8 100644 --- a/templates/admin/components/sidebar.html +++ b/templates/admin/components/sidebar.html @@ -47,6 +47,12 @@ 管理员 +