5eecbecd7f
- Portal: wrap .panel-head in matching card (border, radius, shadow) with cyan->purple left accent bar; h2 28px -> 20px; description right-aligned with vertical divider; top edge now aligns with side-menu & links-panel - Login: switch from absolute-positioned icons to el-input prefix slot; layout brand row (logo + title + uppercase subtitle); add form-title section with gradient bar; consistent cyan focus; gradient button; dashed top divider for back-link; card width 420 -> 460 - Add .gitignore (node_modules, dist, __pycache__, .env, etc.)
154 lines
6.3 KiB
Python
154 lines
6.3 KiB
Python
"""SQLAlchemy models for OPS Resource Portal."""
|
|
from datetime import datetime, timezone
|
|
|
|
from extensions import db
|
|
|
|
|
|
def utcnow():
|
|
return datetime.now(timezone.utc)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Association table: resource <-> option (faceted tagging)
|
|
# ---------------------------------------------------------------------------
|
|
resource_options = db.Table(
|
|
"resource_options",
|
|
db.Column("resource_id", db.Integer, db.ForeignKey("resources.id", ondelete="CASCADE"), primary_key=True),
|
|
db.Column("option_id", db.Integer, db.ForeignKey("options.id", ondelete="CASCADE"), primary_key=True),
|
|
)
|
|
|
|
|
|
class User(db.Model):
|
|
__tablename__ = "users"
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
username = db.Column(db.String(80), unique=True, nullable=False)
|
|
password = db.Column(db.String(128), nullable=False) # bcrypt hash
|
|
created_at = db.Column(db.DateTime, default=utcnow)
|
|
|
|
def set_password(self, raw):
|
|
import bcrypt
|
|
self.password = bcrypt.hashpw(raw.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")
|
|
|
|
def check_password(self, raw):
|
|
import bcrypt
|
|
try:
|
|
return bcrypt.checkpw(raw.encode("utf-8"), self.password.encode("utf-8"))
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
class SiteSetting(db.Model):
|
|
"""Key-value site configuration editable from the admin panel."""
|
|
__tablename__ = "site_settings"
|
|
key = db.Column(db.String(64), primary_key=True)
|
|
value = db.Column(db.Text, default="")
|
|
updated_at = db.Column(db.DateTime, default=utcnow, onupdate=utcnow)
|
|
|
|
|
|
class Category(db.Model):
|
|
"""Left-nav menu entry, e.g. 基础设备 / pypi源."""
|
|
__tablename__ = "categories"
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
name = db.Column(db.String(80), nullable=False)
|
|
icon = db.Column(db.String(16), default="📁") # emoji icon
|
|
description = db.Column(db.String(255), default="")
|
|
sort_order = db.Column(db.Integer, default=0)
|
|
is_active = db.Column(db.Boolean, default=True)
|
|
created_at = db.Column(db.DateTime, default=utcnow)
|
|
|
|
rows = db.relationship("OptionRow", backref="category", lazy=True,
|
|
cascade="all, delete-orphan", order_by="OptionRow.sort_order")
|
|
resources = db.relationship("Resource", backref="category", lazy=True,
|
|
cascade="all, delete-orphan")
|
|
|
|
def to_dict(self, with_rows=True):
|
|
d = {
|
|
"id": self.id, "name": self.name, "icon": self.icon,
|
|
"description": self.description, "sort_order": self.sort_order,
|
|
"is_active": self.is_active,
|
|
"resource_count": len(self.resources),
|
|
}
|
|
if with_rows:
|
|
d["rows"] = [r.to_dict() for r in self.rows]
|
|
return d
|
|
|
|
|
|
class OptionRow(db.Model):
|
|
"""A selection row inside a category, e.g. 运行环境 / 操作系统."""
|
|
__tablename__ = "option_rows"
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
category_id = db.Column(db.Integer, db.ForeignKey("categories.id", ondelete="CASCADE"), nullable=False)
|
|
title = db.Column(db.String(80), nullable=False) # e.g. 运行环境
|
|
sort_order = db.Column(db.Integer, default=0)
|
|
|
|
options = db.relationship("Option", backref="row", lazy=True,
|
|
cascade="all, delete-orphan", order_by="Option.sort_order")
|
|
|
|
def to_dict(self):
|
|
return {
|
|
"id": self.id, "title": self.title, "sort_order": self.sort_order,
|
|
"options": [o.to_dict() for o in self.options],
|
|
}
|
|
|
|
|
|
class Option(db.Model):
|
|
"""A selectable tile inside a row, e.g. 物理机 / ubuntu."""
|
|
__tablename__ = "options"
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
row_id = db.Column(db.Integer, db.ForeignKey("option_rows.id", ondelete="CASCADE"), nullable=False)
|
|
label = db.Column(db.String(80), nullable=False)
|
|
icon = db.Column(db.String(16), default="")
|
|
sort_order = db.Column(db.Integer, default=0)
|
|
|
|
def to_dict(self):
|
|
return {"id": self.id, "label": self.label, "icon": self.icon, "sort_order": self.sort_order}
|
|
|
|
|
|
class Resource(db.Model):
|
|
"""A published internal resource: command / link / text / file."""
|
|
__tablename__ = "resources"
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
category_id = db.Column(db.Integer, db.ForeignKey("categories.id", ondelete="CASCADE"), nullable=False)
|
|
title = db.Column(db.String(160), nullable=False)
|
|
type = db.Column(db.String(16), nullable=False, default="command") # command|link|text|file
|
|
content = db.Column(db.Text, default="") # command text / rich text / note
|
|
url = db.Column(db.String(500), default="") # link target / file URL
|
|
description = db.Column(db.String(255), default="")
|
|
sort_order = db.Column(db.Integer, default=0)
|
|
is_active = db.Column(db.Boolean, default=True)
|
|
created_at = db.Column(db.DateTime, default=utcnow)
|
|
updated_at = db.Column(db.DateTime, default=utcnow, onupdate=utcnow)
|
|
|
|
options = db.relationship("Option", secondary=resource_options, lazy="subquery",
|
|
backref=db.backref("resources", lazy=True))
|
|
|
|
def to_dict(self):
|
|
return {
|
|
"id": self.id, "category_id": self.category_id, "title": self.title,
|
|
"type": self.type, "content": self.content, "url": self.url,
|
|
"description": self.description, "sort_order": self.sort_order,
|
|
"is_active": self.is_active,
|
|
"option_ids": sorted(o.id for o in self.options),
|
|
"created_at": self.created_at.strftime("%Y-%m-%d %H:%M") if self.created_at else "",
|
|
"updated_at": self.updated_at.strftime("%Y-%m-%d %H:%M") if self.updated_at else "",
|
|
}
|
|
|
|
|
|
class SideLink(db.Model):
|
|
"""Right-panel 其他链接 entry."""
|
|
__tablename__ = "side_links"
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
title = db.Column(db.String(160), nullable=False)
|
|
url = db.Column(db.String(500), nullable=False)
|
|
group_name = db.Column(db.String(80), default="其他链接")
|
|
sort_order = db.Column(db.Integer, default=0)
|
|
is_active = db.Column(db.Boolean, default=True)
|
|
created_at = db.Column(db.DateTime, default=utcnow)
|
|
|
|
def to_dict(self):
|
|
return {
|
|
"id": self.id, "title": self.title, "url": self.url,
|
|
"group_name": self.group_name, "sort_order": self.sort_order,
|
|
"is_active": self.is_active,
|
|
}
|