Files
Hermes 04ddf2762d feat(portal): replace hardcoded '24/7 内网' stat with live resource count
Backend: bootstrap() now returns stats.total_resources (count of
is_active=True resources).
Frontend: stats adds 'resources' computed from bootstrapStats;
third hero-stat shows '{{ stats.resources }} 资源数量' instead of
the static '24/7 内网' placeholder.
2026-07-22 11:21:23 +08:00

98 lines
3.2 KiB
Python

"""Public portal API — no auth required."""
from flask import Blueprint, jsonify, request
from models import Category, Option, Resource, SideLink, SiteSetting
bp = Blueprint("portal", __name__, url_prefix="/api/portal")
@bp.route("/bootstrap")
def bootstrap():
"""Everything the portal page needs in one request."""
settings = {s.key: s.value for s in SiteSetting.query.all()}
categories = (
Category.query.filter_by(is_active=True)
.order_by(Category.sort_order, Category.id)
.all()
)
links = (
SideLink.query.filter_by(is_active=True)
.order_by(SideLink.sort_order, SideLink.id)
.all()
)
return jsonify({
"settings": settings,
"stats": {
"total_resources": Resource.query.filter_by(is_active=True).count(),
},
"categories": [c.to_dict() for c in categories],
"links": [l.to_dict() for l in links],
})
@bp.route("/categories/<int:cid>/resources")
def category_resources(cid):
"""Resources for a category, faceted by selected option ids."""
category = Category.query.get_or_404(cid)
selected = []
raw = request.args.get("option_ids", "")
keyword = request.args.get("keyword", "").strip()
if raw.strip():
try:
selected = [int(x) for x in raw.split(",") if x.strip()]
except ValueError:
selected = []
query = Resource.query.filter_by(category_id=cid, is_active=True)
if keyword:
query = query.filter(
Resource.title.like(f"%{keyword}%") |
Resource.description.like(f"%{keyword}%") |
Resource.content.like(f"%{keyword}%")
)
resources = query.order_by(Resource.sort_order, Resource.id).all()
if selected:
chosen = Option.query.filter(Option.id.in_(selected)).all()
by_row = {}
for o in chosen:
by_row.setdefault(o.row_id, []).append(o.id)
def matches(res):
tagged_ids = {o.id for o in res.options}
tagged_row_ids = {o.row_id for o in res.options}
for row_id, opt_ids in by_row.items():
if row_id not in tagged_row_ids:
continue # untagged in this row -> applies to all
if not tagged_ids.intersection(opt_ids):
return False
return True
resources = [r for r in resources if matches(r)]
return jsonify({
"category": category.to_dict(),
"resources": [r.to_dict() for r in resources],
"selected_option_ids": selected,
})
@bp.route("/search")
def global_search():
"""Global search across all resources."""
keyword = request.args.get("keyword", "").strip()
if not keyword or len(keyword) < 2:
return jsonify({"results": [], "keyword": keyword})
results = Resource.query.filter_by(is_active=True).filter(
Resource.title.like(f"%{keyword}%") |
Resource.description.like(f"%{keyword}%") |
Resource.content.like(f"%{keyword}%")
).order_by(Resource.category_id, Resource.sort_order).limit(50).all()
return jsonify({
"keyword": keyword,
"count": len(results),
"results": [r.to_dict() for r in results]
})