from markupsafe import Markup from sqladmin import ModelView, action from sqladmin.fields import SelectField from sqlalchemy import update from sqlalchemy.orm import Session from starlette.requests import Request from starlette.responses import RedirectResponse from app.admin.widgets import ImagePickerField from app.db.session import sync_engine from app.models.audit_log import AuditLog from app.models.comment import Comment from app.models.correction import Correction from app.models.notification import Notification from app.models.shooting import ShootingRequest, ShootingApplication from app.models.event import Event, EventRegistration, EventPhoto from app.models.promotion import Promotion from app.models.membership import MembershipPlan, UserMembership from app.models.app_nav_config import AppNavConfig from app.models.favorite import Favorite from app.models.point_ledger import PointLedger from app.models.rating import Rating from app.models.report import Report from app.models.spot import Spot, SpotImage from app.models.tag import Tag from app.models.user import User def _sync_log_admin(operator_id: int, action_name: str, target_type: str, target_id=None, detail=None): import json detail_str = json.dumps(detail, ensure_ascii=False) if isinstance(detail, dict) else (str(detail) if detail else None) with Session(sync_engine) as session: entry = AuditLog( operator_id=operator_id, action=action_name, target_type=target_type, target_id=target_id, detail=detail_str, ) session.add(entry) session.commit() def _bulk_update_status(model, pks_str: str, field: str, value: str): if not pks_str: return pks = [int(pk) for pk in pks_str.split(",") if pk.strip()] if not pks: return with Session(sync_engine) as session: session.execute( update(model).where(model.id.in_(pks)).values(**{field: value}) ) session.commit() AUDIT_STATUS_CHOICES = [ ("pending", "待审核"), ("approved", "已通过"), ("rejected", "已驳回"), ("deleted", "已删除"), ] ROLE_CHOICES = [ ("user", "普通用户"), ("moderator", "审核员"), ("admin", "管理员"), ] IDENTITY_CHOICES = [ ("photographer", "摄影师"), ("cosplayer", "Coser"), ("both", "都是"), ] REPORT_STATUS_CHOICES = [ ("pending", "待处理"), ("processing", "处理中"), ("resolved", "已处理"), ("dismissed", "已驳回"), ] REPORT_TARGET_CHOICES = [ ("comment", "评论"), ("spot", "地点"), ("user", "用户"), ] CORRECTION_STATUS_CHOICES = [ ("pending", "待处理"), ("accepted", "已采纳"), ("rejected", "已驳回"), ] CORRECTION_FIELD_CHOICES = [ ("title", "地点名称"), ("city", "所在城市"), ("description", "地点介绍"), ("transport", "交通方式"), ("best_time", "最佳拍摄时间"), ("difficulty", "路径难度"), ("price", "收费信息"), ] def _select_overrides(*field_names): return {field_name: SelectField for field_name in field_names} def _select_args(**field_choices): return { field_name: { "choices": choices, } for field_name, choices in field_choices.items() } STATUS_BADGE_MAP = { "pending": ("待审核", "#f59e0b", "rgba(245,158,11,0.12)"), "approved": ("已通过", "#22c55e", "rgba(34,197,94,0.12)"), "rejected": ("已驳回", "#ef4444", "rgba(239,68,68,0.12)"), "deleted": ("已删除", "#94a3b8", "rgba(148,163,184,0.12)"), "processing": ("处理中", "#3b82f6", "rgba(59,130,246,0.12)"), "resolved": ("已处理", "#22c55e", "rgba(34,197,94,0.12)"), "dismissed": ("已驳回", "#94a3b8", "rgba(148,163,184,0.12)"), "accepted": ("已采纳", "#22c55e", "rgba(34,197,94,0.12)"), } ROLE_BADGE_MAP = { "user": ("普通用户", "#64748b", "rgba(100,116,139,0.12)"), "moderator": ("审核员", "#3b82f6", "rgba(59,130,246,0.12)"), "admin": ("管理员", "#ef4444", "rgba(239,68,68,0.12)"), } FIELD_NAME_MAP = dict(CORRECTION_FIELD_CHOICES) TARGET_TYPE_MAP = { "comment": "评论", "spot": "地点", "user": "用户", } def _badge(text, color, bg): return Markup( f'' f'{text}' ) def _status_badge(model, name): val = getattr(model, name, None) or "" info = STATUS_BADGE_MAP.get(val) if info: return _badge(info[0], info[1], info[2]) return val def _role_badge(model, name): val = getattr(model, name, None) or "" info = ROLE_BADGE_MAP.get(val) if info: return _badge(info[0], info[1], info[2]) return val def _bool_badge(model, name): val = getattr(model, name, None) if val: return _badge("是", "#22c55e", "rgba(34,197,94,0.12)") return _badge("否", "#94a3b8", "rgba(148,163,184,0.12)") def _field_name_label(model, name): val = getattr(model, name, None) or "" return FIELD_NAME_MAP.get(val, val) def _target_type_label(model, name): val = getattr(model, name, None) or "" return TARGET_TYPE_MAP.get(val, val) def _img_preview(model, name): url = getattr(model, name, None) or getattr(model, "image_url", None) if not url: return Markup('无头像') return Markup( f'' ) def _spot_images_preview(model, name): if not hasattr(model, "images") or not model.images: return Markup('无图片') html = '
' for img in model.images[:5]: html += ( f'' ) if len(model.images) > 5: html += ( f'' f'+{len(model.images) - 5}' ) html += "
" return Markup(html) def _text_preview(model, name, max_len=40): val = getattr(model, name, None) or "" if len(val) > max_len: return Markup( f'' f'{val[:max_len]}…' ) return val def _score_stars(model, name): score = getattr(model, name, None) if score is None: return "" full = int(score) html = '' for _ in range(full): html += "★" for _ in range(5 - full): html += '' html += f' {score}' return Markup(html) def _point_change(model, name): val = getattr(model, name, None) or 0 if val > 0: return _badge(f"+{val}", "#22c55e", "rgba(34,197,94,0.12)") elif val < 0: return _badge(str(val), "#ef4444", "rgba(239,68,68,0.12)") return _badge("0", "#94a3b8", "rgba(148,163,184,0.12)") class AuditedModelView(ModelView): async def after_model_change(self, data, model, is_created, request: Request): uid = request.session.get("admin_user_id") if uid: action_name = "admin.create" if is_created else "admin.edit" target_type = model.__tablename__ if hasattr(model, "__tablename__") else type(model).__name__ target_id = getattr(model, "id", None) _sync_log_admin(uid, action_name, target_type, target_id) async def after_model_delete(self, model, request: Request): uid = request.session.get("admin_user_id") if uid: target_type = model.__tablename__ if hasattr(model, "__tablename__") else type(model).__name__ target_id = getattr(model, "id", None) _sync_log_admin(uid, "admin.delete", target_type, target_id) class UserAdmin(AuditedModelView, model=User): column_list = [User.id, "avatar_preview", User.nickname, User.phone, User.email, User.city, User.role, User.is_active, User.created_at] column_details_list = [ User.id, User.nickname, User.phone, User.email, User.avatar_url, User.city, User.bio, User.identity, User.role, User.is_active, User.created_at, User.updated_at, ] column_searchable_list = [User.nickname, User.phone, User.email] column_sortable_list = [User.id, User.created_at, User.role] column_default_sort = ("id", True) can_create = False can_delete = False form_columns = [User.nickname, User.avatar_url, User.city, User.bio, User.identity, User.role, User.is_active] name = "用户" name_plural = "用户管理" icon = "fa-solid fa-user" column_labels = { User.id: "ID", User.nickname: "昵称", User.phone: "手机号", User.email: "邮箱", User.city: "城市", User.bio: "个人简介", User.role: "角色", User.is_active: "是否激活", User.created_at: "注册时间", User.updated_at: "更新时间", User.identity: "身份", User.avatar_url: "头像地址", "avatar_preview": "头像", } form_overrides = { **_select_overrides("role", "identity"), "avatar_url": ImagePickerField, } form_args = _select_args(role=ROLE_CHOICES, identity=IDENTITY_CHOICES) column_formatters = { "avatar_preview": lambda m, n: _img_preview(m, "avatar_url"), User.role: _role_badge, User.is_active: _bool_badge, } class SpotAdmin(AuditedModelView, model=Spot): column_list = [Spot.id, Spot.title, Spot.city, Spot.audit_status, "images_preview", "creator", Spot.created_at] column_searchable_list = [Spot.title, Spot.city] column_sortable_list = [Spot.id, Spot.created_at, Spot.audit_status] column_default_sort = [("audit_status", False), ("created_at", False)] column_details_list = [ Spot.id, Spot.title, Spot.city, Spot.description, Spot.transport, Spot.best_time, Spot.difficulty, Spot.is_free, Spot.price_min, Spot.price_max, Spot.audit_status, Spot.reject_reason, Spot.avg_rating, Spot.rating_count, "creator", Spot.created_at, Spot.updated_at, ] form_include_pk = False form_columns = ["title", "city", "description", "transport", "best_time", "difficulty", "is_free", "price_min", "price_max", "audit_status", "reject_reason", "creator"] can_delete = False name = "地点" name_plural = "地点管理" icon = "fa-solid fa-map-pin" column_labels = { Spot.id: "ID", Spot.title: "名称", Spot.city: "城市", Spot.audit_status: "审核状态", Spot.reject_reason: "驳回原因", "creator": "投稿者", Spot.created_at: "创建时间", Spot.updated_at: "更新时间", Spot.description: "介绍", Spot.transport: "交通方式", Spot.best_time: "最佳拍摄时间", Spot.difficulty: "难度说明", Spot.is_free: "是否免费", Spot.price_min: "最低价格(元)", Spot.price_max: "最高价格(元)", Spot.avg_rating: "平均评分", Spot.rating_count: "评分数", "images_preview": "图片", } form_overrides = _select_overrides("audit_status") form_args = _select_args(audit_status=AUDIT_STATUS_CHOICES) form_ajax_refs = { "creator": {"fields": ("nickname", "phone", "email"), "order_by": "nickname"}, } column_formatters = { "images_preview": _spot_images_preview, Spot.audit_status: _status_badge, } @action(name="approve_spots", label="批量通过", confirmation_message="确认将选中地点设为「已通过」?") async def action_approve(self, request: Request): _bulk_update_status(Spot, request.query_params.get("pks", ""), "audit_status", "approved") return RedirectResponse(request.url_for("admin:list", identity=self.identity), status_code=302) @action(name="reject_spots", label="批量驳回", confirmation_message="确认将选中地点设为「已驳回」?") async def action_reject(self, request: Request): _bulk_update_status(Spot, request.query_params.get("pks", ""), "audit_status", "rejected") return RedirectResponse(request.url_for("admin:list", identity=self.identity), status_code=302) class SpotImageAdmin(AuditedModelView, model=SpotImage): column_list = [SpotImage.id, "spot", "image_preview", SpotImage.is_cover, SpotImage.audit_status, SpotImage.created_at] column_sortable_list = [SpotImage.id, SpotImage.spot_id, SpotImage.audit_status] column_default_sort = ("id", True) form_columns = ["spot", "image_url", "is_cover", "audit_status", "sort_order"] name = "地点图片" name_plural = "图片管理" icon = "fa-solid fa-image" column_labels = { SpotImage.id: "ID", "spot": "所属地点", SpotImage.image_url: "图片地址", "image_preview": "图片预览", SpotImage.is_cover: "封面", SpotImage.audit_status: "审核状态", SpotImage.created_at: "上传时间", SpotImage.sort_order: "排序", } form_overrides = { **_select_overrides("audit_status"), "image_url": ImagePickerField, } form_args = _select_args(audit_status=AUDIT_STATUS_CHOICES) form_ajax_refs = { "spot": {"fields": ("title", "city"), "order_by": "title"}, } column_formatters = { "image_preview": lambda m, n: Markup( f'' ) if m.image_url else Markup('无图片'), SpotImage.audit_status: _status_badge, SpotImage.is_cover: _bool_badge, } @action(name="approve_images", label="批量通过", confirmation_message="确认将选中图片设为「已通过」?") async def action_approve(self, request: Request): _bulk_update_status(SpotImage, request.query_params.get("pks", ""), "audit_status", "approved") return RedirectResponse(request.url_for("admin:list", identity=self.identity), status_code=302) @action(name="reject_images", label="批量驳回", confirmation_message="确认将选中图片设为「已驳回」?") async def action_reject(self, request: Request): _bulk_update_status(SpotImage, request.query_params.get("pks", ""), "audit_status", "rejected") return RedirectResponse(request.url_for("admin:list", identity=self.identity), status_code=302) class FavoriteAdmin(ModelView, model=Favorite): column_list = [Favorite.id, "user", "spot", Favorite.created_at] column_sortable_list = [Favorite.id, Favorite.created_at] column_default_sort = ("id", True) can_create = False can_edit = False can_delete = False name = "收藏" name_plural = "收藏记录" icon = "fa-solid fa-heart" column_labels = { Favorite.id: "ID", "user": "用户", "spot": "地点", Favorite.created_at: "收藏时间", } class PointLedgerAdmin(ModelView, model=PointLedger): column_list = [ PointLedger.id, "user", PointLedger.change, PointLedger.balance, PointLedger.reason, PointLedger.ref_type, PointLedger.created_at, ] column_sortable_list = [PointLedger.id, PointLedger.created_at] column_default_sort = ("id", True) can_create = False can_edit = False can_delete = False name = "积分记录" name_plural = "积分流水" icon = "fa-solid fa-coins" column_labels = { PointLedger.id: "ID", "user": "用户", PointLedger.change: "变动", PointLedger.balance: "余额", PointLedger.reason: "原因", PointLedger.ref_type: "关联类型", PointLedger.created_at: "时间", } column_formatters = { PointLedger.change: _point_change, } class CommentAdmin(AuditedModelView, model=Comment): column_list = [Comment.id, "spot_id", "user", Comment.content, Comment.audit_status, Comment.created_at] column_searchable_list = [Comment.content] column_sortable_list = [Comment.id, Comment.created_at, Comment.audit_status] column_default_sort = ("id", True) form_columns = [Comment.audit_status] can_create = False can_delete = True name = "评论" name_plural = "评论管理" icon = "fa-solid fa-comment" column_labels = { Comment.id: "ID", "spot_id": "地点ID", "user": "用户", Comment.content: "内容", Comment.audit_status: "审核状态", Comment.created_at: "评论时间", } form_overrides = _select_overrides("audit_status") form_args = _select_args(audit_status=AUDIT_STATUS_CHOICES) column_formatters = { Comment.audit_status: _status_badge, Comment.content: lambda m, n: _text_preview(m, "content", 50), } @action(name="approve_comments", label="批量通过", confirmation_message="确认将选中评论设为「已通过」?") async def action_approve(self, request: Request): _bulk_update_status(Comment, request.query_params.get("pks", ""), "audit_status", "approved") return RedirectResponse(request.url_for("admin:list", identity=self.identity), status_code=302) @action(name="reject_comments", label="批量驳回", confirmation_message="确认将选中评论设为「已驳回」?") async def action_reject(self, request: Request): _bulk_update_status(Comment, request.query_params.get("pks", ""), "audit_status", "rejected") return RedirectResponse(request.url_for("admin:list", identity=self.identity), status_code=302) class ReportAdmin(AuditedModelView, model=Report): column_list = [ Report.id, "reporter", Report.target_type, Report.target_id, Report.reason, Report.status, "handler", Report.created_at, ] column_searchable_list = [Report.target_type, Report.reason] column_sortable_list = [Report.id, Report.created_at, Report.status] column_default_sort = [("status", False), ("created_at", False)] form_columns = ["status", "target_type", "handler", "conclusion", "resolved_at"] can_create = False can_delete = False name = "举报" name_plural = "举报管理" icon = "fa-solid fa-flag" column_labels = { Report.id: "ID", "reporter": "举报人", Report.target_type: "目标类型", Report.target_id: "目标ID", Report.reason: "原因", Report.status: "状态", "handler": "处理人", Report.created_at: "举报时间", Report.conclusion: "处理结论", Report.resolved_at: "处理时间", } form_overrides = _select_overrides("status", "target_type") form_args = _select_args( status=REPORT_STATUS_CHOICES, target_type=REPORT_TARGET_CHOICES, ) form_ajax_refs = { "handler": {"fields": ("nickname", "phone", "email"), "order_by": "nickname"}, } column_formatters = { Report.status: _status_badge, Report.target_type: _target_type_label, Report.reason: lambda m, n: _text_preview(m, "reason", 40), } @action(name="resolve_reports", label="批量标记已处理", confirmation_message="确认将选中举报标记为「已处理」?") async def action_resolve(self, request: Request): _bulk_update_status(Report, request.query_params.get("pks", ""), "status", "resolved") return RedirectResponse(request.url_for("admin:list", identity=self.identity), status_code=302) @action(name="dismiss_reports", label="批量驳回", confirmation_message="确认将选中举报标记为「已驳回」?") async def action_dismiss(self, request: Request): _bulk_update_status(Report, request.query_params.get("pks", ""), "status", "dismissed") return RedirectResponse(request.url_for("admin:list", identity=self.identity), status_code=302) class RatingAdmin(ModelView, model=Rating): column_list = [Rating.id, "spot_id", "user_id", Rating.score, Rating.short_comment, Rating.created_at] column_sortable_list = [Rating.id, Rating.created_at, Rating.score] column_default_sort = ("id", True) can_create = False can_edit = False can_delete = False name = "评分" name_plural = "评分记录" icon = "fa-solid fa-star" column_labels = { Rating.id: "ID", Rating.spot_id: "地点ID", Rating.user_id: "用户ID", Rating.score: "评分", Rating.short_comment: "短评", Rating.created_at: "评分时间", } column_formatters = { Rating.score: _score_stars, Rating.short_comment: lambda m, n: _text_preview(m, "short_comment", 40), } class TagAdmin(AuditedModelView, model=Tag): column_list = [Tag.id, Tag.name, Tag.category, Tag.usage_count, Tag.is_active, Tag.created_at] column_searchable_list = [Tag.name, Tag.category] column_sortable_list = [Tag.id, Tag.usage_count, Tag.name] column_default_sort = ("usage_count", True) form_columns = [Tag.name, Tag.category, Tag.is_active] name = "标签" name_plural = "标签管理" icon = "fa-solid fa-tag" column_labels = { Tag.id: "ID", Tag.name: "名称", Tag.category: "分类", Tag.usage_count: "使用次数", Tag.is_active: "是否启用", Tag.created_at: "创建时间", } class AuditLogAdmin(ModelView, model=AuditLog): column_list = [ AuditLog.id, "operator", AuditLog.action, AuditLog.target_type, AuditLog.target_id, AuditLog.detail, AuditLog.created_at, ] column_searchable_list = [AuditLog.action, AuditLog.target_type] column_sortable_list = [AuditLog.id, AuditLog.created_at, AuditLog.action] column_default_sort = ("id", True) can_create = False can_edit = False can_delete = False name = "操作日志" name_plural = "操作日志" icon = "fa-solid fa-clipboard-list" column_labels = { AuditLog.id: "ID", "operator": "操作人", AuditLog.action: "操作", AuditLog.target_type: "目标类型", AuditLog.target_id: "目标ID", AuditLog.detail: "详情", AuditLog.created_at: "操作时间", } class CorrectionAdmin(AuditedModelView, model=Correction): column_list = [ Correction.id, "spot", "user", Correction.field_name, Correction.suggested_value, Correction.reason, Correction.status, Correction.created_at, ] column_searchable_list = [Correction.field_name, Correction.suggested_value] column_sortable_list = [Correction.id, Correction.created_at, Correction.status] column_default_sort = [("status", False), ("created_at", False)] form_columns = ["status"] can_create = False can_delete = True name = "校正建议" name_plural = "校正建议" icon = "fa-solid fa-pen-to-square" column_labels = { Correction.id: "ID", "spot": "地点", "user": "建议人", Correction.field_name: "校正字段", Correction.suggested_value: "建议值", Correction.reason: "理由", Correction.status: "状态", Correction.created_at: "提交时间", } form_overrides = _select_overrides("status", "field_name") form_args = _select_args( status=CORRECTION_STATUS_CHOICES, field_name=CORRECTION_FIELD_CHOICES, ) column_formatters = { Correction.status: _status_badge, Correction.field_name: _field_name_label, Correction.suggested_value: lambda m, n: _text_preview(m, "suggested_value", 40), Correction.reason: lambda m, n: _text_preview(m, "reason", 30), } @action(name="accept_corrections", label="批量采纳", confirmation_message="确认将选中校正建议标记为「已采纳」?") async def action_accept(self, request: Request): _bulk_update_status(Correction, request.query_params.get("pks", ""), "status", "accepted") return RedirectResponse(request.url_for("admin:list", identity=self.identity), status_code=302) @action(name="reject_corrections", label="批量驳回", confirmation_message="确认将选中校正建议标记为「已驳回」?") async def action_reject(self, request: Request): _bulk_update_status(Correction, request.query_params.get("pks", ""), "status", "rejected") return RedirectResponse(request.url_for("admin:list", identity=self.identity), status_code=302) class NotificationAdmin(ModelView, model=Notification): column_list = [Notification.id, "user", Notification.type, Notification.title, Notification.is_read, Notification.created_at] column_searchable_list = [Notification.title, Notification.type] column_sortable_list = [Notification.id, Notification.created_at] column_default_sort = ("id", True) can_create = False can_edit = False can_delete = True name = "通知" name_plural = "通知管理" icon = "fa-solid fa-bell" column_labels = { Notification.id: "ID", "user": "用户", Notification.type: "类型", Notification.title: "标题", Notification.is_read: "已读", Notification.created_at: "创建时间", } column_formatters = { Notification.is_read: _bool_badge, } SHOOTING_STATUS_CHOICES = [ ("open", "开放"), ("matched", "已匹配"), ("closed", "已关闭"), ] ROLE_NEEDED_CHOICES = [ ("photographer", "摄影师"), ("cosplayer", "Coser"), ("both", "不限"), ] APP_STATUS_CHOICES = [ ("pending", "待处理"), ("accepted", "已接受"), ("rejected", "已拒绝"), ] class ShootingRequestAdmin(AuditedModelView, model=ShootingRequest): column_list = [ShootingRequest.id, ShootingRequest.title, ShootingRequest.city, "creator", ShootingRequest.status, ShootingRequest.audit_status, ShootingRequest.created_at] column_searchable_list = [ShootingRequest.title, ShootingRequest.city] column_sortable_list = [ShootingRequest.id, ShootingRequest.created_at, ShootingRequest.status] column_default_sort = ("id", True) form_columns = ["title", "city", "description", "style", "status", "audit_status", "reject_reason", "role_needed"] can_delete = False name = "约拍单" name_plural = "约拍管理" icon = "fa-solid fa-camera" column_labels = { ShootingRequest.id: "ID", ShootingRequest.title: "标题", ShootingRequest.city: "城市", "creator": "发布者", ShootingRequest.status: "状态", ShootingRequest.audit_status: "审核状态", ShootingRequest.created_at: "创建时间", ShootingRequest.description: "描述", ShootingRequest.style: "风格", ShootingRequest.reject_reason: "驳回原因", ShootingRequest.role_needed: "需要角色", } form_overrides = _select_overrides("status", "audit_status", "role_needed") form_args = _select_args( status=SHOOTING_STATUS_CHOICES, audit_status=AUDIT_STATUS_CHOICES, role_needed=ROLE_NEEDED_CHOICES, ) form_ajax_refs = { "creator": {"fields": ("nickname", "phone", "email"), "order_by": "nickname"}, } column_formatters = { ShootingRequest.status: _status_badge, ShootingRequest.audit_status: _status_badge, } @action(name="approve_shooting", label="批量通过", confirmation_message="确认将选中约拍设为「已通过」?") async def action_approve(self, request: Request): _bulk_update_status(ShootingRequest, request.query_params.get("pks", ""), "audit_status", "approved") return RedirectResponse(request.url_for("admin:list", identity=self.identity), status_code=302) @action(name="reject_shooting", label="批量驳回", confirmation_message="确认将选中约拍设为「已驳回」?") async def action_reject(self, request: Request): _bulk_update_status(ShootingRequest, request.query_params.get("pks", ""), "audit_status", "rejected") return RedirectResponse(request.url_for("admin:list", identity=self.identity), status_code=302) class ShootingApplicationAdmin(ModelView, model=ShootingApplication): column_list = [ShootingApplication.id, "request", "applicant", ShootingApplication.status, ShootingApplication.created_at] column_sortable_list = [ShootingApplication.id, ShootingApplication.created_at] column_default_sort = ("id", True) can_create = False can_edit = False can_delete = True name = "约拍报名" name_plural = "约拍报名" icon = "fa-solid fa-handshake" column_labels = { ShootingApplication.id: "ID", "request": "约拍单", "applicant": "报名人", ShootingApplication.status: "状态", ShootingApplication.created_at: "报名时间", } column_formatters = { ShootingApplication.status: _status_badge, } EVENT_STATUS_CHOICES = [ ("upcoming", "即将开始"), ("ongoing", "进行中"), ("ended", "已结束"), ("cancelled", "已取消"), ] class EventAdmin(AuditedModelView, model=Event): column_list = [Event.id, Event.title, Event.city, "creator", Event.status, Event.audit_status, Event.registration_count, Event.start_time, Event.created_at] column_searchable_list = [Event.title, Event.city] column_sortable_list = [Event.id, Event.start_time, Event.created_at, Event.registration_count] column_default_sort = ("id", True) form_columns = ["title", "city", "description", "location_name", "status", "audit_status", "reject_reason", "max_participants"] can_delete = False name = "活动" name_plural = "活动管理" icon = "fa-solid fa-calendar-days" column_labels = { Event.id: "ID", Event.title: "标题", Event.city: "城市", "creator": "发起人", Event.status: "状态", Event.audit_status: "审核状态", Event.registration_count: "报名数", Event.start_time: "开始时间", Event.created_at: "创建时间", Event.description: "描述", Event.location_name: "地点名称", Event.reject_reason: "驳回原因", Event.max_participants: "人数上限", } form_overrides = _select_overrides("status", "audit_status") form_args = _select_args( status=EVENT_STATUS_CHOICES, audit_status=AUDIT_STATUS_CHOICES, ) form_ajax_refs = { "creator": {"fields": ("nickname", "phone", "email"), "order_by": "nickname"}, } column_formatters = { Event.status: _status_badge, Event.audit_status: _status_badge, } @action(name="approve_event", label="批量通过", confirmation_message="确认将选中活动设为「已通过」?") async def action_approve(self, request: Request): _bulk_update_status(Event, request.query_params.get("pks", ""), "audit_status", "approved") return RedirectResponse(request.url_for("admin:list", identity=self.identity), status_code=302) @action(name="reject_event", label="批量驳回", confirmation_message="确认将选中活动设为「已驳回」?") async def action_reject(self, request: Request): _bulk_update_status(Event, request.query_params.get("pks", ""), "audit_status", "rejected") return RedirectResponse(request.url_for("admin:list", identity=self.identity), status_code=302) class EventRegistrationAdmin(ModelView, model=EventRegistration): column_list = [EventRegistration.id, "event", "user", EventRegistration.status, EventRegistration.created_at] column_sortable_list = [EventRegistration.id, EventRegistration.created_at] column_default_sort = ("id", True) can_create = False can_edit = False can_delete = True name = "活动报名" name_plural = "活动报名" icon = "fa-solid fa-clipboard-check" column_labels = { EventRegistration.id: "ID", "event": "活动", "user": "用户", EventRegistration.status: "状态", EventRegistration.created_at: "报名时间", } class EventPhotoAdmin(ModelView, model=EventPhoto): column_list = [EventPhoto.id, "event", "uploader", EventPhoto.image_url, EventPhoto.created_at] column_sortable_list = [EventPhoto.id, EventPhoto.created_at] column_default_sort = ("id", True) can_create = False can_edit = False can_delete = True name = "活动照片" name_plural = "活动相册" icon = "fa-solid fa-images" column_labels = { EventPhoto.id: "ID", "event": "活动", "uploader": "上传者", EventPhoto.image_url: "图片", EventPhoto.created_at: "上传时间", } POSITION_CHOICES = [ ("home_banner", "首页Banner"), ("discover_top", "发现页顶部"), ("spot_detail", "地点详情页"), ] LINK_TYPE_CHOICES = [ ("spot", "地点"), ("event", "活动"), ("shooting", "约拍"), ("url", "外部链接"), ] class PromotionAdmin(AuditedModelView, model=Promotion): column_list = [Promotion.id, Promotion.title, Promotion.position, Promotion.is_active, Promotion.sort_order, Promotion.impressions, Promotion.clicks, Promotion.start_time, Promotion.end_time] column_searchable_list = [Promotion.title] column_sortable_list = [Promotion.id, Promotion.sort_order, Promotion.impressions, Promotion.clicks] column_default_sort = ("sort_order", False) name = "推广位" name_plural = "推广位管理" icon = "fa-solid fa-bullhorn" column_labels = { Promotion.id: "ID", Promotion.title: "标题", Promotion.image_url: "图片", Promotion.link_type: "链接类型", Promotion.link_id: "关联ID", "spot": "关联地点", "event": "关联活动", "shooting": "关联约拍", Promotion.link_url: "外部链接", Promotion.position: "展示位置", Promotion.sort_order: "排序", Promotion.is_active: "启用", Promotion.impressions: "曝光", Promotion.clicks: "点击", Promotion.start_time: "开始时间", Promotion.end_time: "结束时间", } form_columns = [ Promotion.title, Promotion.image_url, Promotion.link_type, "spot", "event", "shooting", Promotion.link_url, Promotion.position, Promotion.sort_order, Promotion.is_active, Promotion.start_time, Promotion.end_time, ] form_overrides = _select_overrides("position", "link_type") form_args = _select_args( position=POSITION_CHOICES, link_type=LINK_TYPE_CHOICES, ) form_ajax_refs = { "spot": {"fields": ("title", "city"), "order_by": "title"}, "event": {"fields": ("title", "city"), "order_by": "title"}, "shooting": {"fields": ("title", "city"), "order_by": "title"}, } column_formatters = { Promotion.is_active: _bool_badge, } async def on_model_change(self, data, model, is_created, request): model.link_id = None if model.link_type == "spot" and model.spot is not None: model.link_id = model.spot.id elif model.link_type == "event" and model.event is not None: model.link_id = model.event.id elif model.link_type == "shooting" and model.shooting is not None: model.link_id = model.shooting.id class MembershipPlanAdmin(AuditedModelView, model=MembershipPlan): column_list = [MembershipPlan.id, MembershipPlan.name, MembershipPlan.duration_days, MembershipPlan.price, MembershipPlan.is_active, MembershipPlan.sort_order] column_searchable_list = [MembershipPlan.name] column_sortable_list = [MembershipPlan.id, MembershipPlan.sort_order, MembershipPlan.price] column_default_sort = ("sort_order", False) name = "会员方案" name_plural = "会员方案" icon = "fa-solid fa-crown" column_labels = { MembershipPlan.id: "ID", MembershipPlan.name: "名称", MembershipPlan.description: "描述", MembershipPlan.duration_days: "天数", MembershipPlan.price: "价格", MembershipPlan.benefits: "权益说明", MembershipPlan.extra_uploads: "额外上传额度", MembershipPlan.extra_top_count: "额外置顶次数", MembershipPlan.is_active: "启用", MembershipPlan.sort_order: "排序", } column_formatters = { MembershipPlan.is_active: _bool_badge, } class UserMembershipAdmin(ModelView, model=UserMembership): column_list = [UserMembership.id, "user", "plan", UserMembership.start_date, UserMembership.end_date, UserMembership.is_active] column_sortable_list = [UserMembership.id, UserMembership.start_date, UserMembership.end_date] column_default_sort = ("id", True) can_create = False can_delete = False name = "会员记录" name_plural = "会员记录" icon = "fa-solid fa-id-card" column_labels = { UserMembership.id: "ID", "user": "用户", "plan": "方案", UserMembership.start_date: "开始", UserMembership.end_date: "到期", UserMembership.is_active: "有效", } column_formatters = { UserMembership.is_active: _bool_badge, } class AppNavConfigAdmin(AuditedModelView, model=AppNavConfig): column_list = [ AppNavConfig.id, AppNavConfig.key, AppNavConfig.label, AppNavConfig.page_path, AppNavConfig.icon, AppNavConfig.active_icon, AppNavConfig.color, AppNavConfig.active_color, AppNavConfig.is_active, AppNavConfig.sort_order, AppNavConfig.updated_at, ] column_searchable_list = [AppNavConfig.key, AppNavConfig.label, AppNavConfig.page_path] column_sortable_list = [AppNavConfig.id, AppNavConfig.sort_order, AppNavConfig.updated_at] column_default_sort = ("sort_order", False) name = "底栏配置" name_plural = "底栏配置" icon = "fa-solid fa-table-columns" column_labels = { AppNavConfig.id: "ID", AppNavConfig.key: "键", AppNavConfig.label: "文案", AppNavConfig.page_path: "页面路径", AppNavConfig.icon: "图标", AppNavConfig.active_icon: "激活图标", AppNavConfig.color: "默认颜色", AppNavConfig.active_color: "激活颜色", AppNavConfig.is_active: "启用", AppNavConfig.sort_order: "排序", AppNavConfig.updated_at: "更新时间", } column_formatters = { AppNavConfig.is_active: _bool_badge, }